diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 86d903d24..4f16ce658 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: PowerManager.currentProfile.charAt(0).toUpperCase() + PowerManager.currentProfile.slice(1) + function formatSeconds(s: int, fallback: string): string { const day = Math.floor(s / 86400); const hr = Math.floor(s / 3600) % 60; @@ -33,13 +36,14 @@ 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)) + 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 { asynchronous: true anchors.horizontalCenter: parent.horizontalCenter + // TODO: Change to use the abstraction (PowerManager in qs.services) active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 @@ -99,10 +103,10 @@ Column { id: profiles property string current: { - const p = PowerProfiles.profile; - if (p === PowerProfile.PowerSaver) + const p = PowerManager.currentProfile; + if (p === "saver") return saver.icon; - if (p === PowerProfile.Performance) + if (p === "performance") return perf.icon; return balance.icon; } @@ -162,7 +166,7 @@ Column { anchors.left: parent.left anchors.leftMargin: Appearance.padding.small - profile: PowerProfile.PowerSaver + profile: "saver" icon: "energy_savings_leaf" } @@ -171,7 +175,7 @@ Column { anchors.centerIn: parent - profile: PowerProfile.Balanced + profile: "balanced" icon: "balance" } @@ -182,7 +186,7 @@ Column { anchors.right: parent.right anchors.rightMargin: Appearance.padding.small - profile: PowerProfile.Performance + profile: "performance" icon: "rocket_launch" } } @@ -199,14 +203,14 @@ 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 StateLayer { function onClicked(): void { - PowerProfiles.profile = parent.profile; + PowerManager.setProfile(parent.profile); } radius: Appearance.rounding.full diff --git a/services/PowerManager.qml b/services/PowerManager.qml new file mode 100644 index 000000000..ee0f6ef52 --- /dev/null +++ b/services/PowerManager.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"; + } +}