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 { 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 // 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() } }