Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions modules/bar/popouts/Battery.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -162,7 +166,7 @@ Column {
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.small

profile: PowerProfile.PowerSaver
profile: "saver"
icon: "energy_savings_leaf"
}

Expand All @@ -171,7 +175,7 @@ Column {

anchors.centerIn: parent

profile: PowerProfile.Balanced
profile: "balanced"
icon: "balance"
}

Expand All @@ -182,7 +186,7 @@ Column {
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.small

profile: PowerProfile.Performance
profile: "performance"
icon: "rocket_launch"
}
}
Expand All @@ -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
Expand Down
93 changes: 93 additions & 0 deletions services/PowerManager.qml
Original file line number Diff line number Diff line change
@@ -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";
}
}
Loading