Files

193 lines
7.2 KiB
QML

pragma Singleton
// Idle, lock, and sleep timings.
//
// hypridle has no IPC for reconfiguration, and its config is hyprlang rather
// than the shared JSON, so this cannot work the way the compositor settings do.
// Instead scripts/panama-idle regenerates a config from the settings store and
// restarts the daemon.
//
// The generated file lives under XDG_STATE_HOME rather than ~/.config/hypr,
// because that directory is a symlink into the Panama repository -- writing
// there at runtime would put machine state into a tracked file. A systemd
// drop-in points hypridle at the generated path with `-c`.
//
// "Managed" is therefore a real state with two sides: when the drop-in is
// installed the timings below are in effect, and when it is not, hypridle is
// running the repository's shipped hypridle.conf and these values are only a
// stored intention. The Power page says which it is rather than showing
// controls that quietly do nothing.
import Quickshell
import Quickshell.Io
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"
property bool managed: false
property string serviceState: "unknown"
property string generatedPath: ""
property string lastError: ""
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")
readonly property int suspendMinutes: DesktopPreferences.get("suspendMinutes")
readonly property bool lockOnSleep: DesktopPreferences.get("lockOnSleep")
// Blanking after locking is legal but pointless, and blanking with lock off
// is fine. Surfacing the one genuinely confusing combination beats silently
// reordering the user's numbers.
readonly property bool lockBeforeBlank: root.lockMinutes > 0
&& root.blankMinutes > 0
&& root.lockMinutes < root.blankMinutes
Process {
id: statusQuery
command: [root.helperPath, "status"]
stdout: StdioCollector {
onStreamFinished: {
try {
const state = JSON.parse(this.text);
root.managed = state.managed === true;
root.serviceState = String(state.active ?? "unknown");
root.generatedPath = String(state.generated ?? "");
root.lastError = "";
} catch (error) {
root.lastError = "Could not read the idle configuration.";
}
}
}
}
Process {
id: applyRun
onExited: (exitCode, exitStatus) => {
root.lastError = exitCode === 0 ? "" : "Could not update the idle configuration.";
root.refresh();
}
}
Component.onCompleted: root.refresh()
function refresh(): void {
if (!statusQuery.running)
statusQuery.running = true;
}
// Regenerates the config from the current settings and restarts hypridle if
// Panama is managing it. Safe to call when it is not: the file is written
// and nothing is restarted.
function apply(): void {
if (applyRun.running)
return;
applyRun.exec([root.helperPath, "apply"]);
}
function setManaged(enabled: bool): void {
if (applyRun.running)
return;
applyRun.exec([root.helperPath, enabled ? "install" : "remove"]);
}
// Regenerate whenever one of the four inputs changes. Coalesced, because a
// slider drag settles through several commits and each one would otherwise
// restart the daemon.
Connections {
target: DesktopPreferences
function onRevisionChanged(): void {
if (root.managed)
regenerate.restart();
}
}
// The charger came or went. hypridle cannot express two sets of timeouts,
// so the config is rebuilt from the other set and the daemon restarted --
// the same path a settings change takes, through the same debounce, so a
// flapping charger cannot restart hypridle in a loop.
Connections {
target: Battery
function onAcChanged(online: bool): void {
if (root.managed)
regenerate.restart();
}
}
Timer {
id: regenerate
interval: 400
onTriggered: root.apply()
}
}