mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-04 20:28:23 +02:00
334 lines
11 KiB
QML
334 lines
11 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
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
|
|
|
|
property bool idledThisCycle: false
|
|
property bool screensaverStartedThisCycle: false
|
|
property string lastEvent: "starting"
|
|
property string lastEventAt: ""
|
|
property string lastScreensaverState: "unknown"
|
|
property string lastScreensaverCheckAt: ""
|
|
property int screensaverCheckCount: 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
|
|
screensaverResumePollTimer.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()
|
|
screensaverResumePollTimer.stop()
|
|
root.idledThisCycle = false
|
|
root.screensaverStartedThisCycle = false
|
|
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
|
|
screensaverResumePollTimer.stop()
|
|
|
|
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()
|
|
screensaverResumePollTimer.stop()
|
|
|
|
if (root.idledThisCycle) runProcess(wakeProcess, "wake", "omarchy-system-wake")
|
|
|
|
root.idledThisCycle = false
|
|
root.screensaverStartedThisCycle = false
|
|
}
|
|
|
|
function checkScreensaverAfterActiveSignal() {
|
|
if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return
|
|
if (screensaverCheckProcess.running) return
|
|
root.lastScreensaverState = "checking"
|
|
root.lastScreensaverCheckAt = nowIso()
|
|
screensaverCheckProcess.command = ["bash", "-lc", "if hyprctl clients -j 2>/dev/null | jq -e '.[] | select(.class == \"org.omarchy.screensaver\" or .initialClass == \"org.omarchy.screensaver\")' >/dev/null; then echo running-window; elif pgrep -f '[o]rg.omarchy.screensaver' >/dev/null; then echo running-process; elif omarchy-toggle-enabled screensaver-off; then echo disabled; else echo stopped; fi"]
|
|
screensaverCheckProcess.running = true
|
|
}
|
|
|
|
function handleActiveSignal() {
|
|
if (!root.idledThisCycle) return
|
|
|
|
// Starting the screensaver can make the compositor report activity. Keep
|
|
// the lock timer running while the screensaver is still alive so the lock
|
|
// deadline remains `idle.lock` seconds from the original user idle time.
|
|
if (root.screensaverStartedThisCycle) {
|
|
logEvent("idle-monitor-active", "screensaver cycle remains armed")
|
|
screensaverResumePollTimer.restart()
|
|
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,
|
|
sleepMonitor: sleepMonitorProcess.running,
|
|
screensaver: root.screensaverTimeoutSeconds,
|
|
lock: root.lockTimeoutSeconds,
|
|
screensaverDelay: root.screensaverDelaySeconds,
|
|
lockDelay: root.lockDelaySeconds,
|
|
timers: {
|
|
screensaver: screensaverTimer.running,
|
|
lock: lockTimer.running,
|
|
poll: screensaverResumePollTimer.running,
|
|
sleepMonitorRestart: sleepMonitorRestartTimer.running
|
|
},
|
|
processes: {
|
|
screensaver: screensaverProcess.running,
|
|
lock: lockProcess.running,
|
|
wake: wakeProcess.running,
|
|
check: screensaverCheckProcess.running,
|
|
sleepLock: sleepLockProcess.running,
|
|
sleepWake: sleepWakeProcess.running
|
|
},
|
|
lastEvent: root.lastEvent,
|
|
lastEventAt: root.lastEventAt,
|
|
lastScreensaverState: root.lastScreensaverState,
|
|
lastScreensaverCheckAt: root.lastScreensaverCheckAt,
|
|
screensaverCheckCount: root.screensaverCheckCount
|
|
})
|
|
}
|
|
|
|
function handleSleepPreparing(preparing) {
|
|
if (!root.idleEnabled) return
|
|
|
|
if (preparing) {
|
|
cancelIdleCycle("sleep-preparing")
|
|
runProcess(sleepLockProcess, "sleep-lock", "OMARCHY_LOCK_ONLY=true omarchy-system-lock")
|
|
} else {
|
|
runProcess(sleepWakeProcess, "sleep-wake", "sleep 1 && omarchy-system-wake")
|
|
}
|
|
}
|
|
|
|
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: screensaverResumePollTimer
|
|
interval: 1000
|
|
repeat: true
|
|
onTriggered: root.checkScreensaverAfterActiveSignal()
|
|
}
|
|
|
|
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: sleepLockProcess
|
|
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-lock exitCode=" + exitCode + " status=" + exitStatus) }
|
|
}
|
|
Process {
|
|
id: sleepWakeProcess
|
|
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-wake exitCode=" + exitCode + " status=" + exitStatus) }
|
|
}
|
|
|
|
Process {
|
|
id: screensaverCheckProcess
|
|
stdout: SplitParser {
|
|
onRead: function(line) {
|
|
root.lastScreensaverState = String(line || "").trim()
|
|
root.lastScreensaverCheckAt = root.nowIso()
|
|
root.screensaverCheckCount++
|
|
root.logEvent("screensaver-check", root.lastScreensaverState)
|
|
}
|
|
}
|
|
onExited: function(exitCode, exitStatus) {
|
|
root.logEvent("process-exit", "screensaver-check exitCode=" + exitCode + " status=" + exitStatus + " state=" + root.lastScreensaverState)
|
|
if (!root.idleEnabled || !root.idledThisCycle || !root.screensaverStartedThisCycle) return
|
|
|
|
var state = String(root.lastScreensaverState || "").trim()
|
|
if (state.indexOf("running") === 0) return
|
|
if (state === "disabled") {
|
|
screensaverResumePollTimer.stop()
|
|
return
|
|
}
|
|
|
|
// If the screensaver disappears while we're still in the idle cycle,
|
|
// treat that as the user returning and require authentication before
|
|
// showing the desktop again.
|
|
root.lockSystem("screensaver-stopped state=" + state)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: sleepMonitorProcess
|
|
command: ["bash", "-lc", "exec dbus-monitor --system \"type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'\""]
|
|
running: true
|
|
stdout: SplitParser {
|
|
onRead: function(line) {
|
|
var text = String(line || "").trim()
|
|
if (text === "boolean true") root.handleSleepPreparing(true)
|
|
else if (text === "boolean false") root.handleSleepPreparing(false)
|
|
}
|
|
}
|
|
onExited: {
|
|
root.logEvent("sleep-monitor-exit", "restarting")
|
|
sleepMonitorRestartTimer.restart()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: sleepMonitorRestartTimer
|
|
interval: 5000
|
|
repeat: false
|
|
onTriggered: {
|
|
if (!sleepMonitorProcess.running) {
|
|
root.logEvent("sleep-monitor-restart")
|
|
sleepMonitorProcess.running = true
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|