diff --git a/Bar.qml b/Bar.qml index aa22fbb..74619a3 100644 --- a/Bar.qml +++ b/Bar.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Services.SystemTray import Quickshell.Services.Pipewire +import Quickshell.Services.UPower PanelWindow { id: root @@ -72,6 +73,8 @@ PanelWindow { case "niriWindows": return niriWindowsComp case "idleInhibit": return idleInhibitComp case "audio": return audioComp + case "battery": return batteryComp + case "wattage": return wattageComp case "tray": return trayComp case "clock": return clockComp default: return null @@ -128,9 +131,12 @@ PanelWindow { Component { id: audioComp Item { + id: audio implicitWidth: audioContent.width + 8 implicitHeight: 26 + property bool muted: Pipewire.defaultAudioSink?.audio.muted ?? false + Row { id: audioContent anchors.centerIn: parent @@ -138,10 +144,10 @@ PanelWindow { Text { anchors.verticalCenter: parent.verticalCenter - text: "󰓃" + text: audio.muted ? String.fromCodePoint(0xF04C4) : "󰓃" font.family: "Maple Mono" font.pixelSize: 16 - color: "white" + color: audio.muted ? "#888" : "white" } Text { @@ -150,7 +156,7 @@ PanelWindow { font.family: "Inter" font.pixelSize: 13 font.weight: 700 - color: "white" + color: audio.muted ? "#888" : "white" } } @@ -216,12 +222,14 @@ PanelWindow { Component { id: clockComp Item { - implicitWidth: root.px(120) + // Size to the text so the clock sits flush against the right margin + // (a fixed width + left-aligned text left dead space to the right edge). + implicitWidth: clockLabel.width implicitHeight: 26 Text { - anchors.fill: parent - verticalAlignment: Text.AlignVCenter + id: clockLabel + anchors.centerIn: parent color: "white" font.family: "Inter" font.pixelSize: 16 @@ -231,6 +239,108 @@ PanelWindow { } } + Component { + id: batteryComp + Item { + id: bat + property var dev: UPower.displayDevice + property bool present: dev && dev.isPresent + // Quickshell reports percentage as a 0.0-1.0 fraction; scale to 0-100. + property real pct: dev ? dev.percentage * 100 : 0 + property int st: dev ? dev.state : 0 + property bool charging: st === UPowerDeviceState.Charging || st === UPowerDeviceState.PendingCharge + property bool full: st === UPowerDeviceState.FullyCharged + property bool low: pct <= 15 && !charging && !full + + // Material Design (Nerd Font) battery ramp, reactive on pct/state. + readonly property string glyph: { + var p = Math.max(0, Math.min(100, pct)) + if (full) return String.fromCodePoint(0xF0079) + if (charging) { + var c = [0xF089C, 0xF0086, 0xF0087, 0xF0088, 0xF089D, + 0xF0089, 0xF089E, 0xF008A, 0xF008B, 0xF0085] + return String.fromCodePoint(c[Math.min(9, Math.floor(p / 10))]) + } + if (p >= 95) return String.fromCodePoint(0xF0079) + var d = [0xF007A, 0xF007B, 0xF007C, 0xF007D, 0xF007E, + 0xF007F, 0xF0080, 0xF0081, 0xF0082] + return String.fromCodePoint(d[Math.max(0, Math.min(8, Math.floor(p / 10) - 1))]) + } + + visible: present + implicitWidth: present ? batContent.width + 8 : 0 + implicitHeight: 26 + + Row { + id: batContent + anchors.centerIn: parent + spacing: 4 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: bat.glyph + font.family: "Maple Mono" + font.pixelSize: 16 + color: bat.low ? "#e06060" : (bat.charging || bat.full ? "#70c070" : "white") + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: Math.round(bat.pct) + "%" + font.family: "Inter" + font.pixelSize: 13 + font.weight: 700 + color: bat.low ? "#e06060" : "white" + } + } + } + } + + Component { + id: wattageComp + Item { + id: watt + property var dev: UPower.displayDevice + property bool present: dev && dev.isPresent + property real rate: dev ? dev.changeRate : 0 + property int st: dev ? dev.state : 0 + property bool charging: st === UPowerDeviceState.Charging || st === UPowerDeviceState.PendingCharge + property bool discharging: st === UPowerDeviceState.Discharging || st === UPowerDeviceState.PendingDischarge + // Show whenever the battery is actively charging or discharging, regardless of + // the instantaneous rate. The rate briefly reads 0 (e.g. just after (un)plugging, + // or when the EC returns an empty current_now), which would otherwise flicker the + // widget out during discharge. + property bool active: present && (charging || discharging) + + visible: active + implicitWidth: active ? wattContent.width + 8 : 0 + implicitHeight: 26 + + Row { + id: wattContent + anchors.centerIn: parent + spacing: 4 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: String.fromCodePoint(0xF140B) // lightning bolt + font.family: "Maple Mono" + font.pixelSize: 16 + color: watt.charging ? "#70c070" : "white" + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: (watt.charging ? "+" : "-") + watt.rate.toFixed(1) + " W" + font.family: "Inter" + font.pixelSize: 13 + font.weight: 700 + color: "white" + } + } + } + } + // --- sections -------------------------------------------------------------- // Order and placement come from Config; reordering is a config edit. @@ -251,13 +361,27 @@ PanelWindow { } } - WidgetRow { - widgets: Config.leftWidgets + // Left section is clipped to the space before the right cluster so a long window + // list truncates instead of overlapping the right-hand widgets. + Item { + id: leftContainer + clip: true + height: parent.height anchors { left: parent.left leftMargin: px(10) + right: rightRow.left + rightMargin: px(8) verticalCenter: parent.verticalCenter } + + WidgetRow { + widgets: Config.leftWidgets + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + } } WidgetRow { @@ -269,6 +393,7 @@ PanelWindow { } WidgetRow { + id: rightRow widgets: Config.rightWidgets anchors { right: parent.right diff --git a/README.md b/README.md index ffe3aa0..87752f3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A minimal top bar for [Quickshell](https://quickshell.outfoxxed.me/) on [Niri](h - **Window strip** — shows all windows on the current workspace, ordered by niri column position. Focused window is highlighted, unfocused are greyscaled. Click to focus. - **System tray** — full StatusNotifier support with native DBusMenu popups (including submenus on hover). - **Audio widget** — shows current default sink volume, scroll to adjust by 1%, click to open a sink picker powered by PipeWire directly. +- **Battery** — charge percentage with a level/charging icon (via UPower); turns red when low, green while charging. Hidden on machines with no battery. +- **Wattage** — live power draw/charge rate in watts (via UPower); shows `-` while discharging, `+` while charging. Hidden when idle or on desktops. - **Clock** — current time, updates every second. Multi-monitor aware — one bar instance per screen, each showing only that screen's windows.