An idle screen costs a screen on wall power and the rest of your afternoon on battery, so they should not be the same number. hypridle has no concept of a power source -- one config, one set of timeouts -- so rather than maintaining two configs and swapping them, panama-idle builds the single config from whichever key set applies, and IdleLock rebuilds it when the charger comes or goes. That runs through the same 400ms debounce a settings change uses, so a loose charger cannot restart hypridle in a loop. The battery keys fall back to their AC counterparts rather than to the schema defaults. Without that, unplugging would silently override a deliberately long timing with a shipped short one, which is the kind of thing you would notice only by losing work. A machine with no battery reads none of it and generates exactly what it generated before. The contract pins that alongside the two obvious directions, and was checked by sabotaging the detection to confirm it fails rather than passing vacuously.
128 lines
4.5 KiB
QML
128 lines
4.5 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 {
|
|
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()
|
|
}
|
|
}
|