import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Io import Quickshell.Wayland import "IdleModel.js" as IdleModel Item { id: root // Injected by omarchy-shell (the first-party service loader). property var shell: null readonly property string home: Quickshell.env("HOME") readonly property string stayAwakeStateDir: home + "/.local/state/omarchy/indicators" readonly property string stayAwakeStatePath: stayAwakeStateDir + "/stay-awake" readonly property int defaultScreensaverSeconds: 150 readonly property int defaultLockSeconds: 300 readonly property var idleConfig: shell && shell.shellConfig && shell.shellConfig.idle ? shell.shellConfig.idle : ({}) readonly property int screensaverTimeoutSeconds: secondsFromConfig(idleConfig.screensaver, defaultScreensaverSeconds) readonly property int lockTimeoutSeconds: secondsFromConfig(idleConfig.lock, defaultLockSeconds) readonly property int firstIdleTimeoutSeconds: Math.min(screensaverTimeoutSeconds, lockTimeoutSeconds) readonly property int screensaverDelaySeconds: Math.max(0, screensaverTimeoutSeconds - firstIdleTimeoutSeconds) readonly property int lockDelaySeconds: Math.max(0, lockTimeoutSeconds - firstIdleTimeoutSeconds) readonly property bool idleEnabled: stayAwakeStateLoaded && !stayAwake readonly property string screensaverClass: "org.omarchy.screensaver" property bool stayAwake: false property bool stayAwakeStateLoaded: false property bool hasPendingStayAwakePersist: false property bool pendingStayAwakePersist: false property bool idledThisCycle: false property bool screensaverStartedThisCycle: false property string lastEvent: "starting" property string lastEventAt: "" property var screensaverWindows: ({}) property int screensaverWindowCount: 0 function secondsFromConfig(value, fallback) { return IdleModel.secondsFromConfig(value, fallback) } function nowIso() { return new Date().toISOString() } function logEvent(event, details) { var suffix = details === undefined || details === null || details === "" ? "" : ": " + String(details) root.lastEventAt = nowIso() root.lastEvent = event + suffix console.log("omarchy idle " + root.lastEventAt + " " + root.lastEvent) } function runProcess(process, label, command) { if (process.running) { logEvent("process-skip", label + " already running") return false } logEvent("process-start", label + " " + command) process.command = ["bash", "-lc", command] process.running = true return true } function launchScreensaver() { root.screensaverStartedThisCycle = true screensaverLaunchGraceTimer.restart() runProcess(screensaverProcess, "screensaver", "[[ $(omarchy-shell lock isLocked 2>/dev/null) == \"true\" ]] || omarchy-launch-screensaver") } function lockSystem(reason) { logEvent("lock-system", reason || "requested") screensaverTimer.stop() lockTimer.stop() screensaverLaunchGraceTimer.stop() root.idledThisCycle = false root.screensaverStartedThisCycle = false resetScreensaverWindows() runProcess(lockProcess, "lock", "omarchy-system-lock") } function startIdleCycle() { if (root.idledThisCycle) { logEvent("idle-cycle-already-running") return } logEvent("idle-cycle-start", "screensaver=" + root.screensaverTimeoutSeconds + " lock=" + root.lockTimeoutSeconds) root.idledThisCycle = true root.screensaverStartedThisCycle = false resetScreensaverWindows() if (root.screensaverDelaySeconds === 0) launchScreensaver() else screensaverTimer.restart() if (root.lockDelaySeconds === 0) lockSystem("lock-timeout-immediate") else lockTimer.restart() } function cancelIdleCycle(reason) { logEvent("idle-cycle-cancel", reason || "requested") screensaverTimer.stop() lockTimer.stop() screensaverLaunchGraceTimer.stop() if (root.idledThisCycle) runProcess(wakeProcess, "wake", "omarchy-system-wake") root.idledThisCycle = false root.screensaverStartedThisCycle = false resetScreensaverWindows() } function resetScreensaverWindows() { root.screensaverWindows = ({}) root.screensaverWindowCount = 0 } function setScreensaverWindow(address, visible) { var next = IdleModel.screensaverWindowsAfter(root.screensaverWindows, address, visible) root.screensaverWindows = next.windows root.screensaverWindowCount = next.count } function handleScreensaverWindowOpened(address) { setScreensaverWindow(address, true) screensaverLaunchGraceTimer.stop() } function handleScreensaverWindowClosed(address) { setScreensaverWindow(address, false) if (!root.idleEnabled || !root.idledThisCycle || !root.screensaverStartedThisCycle) return if (root.screensaverWindowCount > 0) return // The user dismissed the screensaver before the lock deadline. Treat that // as activity and cancel the pending lock; the lock timer is only allowed // to fire while the screensaver remains up. root.cancelIdleCycle("screensaver-dismissed") } function eventParts(event, count) { return IdleModel.eventParts(event, count) } function handleHyprlandEvent(event) { var name = String(event && event.name ? event.name : "") if (name === "openwindow") { var open = eventParts(event, 4) if (String(open[2] || "") === root.screensaverClass) root.handleScreensaverWindowOpened(open[0]) } else if (name === "closewindow") { var close = eventParts(event, 1) var address = String(close[0] || "") if (root.screensaverWindows[address]) root.handleScreensaverWindowClosed(address) } } function handleActiveSignal() { if (!root.idledThisCycle) return // Starting the screensaver can make the compositor report activity. Keep // the lock timer running once the screensaver exists (or during its short // launch grace); Hyprland window events cancel the cycle if it exits before // the normal lock deadline. if (root.screensaverStartedThisCycle && (root.screensaverWindowCount > 0 || screensaverLaunchGraceTimer.running)) { logEvent("idle-monitor-active", "screensaver cycle remains armed") return } cancelIdleCycle("activity") } function handleIdleChanged() { logEvent("idle-monitor", idleMonitor.isIdle ? "idle" : "active") if (!root.idleEnabled) return if (idleMonitor.isIdle) startIdleCycle() else handleActiveSignal() } function statusJson() { return JSON.stringify({ enabled: root.idleEnabled, stayAwake: root.stayAwake, stayAwakeStateLoaded: root.stayAwakeStateLoaded, stayAwakeStatePath: root.stayAwakeStatePath, idle: idleMonitor.isIdle, inIdleCycle: root.idledThisCycle, screensaverStarted: root.screensaverStartedThisCycle, screensaver: root.screensaverTimeoutSeconds, lock: root.lockTimeoutSeconds, screensaverDelay: root.screensaverDelaySeconds, lockDelay: root.lockDelaySeconds, screensaverWindows: root.screensaverWindowCount, timers: { screensaver: screensaverTimer.running, lock: lockTimer.running, screensaverLaunchGrace: screensaverLaunchGraceTimer.running }, processes: { screensaver: screensaverProcess.running, lock: lockProcess.running, wake: wakeProcess.running }, lastEvent: root.lastEvent, lastEventAt: root.lastEventAt }) } function persistStayAwake(value) { var command = value ? "mkdir -p \"$HOME/.local/state/omarchy/indicators\" && touch \"$HOME/.local/state/omarchy/indicators/stay-awake\"" : "rm -f \"$HOME/.local/state/omarchy/indicators/stay-awake\"" if (stayAwakeStateWriter.running) { root.pendingStayAwakePersist = !!value root.hasPendingStayAwakePersist = true return } stayAwakeStateWriter.command = ["bash", "-lc", command] stayAwakeStateWriter.running = true } function refreshStayAwakeState() { if (!stayAwakeStateProbe.running) stayAwakeStateProbe.running = true } function applyStayAwake(value, persist, reason) { var enabled = !!value var changed = !root.stayAwakeStateLoaded || root.stayAwake !== enabled if (persist) persistStayAwake(enabled) root.stayAwake = enabled root.stayAwakeStateLoaded = true if (!changed) return enabled ? "disabled" : "enabled" logEvent("stay-awake", (enabled ? "enabled" : "disabled") + (reason ? " " + reason : "")) if (enabled) cancelIdleCycle("stay-awake") else Qt.callLater(root.handleIdleChanged) return enabled ? "disabled" : "enabled" } function setIdleEnabled(value) { return applyStayAwake(!value, true, "ipc") } IdleMonitor { id: idleMonitor enabled: root.idleEnabled timeout: root.firstIdleTimeoutSeconds respectInhibitors: true onIsIdleChanged: root.handleIdleChanged() } Timer { id: screensaverTimer interval: root.screensaverDelaySeconds * 1000 repeat: false onTriggered: root.launchScreensaver() } Timer { id: lockTimer interval: root.lockDelaySeconds * 1000 repeat: false onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem("lock-timeout") } Timer { id: screensaverLaunchGraceTimer interval: 3000 repeat: false onTriggered: { if (root.idleEnabled && root.idledThisCycle && root.screensaverStartedThisCycle && root.screensaverWindowCount === 0 && !idleMonitor.isIdle) { root.cancelIdleCycle("screensaver-not-running") } } } Connections { target: Hyprland function onRawEvent(event) { root.handleHyprlandEvent(event) } } Process { id: screensaverProcess onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "screensaver exitCode=" + exitCode + " status=" + exitStatus) } } Process { id: lockProcess onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "lock exitCode=" + exitCode + " status=" + exitStatus) } } Process { id: wakeProcess onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "wake exitCode=" + exitCode + " status=" + exitStatus) } } Process { id: stayAwakeStateProbe command: ["bash", "-lc", "mkdir -p \"$HOME/.local/state/omarchy/indicators\"; if [[ -f $HOME/.local/state/omarchy/indicators/stay-awake ]]; then echo yes; else echo no; fi"] stdout: SplitParser { onRead: function(line) { root.applyStayAwake(String(line).trim() === "yes", false, "state-file") } } onExited: function() { stayAwakeStateDirWatcher.reload() } } Process { id: stayAwakeStateWriter onExited: function() { if (root.hasPendingStayAwakePersist) { var pending = root.pendingStayAwakePersist root.hasPendingStayAwakePersist = false root.persistStayAwake(pending) return } root.refreshStayAwakeState() } } FileView { id: stayAwakeStateDirWatcher path: root.stayAwakeStateDir watchChanges: true printErrors: false onFileChanged: root.refreshStayAwakeState() } Component.onCompleted: { logEvent("service-ready") refreshStayAwakeState() } IpcHandler { target: "idle" function status(): string { return root.statusJson() } function debug(): string { return root.statusJson() } function enable(): string { return root.setIdleEnabled(true) } function disable(): string { return root.setIdleEnabled(false) } function toggle(): string { return root.setIdleEnabled(!root.idleEnabled) } } }