Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

idle

Idle inhibitors, plus threshold and inhibit methods.

mantle.idle reads like the others (:get, :map, :on_change) but has no actions. Its thresholds take Lua callbacks, which cannot cross to the Supervisor, so it has methods instead. Any method, :get included, starts it.

local dimmed = state("dimmed", false)

mantle.idle:register_threshold(300, function()
    dimmed:set(true)
end, function()
    dimmed:set(false)
end)

State

mantle.idle:get() returns IdleState, nil before the first push. A field marked ? may be absent.

mantle.idle’s payload.

FieldTypeDescription
compositor_hold_stalebooleanThe compositor last withheld idle notifications, but input resumed before it could answer again. Its hold may have ended; a fresh idle period clears this uncertainty.
inhibitedbooleanTrue when logind blocks idle, a ScreenSaver client holds it, or the compositor was last observed withholding idle notifications. The compositor answer can remain true after a release while the seat is active; the next idle threshold refreshes it.
inhibitorsIdleInhibitor[]Holders other than this shell, ScreenSaver clients included. The compositor’s hold has an empty who; draw why then. An unconfirmed compositor hold is omitted.

IdleInhibitor

One holder blocking idle.

FieldTypeDescription
whostringFree-text holder name, e.g. "mpv"; draw it, never match it.
whystringFree-text reason, e.g. "Playing video"; often empty.

Actions

None: read-only, so any method but get, map and on_change raises.

Methods

MethodContract
:register_threshold(seconds, on_idle, on_resume)Runs on_idle after seconds (1 to 4294967, whole) without input and on_resume(cause) when that idle ends or input follows a non-input resume. Returns an integer handle. If this idle period already passed seconds for another registration, on_idle runs at once
:cancel_threshold(handle)Drops one registration. An unknown or cancelled handle is a no-op
:inhibit(reason)Takes one hold on a logind idle block inhibitor. Counted: two calls need two releases
:release_inhibit()Releases one hold; with none held, a no-op

on_resume receives "input" for user input, "activity" when the compositor’s regular idle notification resumes before input is confirmed, or "inhibitor" when logind blocks idle. An "activity" or "inhibitor" callback can be followed by "input" for the same idle period. On a protocol v1 compositor there is no input-only listener; its regular resume is reported as "input" and may also represent compositor-specific activity such as a presence sensor.

EventThresholdsInhibit holds
In-place reloadDropped before the config evaluates again; top-level register_threshold calls re-register. The same seconds keeps its timer and does not re-run on_idle this idle periodKept
Renderer replacementDroppedDropped

Backend

PartBehaviour
Thresholdsext_idle_notifier_v1 on the Supervisor’s own Wayland connection. Missing protocol, or setup over 5 s: thresholds never fire, logged once
InhibitEvery hold, from any generation, shares one logind Inhibit("idle", "block") fd, closed when the last hold goes
ScreenSaverHosts org.freedesktop.ScreenSaver when the name is free. A browser’s video hold arrives here, directly or through xdg-desktop-portal, and takes the same fd. A client that leaves the bus loses its holds
GateMantle, not logind, acts on idle, so it honours inhibitors itself. While logind’s BlockInhibited names idle, idled thresholds get on_resume("inhibitor") and none fire; input during the hold sends on_resume("input"). On release, ones still idle get on_idle again
Compositor holdsA Wayland idle inhibitor shows when the shortest threshold’s input-only twin fires and the normal notification does not, or when the normal one resumes alone because a hold began on an idle seat, so it needs a registered threshold and an idle seat. It sets inhibited and adds one holder with an empty who. After a detected hold, input resuming sets compositor_hold_stale, removes the unconfirmed holder from inhibitors, and keeps inhibited at its last value until the next idle threshold

How do I…

Keep the screen awake (caffeine)

The hold survives reloads, so named state records it:

local caffeine = state("caffeine", false)

rect {
    on_click = function()
        if caffeine:get() then
            mantle.idle:release_inhibit()
        else
            mantle.idle:inhibit("caffeine")
        end
        caffeine:set(not caffeine:get())
    end,
    children = {
        text {
            content = computed({ caffeine, mantle.idle }, function(held, idle)
                if held then
                    return "awake (held)"
                elseif idle.inhibited then
                    return "awake (another app)"
                end
                return "idle allowed"
            end, "idle allowed"),
        },
    },
}

Gotchas

TrapFix
register_threshold inside on_change, a timer or a click handlerEach call adds a registration that lives until the next reload. Register once at top level, or keep the handle and cancel_threshold it
An inhibit hold that never endsHolds survive reloads and are counted. Record the hold in a state and release once per inhibit
A threshold never fires while a video playsSomething holds idle off and inhibited is true. Draw inhibitors to show who

See also: Lock screen recipe, which locks after an idle threshold.

Source: supervisor/src/capabilities/idle/