import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Io import Quickshell.Wayland Item { id: root // Injected by omarchy-shell (the first-party service loader). property var shell: null 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: persisted.idleEnabled readonly property string screensaverClass: "org.omarchy.screensaver" property bool idledThisCycle: false property bool screensaverStartedThisCycle: false property string lastEvent: "starting" property string lastEventAt: "" property var screensaverWindows: ({}) property int screensaverWindowCount: 0 PersistentProperties { id: persisted reloadableId: "omarchy-idle" property bool idleEnabled: true } function secondsFromConfig(value, fallback) { var n = Number(value) if (!isFinite(n) || n < 0) return fallback return Math.floor(n) } 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 key = String(address || "") if (!key) return var next = {} var count = 0 for (var existing in root.screensaverWindows) { if (existing !== key && root.screensaverWindows[existing]) { next[existing] = true count++ } } if (visible) { next[key] = true count++ } root.screensaverWindows = next root.screensaverWindowCount = 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) { try { if (event && event.parse) return event.parse(count) } catch (error) {} return String(event && event.data ? event.data : "").split(",") } 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, 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 setIdleEnabled(value) { var enabled = !!value if (persisted.idleEnabled === enabled) return enabled ? "enabled" : "disabled" persisted.idleEnabled = enabled logEvent("idle-enabled", enabled ? "enabled" : "disabled") if (!enabled) cancelIdleCycle("disabled") else Qt.callLater(root.handleIdleChanged) return enabled ? "enabled" : "disabled" } 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) } } Component.onCompleted: { logEvent("service-ready") Qt.callLater(root.handleIdleChanged) } 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) } } }