Treat idle timeout 0 as disabled, not immediate

idle.screensaver / idle.lock of 0 was included in Math.min(), so
IdleMonitor's timeout became 0. Releasing a Wayland idle inhibitor
(SDL's default screensaver inhibit) then reported idle immediately and
locked the session even when lock had been "disabled" by setting it to 0.

Treat 0 as disabled when computing the first idle deadline, do not start
the corresponding action, and leave IdleMonitor off when both timeouts
are 0.

Fixes #10860
This commit is contained in:
Paulo Geyer committed 2026-09-19 13:57:52 -03:00
1 parent 60663faf87
commit 14921a956e
3 files changed
+62 -10

No files matched your search

+22
View File
@@ -4,6 +4,26 @@ function secondsFromConfig(value, fallback) {
return Math.floor(n)
}
// 0 means the action is disabled, not "fire immediately". min(0, 300) would
// otherwise make IdleMonitor report idle as soon as an inhibitor is released.
function firstIdleTimeout(screensaverSeconds, lockSeconds) {
var times = []
if (screensaverSeconds > 0) times.push(screensaverSeconds)
if (lockSeconds > 0) times.push(lockSeconds)
if (times.length === 0) return 0
var min = times[0]
for (var i = 1; i < times.length; i++) {
if (times[i] < min) min = times[i]
}
return min
}
function delayAfterFirstIdle(timeoutSeconds, firstIdleSeconds) {
if (!(timeoutSeconds > 0)) return 0
var delay = timeoutSeconds - firstIdleSeconds
return delay > 0 ? delay : 0
}
function eventParts(event, count) {
try {
if (event && event.parse) return event.parse(count)
@@ -46,6 +66,8 @@ function screensaverWindowsAfter(windows, address, visible) {
if (typeof module !== "undefined") {
module.exports = {
secondsFromConfig: secondsFromConfig,
firstIdleTimeout: firstIdleTimeout,
delayAfterFirstIdle: delayAfterFirstIdle,
eventParts: eventParts,
screensaverWindowsAfter: screensaverWindowsAfter
}
+17 -10
View File
@@ -20,9 +20,12 @@ Item {
? shell.shellConfig.idle : (shell && shell.idleConfig ? shell.idleConfig : ({}))
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 screensaverEnabled: screensaverTimeoutSeconds > 0
readonly property bool lockEnabled: lockTimeoutSeconds > 0
readonly property bool idleTimersEnabled: screensaverEnabled || lockEnabled
readonly property int firstIdleTimeoutSeconds: IdleModel.firstIdleTimeout(screensaverTimeoutSeconds, lockTimeoutSeconds)
readonly property int screensaverDelaySeconds: IdleModel.delayAfterFirstIdle(screensaverTimeoutSeconds, firstIdleTimeoutSeconds)
readonly property int lockDelaySeconds: IdleModel.delayAfterFirstIdle(lockTimeoutSeconds, firstIdleTimeoutSeconds)
readonly property bool idleEnabled: stayAwakeStateLoaded && !stayAwake
readonly property string screensaverClass: "org.omarchy.screensaver"
@@ -91,11 +94,15 @@ Item {
root.screensaverStartedThisCycle = false
resetScreensaverWindows()
if (root.screensaverDelaySeconds === 0) launchScreensaver()
else screensaverTimer.restart()
if (root.screensaverEnabled) {
if (root.screensaverDelaySeconds === 0) launchScreensaver()
else screensaverTimer.restart()
}
if (root.lockDelaySeconds === 0) lockSystem("lock-timeout-immediate")
else lockTimer.restart()
if (root.lockEnabled) {
if (root.lockDelaySeconds === 0) lockSystem("lock-timeout-immediate")
else lockTimer.restart()
}
}
function cancelIdleCycle(reason) {
@@ -172,7 +179,7 @@ Item {
function handleIdleChanged() {
logEvent("idle-monitor", idleMonitor.isIdle ? "idle" : "active")
if (!root.idleEnabled) return
if (!root.idleEnabled || !root.idleTimersEnabled) return
if (idleMonitor.isIdle) startIdleCycle()
else handleActiveSignal()
@@ -250,8 +257,8 @@ Item {
IdleMonitor {
id: idleMonitor
enabled: root.idleEnabled
timeout: root.firstIdleTimeoutSeconds
enabled: root.idleEnabled && root.idleTimersEnabled
timeout: Math.max(1, root.firstIdleTimeoutSeconds)
respectInhibitors: true
onIsIdleChanged: root.handleIdleChanged()
}
+23
View File
@@ -5,11 +5,34 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const idle = requireFromRoot('shell/plugins/services/idle/IdleModel.js')
const serviceSource = fs.readFileSync(root + '/shell/plugins/services/idle/Service.qml', 'utf8')
assertEqual(idle.secondsFromConfig('42.9', 10), 42, 'idle floors configured seconds')
assertEqual(idle.secondsFromConfig('-1', 10), 10, 'idle rejects negative seconds')
assertEqual(idle.secondsFromConfig('nope', 10), 10, 'idle rejects invalid seconds')
assertEqual(idle.secondsFromConfig(0, 300), 0, 'idle keeps an explicit zero timeout')
assertEqual(idle.firstIdleTimeout(150, 300), 150, 'idle uses the sooner of screensaver and lock')
assertEqual(idle.firstIdleTimeout(0, 300), 300, 'idle ignores a disabled screensaver when computing first idle')
assertEqual(idle.firstIdleTimeout(150, 0), 150, 'idle ignores a disabled lock when computing first idle')
assertEqual(idle.firstIdleTimeout(0, 0), 0, 'idle has no first-idle timeout when both actions are disabled')
assertEqual(idle.delayAfterFirstIdle(300, 150), 150, 'idle delays lock until its own timeout')
assertEqual(idle.delayAfterFirstIdle(150, 150), 0, 'idle fires an action immediately when it is the first timeout')
assertEqual(idle.delayAfterFirstIdle(0, 150), 0, 'idle does not schedule a disabled action')
assert(
serviceSource.includes('IdleModel.firstIdleTimeout(screensaverTimeoutSeconds, lockTimeoutSeconds)'),
'idle ignores a zero timeout when computing the first idle deadline'
)
assert(
/enabled: root\.idleEnabled && root\.idleTimersEnabled/.test(serviceSource),
'idle monitor is off when both screensaver and lock are disabled'
)
assert(
/if \(root\.lockEnabled\) \{\s*\n\s*if \(root\.lockDelaySeconds === 0\) lockSystem/.test(serviceSource),
'idle does not lock immediately when lock timeout is disabled'
)
assertDeepEqual(idle.eventParts({ data: 'a,b,c' }, 2), ['a', 'b', 'c'], 'idle parses raw event data')
assertDeepEqual(