From 7ade51b92097c54935735fce2cacf967171c826b Mon Sep 17 00:00:00 2001 From: Matthias Puchstein Date: Sun, 17 May 2026 09:21:10 +0200 Subject: [PATCH] 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. --- dot_config/quickshell/Bar.qml | 2 +- .../quickshell/bar/popouts/SystemPopout.qml | 88 ++++++++++++++++++- dot_config/quickshell/shared/Config.qml.tmpl | 27 +++++- dot_config/quickshell/shared/Kubernetes.qml | 10 ++- 4 files changed, 116 insertions(+), 11 deletions(-) diff --git a/dot_config/quickshell/Bar.qml b/dot_config/quickshell/Bar.qml index f9a62f1..3b1f92c 100644 --- a/dot_config/quickshell/Bar.qml +++ b/dot_config/quickshell/Bar.qml @@ -124,7 +124,7 @@ Scope { ] } - BarComponents.KubernetesPill { id: kubernetesBtn } + BarComponents.KubernetesPill { id: kubernetesBtn; visible: Shared.Config.kubeEnabled } BarComponents.SystemPill { id: systemBtn } } diff --git a/dot_config/quickshell/bar/popouts/SystemPopout.qml b/dot_config/quickshell/bar/popouts/SystemPopout.qml index 13fe617..7b79cce 100644 --- a/dot_config/quickshell/bar/popouts/SystemPopout.qml +++ b/dot_config/quickshell/bar/popouts/SystemPopout.qml @@ -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); + } } } diff --git a/dot_config/quickshell/shared/Config.qml.tmpl b/dot_config/quickshell/shared/Config.qml.tmpl index bc42e91..4b80745 100644 --- a/dot_config/quickshell/shared/Config.qml.tmpl +++ b/dot_config/quickshell/shared/Config.qml.tmpl @@ -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"] } diff --git a/dot_config/quickshell/shared/Kubernetes.qml b/dot_config/quickshell/shared/Kubernetes.qml index 15df4f7..388610e 100644 --- a/dot_config/quickshell/shared/Kubernetes.qml +++ b/dot_config/quickshell/shared/Kubernetes.qml @@ -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; + } } }