// Souveraine's staged idle projection. // // It does not replace logind or hypridle. It gives all surfaces one state // vocabulary while target-specific adapters evolve. Native idle-notify stays // opt-in until it is verified on the Pixel compositor. // // The state graph extends into sleep/suspend when SessionEvents is present: // Active → Dimmed → LockRequested → LockSecure → Suspending → Asleep → Waking → Active // The sleep states are driven by logind's PrepareForSleep signal via // SessionEvents.qml; they are not reachable from idle timers alone. pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland import qs import qs.modules.common import qs.modules.common.functions Singleton { id: root enum State { Active, Dimmed, LockRequested, LockSecure, Suspending, Asleep, Waking } property int state: IdleCoordinator.Active readonly property bool nativeEnabled: Config.options.lock.idle.nativeCoordinatorEnabled readonly property bool lockSecure: GlobalStates.screenLockSecure readonly property bool lockRequested: GlobalStates.screenLocked signal dimRequested() signal activeRequested() signal stateTransitioned(int state) // Whether we lowered the backlight, so Active only restores what // Dimmed saved — never a stale brightnessctl snapshot. property bool displayDimmed: false function setState(next) { if (root.state === next) return; root.state = next; // Publish the coarse in-use bool the ii-base pollers gate on // (quickshell-idle-power task 4). Waking counts as active so stats // are fresh by the time the screen is visible again. GlobalStates.displayActive = (next === IdleCoordinator.Active || next === IdleCoordinator.Waking); root.stateTransitioned(next); console.log("[idle-coordinator] state=" + next); if (next === IdleCoordinator.Dimmed) { dimProc.action = "dim"; dimProc.command = ["brightnessctl", "-q", "-s", "set", Config.options.lock.idle.dimBrightness]; dimProc.running = true; root.displayDimmed = true; } else if (next === IdleCoordinator.Active && root.displayDimmed) { dimProc.action = "restore"; dimProc.command = ["brightnessctl", "-q", "-r"]; dimProc.running = true; root.displayDimmed = false; } } Process { id: dimProc property string action: "" onExited: (exitCode, exitStatus) => { if (exitCode !== 0) console.log("[idle-coordinator] brightness " + dimProc.action + " failed (exit " + exitCode + ")"); } } function returnActive() { if (root.lockRequested || root.lockSecure) return; root.setState(IdleCoordinator.Active); root.activeRequested(); } // Keep System Awake is checked inside the handlers, NOT bound to // `enabled`: flipping enabled destroys/recreates the ext-idle-notify // object, and doing that during lock teardown (ii's LockScreen toggles // Idle.inhibit) races the compositor into a fatal "invalid object" // protocol error that kills the whole shell. The Wayland idle-inhibitor // surface is not honored on the Pixel compositor, so respectInhibitors // alone can't see the toggle either (see Idle.qml). IdleMonitor { id: dimMonitor enabled: root.nativeEnabled timeout: Math.max(1, Config.options.lock.idle.dimAfterSeconds) * 1000 respectInhibitors: true onIsIdleChanged: { if (isIdle && !root.lockRequested && !Idle.inhibit) { root.setState(IdleCoordinator.Dimmed); root.dimRequested(); } else if (!isIdle) { root.returnActive(); } } } IdleMonitor { id: lockMonitor enabled: root.nativeEnabled timeout: Math.max(1, Config.options.lock.idle.lockAfterSeconds) * 1000 respectInhibitors: true onIsIdleChanged: { if (isIdle && !root.lockRequested && !Idle.inhibit) { root.setState(IdleCoordinator.LockRequested); Session.lock(); } else if (!isIdle) { root.returnActive(); } } } Connections { target: GlobalStates function onScreenLockedChanged() { if (GlobalStates.screenLocked) root.setState(IdleCoordinator.LockRequested); else root.returnActive(); } function onScreenLockSecureChanged() { if (GlobalStates.screenLockSecure) root.setState(IdleCoordinator.LockSecure); } } // Logind sleep/suspend lifecycle. SessionEvents drives these states // when PrepareForSleep fires; they are unreachable without it. // Wires to SessionEvents once that singleton exists. Connections { target: typeof SessionEvents !== "undefined" ? SessionEvents : null function onPrepareForSleep(suspending) { if (suspending) { root.setState(IdleCoordinator.Suspending); } else { // Waking from sleep. The lock may or may not still be // held — returnActive() checks that before clearing. root.setState(IdleCoordinator.Waking); // Brief waking state before returning to the idle graph. // Surfaces can animate a wake transition during this window. wakeResetTimer.start(); } } } Timer { id: wakeResetTimer interval: 1500 repeat: false onTriggered: { if (root.state === IdleCoordinator.Waking) root.returnActive(); } } }