From b1fb589b2e4f136b39a77d385f2ece03e14913de Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:19:01 +0800 Subject: [PATCH 1/5] battery: Integrated tlp for power profile switching + Implemented the ability to switch between power profiles depending on the power management backend, in this case, tlp or power-profiles-daemon (ppd). + Added an abstraction layer that checks whether the user uses tlp or ppd, and depending on which one, it will use the corresponding commands for that backend to switch between power profiles. + The abstraction layer can be potentially expanded upon to include more commands from tlp to be hooked to some tab in the settings to leverage its insane battery customizability. Some Limitations: + If one changes the power management backend for some reason, the shell will not react to it (it's a waste of performance to make it so). The shells needs to be restarted to make it see the switch. + Switching power profiles while on tlp will require a password prompt to gain sudo because tlp works differently from ppd, because of this, the UX may feel off. The only solution to this is to make the user (or installation script) modify the sudoer file so tlp can just run unrestricted. I'll leave this as is for now tho. --- modules/bar/popouts/Battery.qml | 25 ++++----- services/Power.qml | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 services/Power.qml diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index ac975e1b7..a9bece321 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -33,12 +33,16 @@ Column { return comps.join(", ") || fallback; } - text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile)) + // Capitalize the first letter of the generic profile string for UI display + property string displayProfile: Power.currentProfile.charAt(0).toUpperCase() + Power.currentProfile.slice(1) + + text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(displayProfile) } Loader { anchors.horizontalCenter: parent.horizontalCenter + // TODO: Change to use the abstraction (Power in qs.services) active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None height: active ? (item?.implicitHeight ?? 0) : 0 @@ -98,10 +102,10 @@ Column { id: profiles property string current: { - const p = PowerProfiles.profile; - if (p === PowerProfile.PowerSaver) + const p = Power.currentProfile; + if (p === "saver") return saver.icon; - if (p === PowerProfile.Performance) + if (p === "performance") return perf.icon; return balance.icon; } @@ -124,21 +128,18 @@ Column { states: [ State { name: saver.icon - Fill { item: saver } }, State { name: balance.icon - Fill { item: balance } }, State { name: perf.icon - Fill { item: perf } @@ -161,7 +162,7 @@ Column { anchors.left: parent.left anchors.leftMargin: Appearance.padding.small - profile: PowerProfile.PowerSaver + profile: "saver" icon: "energy_savings_leaf" } @@ -170,7 +171,7 @@ Column { anchors.centerIn: parent - profile: PowerProfile.Balanced + profile: "balanced" icon: "balance" } @@ -181,7 +182,7 @@ Column { anchors.right: parent.right anchors.rightMargin: Appearance.padding.small - profile: PowerProfile.Performance + profile: "performance" icon: "rocket_launch" } } @@ -198,7 +199,7 @@ Column { component Profile: Item { required property string icon - required property int profile + required property string profile implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 @@ -208,7 +209,7 @@ Column { color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface function onClicked(): void { - PowerProfiles.profile = parent.profile; + Power.setProfile(parent.profile); } } diff --git a/services/Power.qml b/services/Power.qml new file mode 100644 index 000000000..16524cb58 --- /dev/null +++ b/services/Power.qml @@ -0,0 +1,93 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.UPower + +// An abstraction layer for emitting commands from different power management backends. +Singleton { + id: root + + // --- GENERIC STATE --- + // Expose generic string properties that UI can bind to + property string activeBackend: "unknown" // Will be "ppd" (power-profiles-daemon) or "tlp" + property string currentProfile: "balanced" // "saver", "balanced", or "performance" + + // --- INITIALIZATION & DETECTION --- + Component.onCompleted: { + // Run a background check to see which daemon is active. + checkBackendProc.running = true; + } + + // Checks the which power management backend is in use + Process { + id: checkBackendProc + command: ["systemctl", "is-active", "tlp"] + stdout: StdioCollector { + id: tlpCollector + onStreamFinished: { + if (tlpCollector.text.trim() === "active") + root.activeBackend = "tlp"; + else { + root.activeBackend = "ppd"; + // Only sync if PowerProfiles is actually alive + if (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== undefined) { + root.currentProfile = mapPpdToGeneric(PowerProfiles.profile); + } + } + } + } + } + + // A dedicated process for running TLP commands --- + Process { + id: tlpProcess + // Custom property to remember what we are trying to switch to + property string pendingProfile: "" + + onExited: exitCode => { + // Exit code 0 means the password was correct and the command succeeded + if (exitCode === 0) + root.currentProfile = pendingProfile; + } + } + + // --- ABSTRACTION METHODS --- + function setProfile(targetProfile) { + switch (activeBackend) { + case ("ppd"): + // PPD is instant and requires no password, so we can safely update the UI immediately + root.currentProfile = targetProfile; + + if (targetProfile === "saver") + PowerProfiles.profile = PowerProfile.PowerSaver; + else if (targetProfile === "performance") + PowerProfiles.profile = PowerProfile.Performance; + else + PowerProfiles.profile = PowerProfile.Balanced; + break; + case ("tlp"): + // Store the profile we want to switch to + tlpProcess.pendingProfile = targetProfile; + + let tlpCommandArg = "balanced"; + if (targetProfile === "saver") + tlpCommandArg = "power-saver"; + if (targetProfile === "performance") + tlpCommandArg = "performance"; + tlpProcess.command = ["pkexec", "tlp", tlpCommandArg]; + tlpProcess.running = true; + break; + } + } + + // --- HELPER METHODS --- + function mapPpdToGeneric(ppdProfile) { + if (ppdProfile === PowerProfile.PowerSaver) + return "saver"; + if (ppdProfile === PowerProfile.Performance) + return "performance"; + return "balanced"; + } +} From f039791dae13fdc66bbf3f2d10a3eed9720814b2 Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:04:32 +0800 Subject: [PATCH 2/5] refactor: formatting --- modules/bar/popouts/Battery.qml | 9 ++++++--- services/Power.qml | 12 +++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index fb80bc192..a8b90e2f8 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -17,6 +17,9 @@ Column { } StyledText { + // Capitalize the first letter of the generic profile string for UI display + property string displayProfile: Power.currentProfile.charAt(0).toUpperCase() + Power.currentProfile.slice(1) + function formatSeconds(s: int, fallback: string): string { const day = Math.floor(s / 86400); const hr = Math.floor(s / 3600) % 60; @@ -33,9 +36,6 @@ Column { return comps.join(", ") || fallback; } - // Capitalize the first letter of the generic profile string for UI display - property string displayProfile: Power.currentProfile.charAt(0).toUpperCase() + Power.currentProfile.slice(1) - text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(displayProfile) } @@ -129,18 +129,21 @@ Column { states: [ State { name: saver.icon + Fill { item: saver } }, State { name: balance.icon + Fill { item: balance } }, State { name: perf.icon + Fill { item: perf } diff --git a/services/Power.qml b/services/Power.qml index 16524cb58..8e13d5a1e 100644 --- a/services/Power.qml +++ b/services/Power.qml @@ -1,5 +1,4 @@ pragma Singleton - import QtQuick import Quickshell import Quickshell.Io @@ -13,19 +12,19 @@ Singleton { // Expose generic string properties that UI can bind to property string activeBackend: "unknown" // Will be "ppd" (power-profiles-daemon) or "tlp" property string currentProfile: "balanced" // "saver", "balanced", or "performance" - // --- INITIALIZATION & DETECTION --- Component.onCompleted: { // Run a background check to see which daemon is active. checkBackendProc.running = true; } - // Checks the which power management backend is in use Process { id: checkBackendProc + command: ["systemctl", "is-active", "tlp"] stdout: StdioCollector { id: tlpCollector + onStreamFinished: { if (tlpCollector.text.trim() === "active") root.activeBackend = "tlp"; @@ -39,27 +38,24 @@ Singleton { } } } - // A dedicated process for running TLP commands --- Process { id: tlpProcess + // Custom property to remember what we are trying to switch to property string pendingProfile: "" - onExited: exitCode => { // Exit code 0 means the password was correct and the command succeeded if (exitCode === 0) root.currentProfile = pendingProfile; } } - // --- ABSTRACTION METHODS --- function setProfile(targetProfile) { switch (activeBackend) { case ("ppd"): // PPD is instant and requires no password, so we can safely update the UI immediately root.currentProfile = targetProfile; - if (targetProfile === "saver") PowerProfiles.profile = PowerProfile.PowerSaver; else if (targetProfile === "performance") @@ -70,7 +66,6 @@ Singleton { case ("tlp"): // Store the profile we want to switch to tlpProcess.pendingProfile = targetProfile; - let tlpCommandArg = "balanced"; if (targetProfile === "saver") tlpCommandArg = "power-saver"; @@ -81,7 +76,6 @@ Singleton { break; } } - // --- HELPER METHODS --- function mapPpdToGeneric(ppdProfile) { if (ppdProfile === PowerProfile.PowerSaver) From 6a454df1b5c772cd7f21ff8ac4675f422830de1c Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:34:42 +0800 Subject: [PATCH 3/5] refactor: formatting --- services/Power.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/Power.qml b/services/Power.qml index 8e13d5a1e..42bd7c28b 100644 --- a/services/Power.qml +++ b/services/Power.qml @@ -12,11 +12,13 @@ Singleton { // Expose generic string properties that UI can bind to property string activeBackend: "unknown" // Will be "ppd" (power-profiles-daemon) or "tlp" property string currentProfile: "balanced" // "saver", "balanced", or "performance" + // --- INITIALIZATION & DETECTION --- Component.onCompleted: { // Run a background check to see which daemon is active. checkBackendProc.running = true; } + // Checks the which power management backend is in use Process { id: checkBackendProc @@ -44,6 +46,7 @@ Singleton { // Custom property to remember what we are trying to switch to property string pendingProfile: "" + onExited: exitCode => { // Exit code 0 means the password was correct and the command succeeded if (exitCode === 0) From 6ad827c97069a399bff6c24967a2c7aea0b01a56 Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:52:19 +0800 Subject: [PATCH 4/5] refactor: formatting...... --- services/Power.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/Power.qml b/services/Power.qml index 42bd7c28b..ee0f6ef52 100644 --- a/services/Power.qml +++ b/services/Power.qml @@ -40,6 +40,7 @@ Singleton { } } } + // A dedicated process for running TLP commands --- Process { id: tlpProcess @@ -53,6 +54,7 @@ Singleton { root.currentProfile = pendingProfile; } } + // // --- ABSTRACTION METHODS --- function setProfile(targetProfile) { switch (activeBackend) { @@ -79,6 +81,7 @@ Singleton { break; } } + // --- HELPER METHODS --- function mapPpdToGeneric(ppdProfile) { if (ppdProfile === PowerProfile.PowerSaver) From 32aaccd2e818aa4c7242c2fd29e5a02b8c68fbfc Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:55:21 +0800 Subject: [PATCH 5/5] refactor: Renamed Power to PowerManager --- modules/bar/popouts/Battery.qml | 8 ++++---- services/{Power.qml => PowerManager.qml} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename services/{Power.qml => PowerManager.qml} (100%) diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 933e34e66..4f16ce658 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -18,7 +18,7 @@ Column { StyledText { // Capitalize the first letter of the generic profile string for UI display - property string displayProfile: Power.currentProfile.charAt(0).toUpperCase() + Power.currentProfile.slice(1) + property string displayProfile: PowerManager.currentProfile.charAt(0).toUpperCase() + PowerManager.currentProfile.slice(1) function formatSeconds(s: int, fallback: string): string { const day = Math.floor(s / 86400); @@ -43,7 +43,7 @@ Column { asynchronous: true anchors.horizontalCenter: parent.horizontalCenter - // TODO: Change to use the abstraction (Power in qs.services) + // TODO: Change to use the abstraction (PowerManager in qs.services) active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 @@ -103,7 +103,7 @@ Column { id: profiles property string current: { - const p = Power.currentProfile; + const p = PowerManager.currentProfile; if (p === "saver") return saver.icon; if (p === "performance") @@ -210,7 +210,7 @@ Column { StateLayer { function onClicked(): void { - Power.setProfile(parent.profile); + PowerManager.setProfile(parent.profile); } radius: Appearance.rounding.full diff --git a/services/Power.qml b/services/PowerManager.qml similarity index 100% rename from services/Power.qml rename to services/PowerManager.qml