quickshell: add laptop support (battery, power-profile, conditional GPU/k8s)

Template Config.qml with chezmoi data for multi-machine support. Surface
gets eDP-1 monitor, battery/power-profile widgets, no discrete GPU or
Kubernetes polling. Desktop behavior unchanged.
This commit is contained in:
Matthias Puchstein
2026-05-17 09:21:10 +02:00
parent 75e3b1bea1
commit 7ade51b920
4 changed files with 116 additions and 11 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ Scope {
]
}
BarComponents.KubernetesPill { id: kubernetesBtn }
BarComponents.KubernetesPill { id: kubernetesBtn; visible: Shared.Config.kubeEnabled }
BarComponents.SystemPill { id: systemBtn }
}
@@ -54,6 +54,10 @@ Item {
property string networkIp: "--"
property string networkIface: "--"
property bool idleActive: false
// Battery & power profile (laptop only)
property int batteryPercent: -1
property string batteryState: "" // "Charging", "Discharging", "Full", "Not charging"
property string powerProfile: "" // "power-saver", "balanced", "performance"
property var panelWindow: null
property string audioDrawer: "" // "" = closed, "sink" or "source"
@@ -290,6 +294,7 @@ Item {
}
MetricBar {
visible: Shared.Config.hasDiscreteGpu
label: "GPU"
value: root.gpuText
fill: root.gpuUsage / 100
@@ -298,6 +303,18 @@ Item {
history: root.gpuHistory
}
// Battery (laptop only)
MetricBar {
visible: Shared.Config.hasBattery && root.batteryPercent >= 0
label: root.batteryState === "Charging" ? "\u{f0084}" : "\u{f007a}"
value: root.batteryPercent + "%"
fill: root.batteryPercent / 100
barColor: root.batteryPercent <= 15 ? Shared.Theme.danger : root.batteryPercent <= 30 ? Shared.Theme.warning : Shared.Theme.success
valueColor: barColor
suffix: root.batteryState === "Charging" ? "CHG" : (root.batteryState === "Full" ? "FULL" : "")
suffixColor: Shared.Theme.subtext0
}
MetricBar {
label: "NVMe"
value: root.nvmeTempText
@@ -511,6 +528,48 @@ Item {
}
}
// ─── POWER PROFILE (laptop only) ────
RowLayout {
Layout.fillWidth: true
visible: Shared.Config.hasPowerProfiles
spacing: 6
Text { text: "\u{f0425}"; color: Shared.Theme.overlay0; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
Text { text: "Profile"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
Item { Layout.fillWidth: true }
Repeater {
model: [
{ name: "power-saver", icon: "\u{f19be}", color: Shared.Theme.green },
{ name: "balanced", icon: "\u{f0a01}", color: Shared.Theme.blue },
{ name: "performance", icon: "\u{f1b4b}", color: Shared.Theme.peach }
]
Rectangle {
required property var modelData
required property int index
width: 28; height: 28
radius: Shared.Theme.radiusSmall
color: root.powerProfile === modelData.name ? Shared.Theme.surface1 : (ppMouse.containsMouse ? Shared.Theme.surface0 : "transparent")
Text {
anchors.centerIn: parent
text: modelData.icon
color: root.powerProfile === modelData.name ? modelData.color : Shared.Theme.overlay0
font.pixelSize: 14
font.family: Shared.Theme.iconFont
}
MouseArea {
id: ppMouse
anchors.fill: parent
hoverEnabled: true
onClicked: root.setPowerProfile(modelData.name)
}
}
}
}
// ─── separator ───
Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 6; Layout.bottomMargin: 6 }
@@ -961,6 +1020,18 @@ Item {
onStreamFinished: { let l = this.text.trim(); if (l) { let p = l.split(":"); root.networkIface = p[0] || "--"; root.networkIp = p[1] || "--"; } else { root.networkIface = "Network"; root.networkIp = "Offline"; } }
}}
// Battery & power profile (laptop)
Process { id: batteryProc; command: ["sh", "-c", "cat /sys/class/power_supply/BAT*/capacity /sys/class/power_supply/BAT*/status 2>/dev/null"]; stdout: StdioCollector {
onStreamFinished: { let l = this.text.trim().split("\n"); if (l.length >= 2) { root.batteryPercent = parseInt(l[0]) || -1; root.batteryState = l[1].trim(); } }
}}
Process { id: ppGetProc; command: ["powerprofilesctl", "get"]; stdout: StdioCollector {
onStreamFinished: root.powerProfile = this.text.trim()
}}
Process { id: ppSetProc; command: ["true"]; stdout: StdioCollector {
onStreamFinished: { rerun(ppGetProc); }
}}
function setPowerProfile(profile) { ppSetProc.command = ["powerprofilesctl", "set", profile]; ppSetProc.running = true; }
Process { id: pavuProc; command: ["pavucontrol"] }
Process { id: idleProc; command: ["pgrep", "-x", Shared.Config.idleProcess]; stdout: StdioCollector { onStreamFinished: root.idleActive = this.text.trim().length > 0 } }
Process { id: idleKill; command: ["killall", Shared.Config.idleProcess] }
@@ -971,11 +1042,15 @@ Item {
Process { id: offProc; command: Shared.Config.powerActions[3].command }
function rerun(proc) { proc.running = false; proc.running = true; }
Timer { interval: 5000; running: true; repeat: true; onTriggered: { rerun(cpuProc); rerun(memProc); rerun(tempProc); rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); } }
Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); } }
Timer { interval: 5000; running: true; repeat: true; onTriggered: {
rerun(cpuProc); rerun(memProc); rerun(tempProc); rerun(nvmeTempProc); rerun(idleProc);
if (Shared.Config.hasDiscreteGpu) rerun(gpuProc);
if (Shared.Config.hasBattery) rerun(batteryProc);
}}
Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); if (Shared.Config.hasPowerProfiles) rerun(ppGetProc); } }
Timer { interval: 300000; running: true; repeat: true; onTriggered: { rerun(updateProc); rerun(alhpProc); } }
// Stagger initial launches to avoid 9 concurrent process spawns
// Stagger initial launches to avoid many concurrent process spawns
Component.onCompleted: {
rerun(cpuProc); rerun(memProc); rerun(tempProc);
staggerTimer.running = true;
@@ -983,6 +1058,11 @@ Item {
Timer {
id: staggerTimer
interval: 200
onTriggered: { rerun(gpuProc); rerun(nvmeTempProc); rerun(idleProc); rerun(diskProc); rerun(netProc); rerun(updateProc); rerun(alhpProc); }
onTriggered: {
if (Shared.Config.hasDiscreteGpu) rerun(gpuProc);
rerun(nvmeTempProc); rerun(idleProc); rerun(diskProc); rerun(netProc); rerun(updateProc); rerun(alhpProc);
if (Shared.Config.hasBattery) rerun(batteryProc);
if (Shared.Config.hasPowerProfiles) rerun(ppGetProc);
}
}
}
+25 -2
View File
@@ -1,3 +1,7 @@
{{- $tags := .chezmoi.config.data.tags -}}
{{- $monitors := .chezmoi.config.data.monitors -}}
{{- $isLaptop := index $tags "laptop" -}}
{{- $isDesktop := index $tags "desktop" -}}
pragma Singleton
import Quickshell
@@ -11,8 +15,12 @@ Singleton {
readonly property string apexFlavor: "{{ trimPrefix "apex-" .chezmoi.config.data.theme }}"
readonly property bool transparency: true // semi-transparent backgrounds (needs Hyprland blur layerrule)
// Monitor — quickshell bar lives on the rightmost monitor (portrait, DP-2)
// Monitor — which monitor the bar lives on
{{- if $isDesktop }}
readonly property string monitor: "DP-2"
{{- else }}
readonly property string monitor: "{{ (index $monitors 0).name }}"
{{- end }}
// Workspaces — show all 10; Hyprland IPC tracks per-monitor active workspace
readonly property int workspaceCount: 10
@@ -28,25 +36,40 @@ Singleton {
// Disk mounts to monitor
readonly property string diskMount1: "/"
readonly property string diskMount1Label: "/"
{{- if $isDesktop }}
readonly property string diskMount2: home + "/data"
readonly property string diskMount2Label: "/data"
{{- else }}
readonly property string diskMount2: home
readonly property string diskMount2Label: "~"
{{- end }}
// Hardware capabilities
readonly property bool hasDiscreteGpu: {{ if $isDesktop }}true{{ else }}false{{ end }}
readonly property bool hasBattery: {{ if $isLaptop }}true{{ else }}false{{ end }}
readonly property bool hasPowerProfiles: {{ if $isLaptop }}true{{ else }}false{{ end }}
// Scripts
readonly property string scriptsDir: home + "/.config/quickshell/scripts"
readonly property string gpuScript: scriptsDir + "/gpu.sh"
// Kubernetes
readonly property bool kubeEnabled: {{ if $isDesktop }}true{{ else }}false{{ end }}
readonly property string kubeNamespace: "tenant-5"
readonly property int kubeStatusRefreshMs: 30000
readonly property int kubeMetricsRefreshMs: 15000
// Idle daemon
readonly property string idleProcess: "hypridle"
readonly property string lockCommand: "swaylock"
readonly property string lockCommand: "{{ if $isLaptop }}hyprlock{{ else }}swaylock{{ end }}"
// Power commands
readonly property var powerActions: [
{{- if $isLaptop }}
{ command: ["hyprlock"] },
{{- else }}
{ command: ["swaylock"] },
{{- end }}
{ command: ["hyprshutdown"] },
{ command: ["hyprshutdown", "-t", "Restarting...", "--post-cmd", "systemctl reboot"] },
{ command: ["hyprshutdown", "-t", "Powering off...", "--post-cmd", "systemctl poweroff"] }
+6 -4
View File
@@ -81,7 +81,7 @@ Singleton {
// Status poller
Timer {
interval: Config.kubeStatusRefreshMs
running: true
running: Config.kubeEnabled
repeat: true
onTriggered: statusProc.running = true
}
@@ -89,7 +89,7 @@ Singleton {
// Metrics poller
Timer {
interval: Config.kubeMetricsRefreshMs
running: true
running: Config.kubeEnabled
repeat: true
onTriggered: metricsProc.running = true
}
@@ -114,7 +114,9 @@ Singleton {
}
Component.onCompleted: {
statusProc.running = true;
metricsStagger.running = true;
if (Config.kubeEnabled) {
statusProc.running = true;
metricsStagger.running = true;
}
}
}