- 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>
159 lines
5.8 KiB
QML
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();
|
|
}
|
|
}
|
|
}
|