From 4d52da994c7e251413d0f6dab5236dde5177c03a Mon Sep 17 00:00:00 2001 From: rope Date: Thu, 11 Jun 2026 11:03:24 +0100 Subject: [PATCH] quickshell: upower battery, event-driven network, minute clock, stylix font, wifi glyphs Co-Authored-By: Claude Fable 5 --- hosts/FredOS-Macbook.nix | 2 + settings/quickshell.nix | 264 +++++++++++++++++---------------------- 2 files changed, 116 insertions(+), 150 deletions(-) diff --git a/hosts/FredOS-Macbook.nix b/hosts/FredOS-Macbook.nix index 159e679..d2895d8 100644 --- a/hosts/FredOS-Macbook.nix +++ b/hosts/FredOS-Macbook.nix @@ -21,6 +21,8 @@ services.tlp.enable = false; services.power-profiles-daemon.enable = true; + # Quickshell's battery widget reads org.freedesktop.UPower over DBus + services.upower.enable = true; boot.loader.systemd-boot.configurationLimit = 5; boot.initrd.systemd.enable = true; diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 75e4040..10773aa 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -12,7 +12,7 @@ in qt6.qt5compat # Qt5Compat.GraphicalEffects in Bar.qml ]; - home-manager.users.fred = { config, lib, pkgs, ... }: + home-manager.users.fred = { config, lib, pkgs, osConfig, ... }: let c = config.lib.stylix.colors; in { @@ -41,7 +41,8 @@ in [ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw" ''; nmcli = "${pkgs.networkmanager}/bin/nmcli"; - powerprofilesctl = "${pkgs.power-profiles-daemon}/bin/powerprofilesctl"; + # Follow stylix's monospace choice so a font swap propagates to the bar + monoFont = osConfig.stylix.fonts.monospace.name; in { "quickshell/qmldir" = { onChange = qsRestart; @@ -73,6 +74,7 @@ in readonly property color base0D: "#${c.base0D}" readonly property color barBg: "#B3${c.base00}" readonly property color toastBg: "#E6${c.base00}" + readonly property string fontFamily: "${monoFont}" } ''; }; @@ -86,7 +88,6 @@ in QtObject { readonly property string nmcli: "${nmcli}" readonly property string wifiConnect: "${wifiConnectScript}" - readonly property string powerprofilesctl: "${powerprofilesctl}" readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play" readonly property string hyprlock: "${pkgs.hyprlock}/bin/hyprlock" readonly property string systemctl: "${pkgs.systemd}/bin/systemctl" @@ -278,7 +279,7 @@ in anchors.rightMargin: 12 verticalAlignment: TextInput.AlignVCenter color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 clip: true onTextChanged: list.currentIndex = 0 @@ -297,7 +298,7 @@ in visible: search.text === "" text: root.mode === "power" ? "Power" : "Search" color: Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } } @@ -338,7 +339,7 @@ in anchors.verticalCenter: parent.verticalCenter text: root.mode === "power" ? modelData.glyph : "" color: Theme.base0D - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 14 } @@ -346,7 +347,7 @@ in anchors.verticalCenter: parent.verticalCenter text: modelData.name color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 elide: Text.ElideRight width: 270 @@ -376,6 +377,7 @@ in import Quickshell.Services.SystemTray import Quickshell.Services.Notifications import Quickshell.Services.Pipewire + import Quickshell.Services.UPower import Quickshell.Widgets import Quickshell.Io import QtQuick @@ -503,7 +505,7 @@ in anchors.centerIn: parent text: modelData.name color: modelData.focused ? Theme.base05 : Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } @@ -524,25 +526,24 @@ in } } - // Center — clock + // Center — clock. SystemClock ticks on the minute boundary + // instead of a 1 Hz Timer; the calendar reads clockText.now too. + SystemClock { + id: sysClock + precision: SystemClock.Minutes + } + Text { id: clockText anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: barBgRect.verticalCenter - property date now: new Date() + property date now: sysClock.date text: now.toLocaleTimeString(Qt.locale(), "HH:mm") color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: clockText.now = new Date() - } - MouseArea { anchors.fill: parent hoverEnabled: true @@ -595,7 +596,7 @@ in anchors.verticalCenter: parent.verticalCenter text: volWidget.volIcon + " " + volWidget.vol + "%" color: volWidget.muted ? Theme.base03 : Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } @@ -639,8 +640,33 @@ in property string _pendingDevice: "" property var _pendingNets: [] + // Event-driven: `nmcli monitor` prints a line on every + // NetworkManager event; debounce bursts into one refresh. + Process { + id: netMonitor + command: [Commands.nmcli, "monitor"] + running: true + stdout: SplitParser { + onRead: data => netRefreshDebounce.restart() + } + onRunningChanged: if (!running) netMonitorRestart.start() + } + Timer { + id: netRefreshDebounce + interval: 500 + onTriggered: netWidget.refreshNet() + } + + Timer { + id: netMonitorRestart interval: 5000 + onTriggered: netMonitor.running = true + } + + // Slow fallback poll in case the monitor dies quietly + Timer { + interval: 60000 running: true repeat: true triggeredOnStart: true @@ -694,7 +720,7 @@ in anchors.centerIn: parent text: netWidget.netIcon color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 14 } @@ -770,87 +796,31 @@ in width: batteryText.width + 4 + batteryIconText.width height: 30 - property int batteryLevel: 0 - property bool charging: false - property string batteryIcon: "\u{f008e}" - property real powerDraw: 0.0 - property real energyNow: 0.0 - property real energyFull: 0.0 - property string timeRemaining: "" - property string powerProfile: "balanced" - - function updateIcon() { - if (charging) { batteryIcon = "\u{f0084}"; return; } - if (batteryLevel >= 90) batteryIcon = "\u{f0079}"; - else if (batteryLevel >= 70) batteryIcon = "\u{f0082}"; - else if (batteryLevel >= 50) batteryIcon = "\u{f007f}"; - else if (batteryLevel >= 30) batteryIcon = "\u{f007c}"; - else if (batteryLevel >= 15) batteryIcon = "\u{f007a}"; - else batteryIcon = "\u{f008e}"; - } - - Timer { - interval: 5000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: { batteryProc.running = true; profileProc.running = true; } - } - - Process { - id: batteryProc - command: ["sh", "-c", "cat /sys/class/power_supply/BAT0/capacity; cat /sys/class/power_supply/BAT0/status; cat /sys/class/power_supply/BAT0/power_now 2>/dev/null || echo 0; cat /sys/class/power_supply/BAT0/energy_now 2>/dev/null || echo 0; cat /sys/class/power_supply/BAT0/energy_full 2>/dev/null || echo 0"] - stdout: SplitParser { - property int lineNum: 0 - onRead: data => { - let trimmed = data.trim(); - let num = parseInt(trimmed); - lineNum++; - if (lineNum === 1) { - if (!isNaN(num)) batteryWidget.batteryLevel = num; - } else if (lineNum === 2) { - batteryWidget.charging = (trimmed === "Charging"); - } else if (lineNum === 3) { - if (!isNaN(num)) batteryWidget.powerDraw = num / 1000000.0; - } else if (lineNum === 4) { - if (!isNaN(num)) batteryWidget.energyNow = num / 1000000.0; - } else if (lineNum === 5) { - if (!isNaN(num)) batteryWidget.energyFull = num / 1000000.0; - lineNum = 0; - if (batteryWidget.powerDraw > 0.5) { - let hours; - if (batteryWidget.charging) { - hours = (batteryWidget.energyFull - batteryWidget.energyNow) / batteryWidget.powerDraw; - } else { - hours = batteryWidget.energyNow / batteryWidget.powerDraw; - } - let h = Math.floor(hours); - let m = Math.round((hours - h) * 60); - batteryWidget.timeRemaining = h + "h " + m + "m"; - } else { - batteryWidget.timeRemaining = ""; - } - } - batteryWidget.updateIcon(); - } - } - } - - Process { - id: profileProc - command: [Commands.powerprofilesctl, "get"] - stdout: SplitParser { - onRead: data => { - batteryWidget.powerProfile = data.trim(); - } - } - } - - Process { - id: setProfileProc - property string target: "balanced" - command: [Commands.powerprofilesctl, "set", target] + // Live DBus-driven properties from the UPower service — + // no polling, no /sys parsing, no subprocess spawns. + property var dev: UPower.displayDevice + property int batteryLevel: dev && dev.ready ? Math.round(dev.percentage * 100) : 0 + property bool charging: dev ? dev.state === UPowerDeviceState.Charging : false + property real powerDraw: dev ? Math.abs(dev.changeRate) : 0.0 + property string timeRemaining: { + if (!dev) return ""; + let secs = charging ? dev.timeToFull : dev.timeToEmpty; + if (!secs || secs <= 0) return ""; + let h = Math.floor(secs / 3600); + let m = Math.round((secs % 3600) / 60); + return h + "h " + m + "m"; } + property string powerProfile: + PowerProfiles.profile === PowerProfile.PowerSaver ? "power-saver" + : PowerProfiles.profile === PowerProfile.Performance ? "performance" + : "balanced" + property string batteryIcon: charging ? "\u{f0084}" + : batteryLevel >= 90 ? "\u{f0079}" + : batteryLevel >= 70 ? "\u{f0082}" + : batteryLevel >= 50 ? "\u{f007f}" + : batteryLevel >= 30 ? "\u{f007c}" + : batteryLevel >= 15 ? "\u{f007a}" + : "\u{f008e}" Row { anchors.verticalCenter: parent.verticalCenter @@ -862,7 +832,7 @@ in color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } @@ -872,15 +842,13 @@ in color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 14 } } function openBatteryDropdown() { bar.toggleDropdown(batteryDropdown, function() { - batteryProc.running = true; - profileProc.running = true; let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0); batteryDropdown.dropdownX = pos.x; }); @@ -1252,7 +1220,7 @@ in Layout.fillWidth: true text: modelData.text ?? "" color: modelData.enabled ? Theme.base05 : Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 elide: Text.ElideRight } @@ -1261,7 +1229,7 @@ in visible: modelData.buttonType !== QsMenuButtonType.None text: modelData.checkState === Qt.Checked ? "\u2713" : "" color: Theme.base0D - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 } } @@ -1299,7 +1267,7 @@ in Text { text: "\u{f057e} Master" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium } @@ -1342,7 +1310,7 @@ in width: 36 text: volWidget.vol + "%" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 horizontalAlignment: Text.AlignRight anchors.verticalCenter: parent.verticalCenter @@ -1360,7 +1328,7 @@ in anchors.centerIn: parent text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 } MouseArea { @@ -1389,7 +1357,7 @@ in visible: appStreamsCol.childrenRect.height > 0 text: "\u{f0641} Applications" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium } @@ -1417,7 +1385,7 @@ in Text { text: modelData.properties["application.name"] || modelData.name || "Unknown" color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 elide: Text.ElideRight width: parent.width @@ -1461,7 +1429,7 @@ in width: 36 text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%" color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 10 horizontalAlignment: Text.AlignRight anchors.verticalCenter: parent.verticalCenter @@ -1492,7 +1460,7 @@ in ? "\u{f05a9} " + netWidget.netConn : "\u{f05aa} Not connected" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium elide: Text.ElideRight @@ -1509,7 +1477,7 @@ in anchors.centerIn: parent text: "Disconnect" color: Theme.base08 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 } @@ -1539,7 +1507,7 @@ in Text { text: "Available networks" color: Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 topPadding: 2 } @@ -1565,13 +1533,13 @@ in Text { text: { let s = modelData.signal; - if (s >= 75) return "\u{f05a9}"; - if (s >= 50) return "\u{f05a9}"; - if (s >= 25) return "\u{f05a9}"; - return "\u{f05aa}"; + if (s >= 75) return "\u{f0928}"; // strength 4 + if (s >= 50) return "\u{f0925}"; // strength 3 + if (s >= 25) return "\u{f0922}"; // strength 2 + return "\u{f091f}"; // strength 1 } color: modelData.active ? Theme.base0B : Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 anchors.verticalCenter: parent.verticalCenter } @@ -1579,7 +1547,7 @@ in Text { text: modelData.ssid color: modelData.active ? Theme.base0B : Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 elide: Text.ElideRight width: 140 @@ -1590,7 +1558,7 @@ in visible: modelData.security !== "" && modelData.security !== "--" text: "\u{f0341}" color: Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 10 anchors.verticalCenter: parent.verticalCenter } @@ -1635,7 +1603,7 @@ in Text { text: batteryWidget.batteryIcon color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 18 anchors.verticalCenter: parent.verticalCenter } @@ -1645,7 +1613,7 @@ in Text { text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " — Charging" : "") color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium } @@ -1653,7 +1621,7 @@ in text: batteryWidget.powerDraw.toFixed(1) + " W" + (batteryWidget.timeRemaining !== "" ? " \u2022 " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "") color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 } } @@ -1669,7 +1637,7 @@ in Text { text: "Power Profile" color: Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 } @@ -1679,9 +1647,9 @@ in Repeater { model: [ - { name: "power-saver", label: "\u{f0425}", tip: "Saver" }, - { name: "balanced", label: "\u{f0376}", tip: "Balanced" }, - { name: "performance", label: "\u{f0e0e}", tip: "Performance" } + { name: "power-saver", profile: PowerProfile.PowerSaver, label: "\u{f0425}", tip: "Saver" }, + { name: "balanced", profile: PowerProfile.Balanced, label: "\u{f0376}", tip: "Balanced" }, + { name: "performance", profile: PowerProfile.Performance, label: "\u{f0e0e}", tip: "Performance" } ] Rectangle { @@ -1703,14 +1671,14 @@ in text: modelData.label color: batteryWidget.powerProfile === modelData.name ? Theme.base0D : Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 14 } Text { anchors.horizontalCenter: parent.horizontalCenter text: modelData.tip color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 9 } } @@ -1719,11 +1687,7 @@ in id: profMouse anchors.fill: parent hoverEnabled: true - onClicked: { - setProfileProc.target = modelData.name; - setProfileProc.running = true; - batteryWidget.powerProfile = modelData.name; - } + onClicked: PowerProfiles.profile = modelData.profile } } } @@ -1756,7 +1720,7 @@ in anchors.horizontalCenter: parent.horizontalCenter text: clockText.now.toLocaleDateString(Qt.locale(), "dddd, d MMMM yyyy") color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 16 font.weight: Font.Medium } @@ -1773,7 +1737,7 @@ in horizontalAlignment: Text.AlignHCenter text: modelData color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } } @@ -1819,7 +1783,7 @@ in let dayNum = parent.index - startDay + 1; return (dayNum === d.getDate()) ? Theme.base05 : Theme.base04; } - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 } } @@ -1839,7 +1803,7 @@ in Text { text: "Notifications" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium } @@ -1848,7 +1812,7 @@ in anchors.right: parent.right text: bar.notifServer.trackedNotifications.values.length > 0 ? "Clear all" : "" color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 MouseArea { anchors.fill: parent @@ -1872,7 +1836,7 @@ in visible: bar.notifServer.trackedNotifications.values.length === 0 text: "No notifications" color: Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 anchors.horizontalCenter: parent.horizontalCenter } @@ -1900,7 +1864,7 @@ in width: parent.width text: notifItem.modelData.summary || notifItem.modelData.appName color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 font.weight: Font.Medium elide: Text.ElideRight @@ -1910,7 +1874,7 @@ in width: parent.width text: notifItem.modelData.body || "" color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 10 elide: Text.ElideRight maximumLineCount: 2 @@ -1936,7 +1900,7 @@ in anchors.centerIn: parent text: modelData.text color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 10 } MouseArea { @@ -1958,7 +1922,7 @@ in anchors.margins: 6 text: "\u{f0156}" color: dismissMa.containsMouse ? Theme.base05 : Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 MouseArea { id: dismissMa @@ -2145,7 +2109,7 @@ in width: parent.width text: toastItem.currentNotif ? (toastItem.currentNotif.summary || toastItem.currentNotif.appName) : "" color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 12 font.weight: Font.Medium elide: Text.ElideRight @@ -2155,7 +2119,7 @@ in width: parent.width text: toastItem.currentNotif ? (toastItem.currentNotif.body || "") : "" color: Theme.base04 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 11 elide: Text.ElideRight maximumLineCount: 3 @@ -2181,7 +2145,7 @@ in anchors.centerIn: parent text: modelData.text color: Theme.base05 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 10 } MouseArea { @@ -2203,7 +2167,7 @@ in anchors.margins: 8 text: "\u{f0156}" color: toastDismissMa.containsMouse ? Theme.base05 : Theme.base03 - font.family: "FiraMono Nerd Font" + font.family: Theme.fontFamily font.pixelSize: 13 MouseArea { id: toastDismissMa