Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/IdleCoordinator.qml
Fimeg 19fe6b1d0e shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00

159 lines
5.8 KiB
QML

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