Draw idle as one timeline, and let the power button answer to its owner

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 20:33:44 -04:00
parent 6f0ce639d9
commit e1ff25fc66
23 changed files with 2655 additions and 174 deletions
@@ -24,6 +24,28 @@ import QtQuick
import qs.config
Singleton {
// Whether logind says this machine can resume from disk. Probed once --
// the answer changes only when swap is reconfigured. The power menu keeps
// its own copy of this probe for its Hibernate entry; this one exists so
// the Power page can state the machine's answer rather than describing
// the menu's behavior from a distance.
property bool canHibernate: false
property bool canHibernateKnown: false
Process {
id: hibernateProbe
command: ["busctl", "call", "org.freedesktop.login1",
"/org/freedesktop/login1", "org.freedesktop.login1.Manager",
"CanHibernate"]
running: true
stdout: StdioCollector {
onStreamFinished: {
canHibernate = this.text.includes('"yes"');
canHibernateKnown = true;
}
}
}
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-idle"
@@ -35,6 +57,49 @@ Singleton {
readonly property bool busy: statusQuery.running || applyRun.running
// What is holding sleep or idle off right now: an array of
// { who, why, what, mode }, newest read wins, blocks sorted before delays.
//
// hypridle has no conditional listener -- there is no way to say "not while
// a video is playing" -- so rather than inventing rules about when not to
// sleep, the Power page shows who is already saying it. `mode` is the part
// that matters when reading the list: a `delay` inhibitor holds sleep for a
// few seconds on the way down and nothing more, while a `block` is what
// actually keeps a machine awake. NetworkManager, UPower and hypridle
// itself hold delays permanently, so a list that did not distinguish them
// would report a machine as pinned awake at all times.
property var inhibitors: []
// False until logind has actually answered. "Nothing holds the machine
// awake" and "nobody could be asked" are different claims and the page
// must not make the first one on the strength of the second.
property bool inhibitorsKnown: false
function refreshInhibitors(): void {
if (!inhibitorQuery.running)
inhibitorQuery.running = true;
}
Process {
id: inhibitorQuery
command: [root.helperPath, "inhibitors"]
stdout: StdioCollector {
onStreamFinished: {
try {
const parsed = JSON.parse(this.text);
root.inhibitors = Array.isArray(parsed) ? parsed : [];
root.inhibitorsKnown = true;
} catch (error) {
// The helper prints nothing at all when logind could not
// be asked, precisely so this lands here rather than
// parsing an empty array and believing it.
root.inhibitors = [];
root.inhibitorsKnown = false;
}
}
}
}
// The values as stored. They only describe what is running when `managed`.
readonly property int blankMinutes: DesktopPreferences.get("screenBlankMinutes")
readonly property int lockMinutes: DesktopPreferences.get("lockMinutes")