The same Power Saver / Balanced / Performance choice GNOME's Power panel offers, and the daemon behind it was already running here -- it simply had no control anywhere in Panama. This machine has been sitting on "performance" with nothing to say so. Talks to the net.hadess.PowerProfiles interface rather than to a binary. Fedora 44 implements it with tuned-ppd instead of power-profiles-daemon, and powerprofilesctl is not installed at all, so anything shelling out to that command would have found nothing while the service was right there. Setting a profile needs no privileges: the daemon accepts a property write from the active session user. Not a stored preference. The daemon owns the profile, it survives Panama restarts, and anything else on the system can change it, so a copy in settings.json would drift -- the same reasoning as monitor brightness. PerformanceDegraded is surfaced because it is what makes the setting a lie: a thermally throttled machine reports "performance" while behaving otherwise, and that is worth saying out loud. The contract stubs busctl, because the real daemon is a system service shared with everything else on the machine and a test that flipped the daily driver to power-saver and then died would leave it there. It pins the parsing in particular: busctl renders the Profiles property flat, so profile names and driver names arrive in one stream, and a pattern loose enough to match both reports the driver as an extra profile. On this machine the driver is called "tuned", which reads exactly like a plausible fourth profile -- verified the contract catches that, and catches an unvalidated name reaching the system service. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
108 lines
3.8 KiB
QML
108 lines
3.8 KiB
QML
pragma Singleton
|
|
|
|
// The system power profile: power-saver, balanced, or performance.
|
|
//
|
|
// The same daemon GNOME's Power panel drives. On Fedora 44 the implementation
|
|
// is tuned-ppd rather than power-profiles-daemon, but it serves the same
|
|
// net.hadess.PowerProfiles interface -- so this talks to the interface, not to
|
|
// either binary. powerprofilesctl is not installed here at all.
|
|
//
|
|
// Not a stored preference. The profile lives in the daemon, survives Panama
|
|
// restarts, and can be changed by anything else on the system; keeping a copy
|
|
// in settings.json would mean restoring a value the daemon had moved past.
|
|
// Same reasoning as monitor brightness.
|
|
//
|
|
// Read on demand and after each change. The daemon does emit PropertiesChanged,
|
|
// but subscribing to it would mean holding a bus connection open for a value
|
|
// that changes only when someone chooses it.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-power-profile"
|
|
|
|
property var profiles: []
|
|
property string active: ""
|
|
property bool scanned: false
|
|
property bool busy: false
|
|
property string lastError: ""
|
|
|
|
// Non-empty when the machine cannot actually deliver the profile it is set
|
|
// to -- thermal throttling, or a laptop running on battery. Worth showing,
|
|
// because otherwise "performance" is a claim the hardware is not honouring.
|
|
property string degraded: ""
|
|
|
|
readonly property bool available: root.profiles.length > 0
|
|
|
|
// Presentation lives here rather than in the page so the Control Center and
|
|
// Settings cannot disagree about what a profile is called.
|
|
function label(profile: string): string {
|
|
switch (profile) {
|
|
case "power-saver": return "Power Saver";
|
|
case "balanced": return "Balanced";
|
|
case "performance": return "Performance";
|
|
default: return profile;
|
|
}
|
|
}
|
|
|
|
function detail(profile: string): string {
|
|
switch (profile) {
|
|
case "power-saver": return "Reduces performance to save energy and run quieter";
|
|
case "balanced": return "Standard behaviour, scaling up only when needed";
|
|
case "performance": return "Holds higher clocks, using more power and making more noise";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
function refresh(): void {
|
|
if (!query.running)
|
|
query.running = true;
|
|
}
|
|
|
|
function set(profile: string): void {
|
|
if (root.busy || profile === root.active)
|
|
return;
|
|
root.busy = true;
|
|
apply.command = [root.helperPath, "set", profile];
|
|
apply.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: query
|
|
command: [root.helperPath, "list"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const parsed = JSON.parse(this.text);
|
|
root.profiles = Array.isArray(parsed.profiles) ? parsed.profiles : [];
|
|
root.active = String(parsed.active ?? "");
|
|
root.degraded = String(parsed.degraded ?? "");
|
|
root.lastError = String(parsed.error ?? "");
|
|
} catch (error) {
|
|
root.profiles = [];
|
|
root.lastError = "Could not read the power profile helper's output.";
|
|
console.warn("PowerProfiles: could not parse helper output:", error);
|
|
}
|
|
root.scanned = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: apply
|
|
stderr: StdioCollector {
|
|
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
|
|
}
|
|
// Re-read rather than assuming: the daemon may refuse, or may land on a
|
|
// different profile than the one asked for.
|
|
onExited: {
|
|
root.busy = false;
|
|
root.refresh();
|
|
}
|
|
}
|
|
}
|