193 lines
7.2 KiB
QML
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()
|
|
}
|
|
}
|