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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user