quickshell: upower battery, event-driven network, minute clock, stylix font, wifi glyphs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-06-11 11:03:24 +01:00
parent 77fca92c5c
commit 4d52da994c
2 changed files with 116 additions and 150 deletions

View file

@ -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