Skip to content

fix(session): persist the idle inhibitor across shell restarts - #3123

Merged
bbedward merged 1 commit into
AvengeMedia:masterfrom
Mark2Mac:persist-idle-inhibit
Aug 20, 2026
Merged

fix(session): persist the idle inhibitor across shell restarts#3123
bbedward merged 1 commit into
AvengeMedia:masterfrom
Mark2Mac:persist-idle-inhibit

Conversation

@Mark2Mac

@Mark2Mac Mark2Mac commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

The idle inhibitor ("Keep Awake") does not survive a shell restart. SessionService holds it as a plain in-memory property and nothing ever writes it out:

property bool idleInhibited: false

Every restart silently clears it. The user's explicit choice is dropped with no message — the pill disappears from the bar and the screen starts sleeping again mid-task.

Restarting is not an edge case: dms restart is a first-class CLI command, and assets/systemd/dms.service in this repo sets Restart=on-failure with TimeoutStopSec=10, so a shell that is slow to stop is killed and brought back automatically.

Every other Control Center toggle already survives a restart. Measured on upstream master, over the state read by Modules/ControlCenter/Components/DragDropGrid.qml and its neighbours:

toggle declared in SessionSpec.js
doNotDisturb yes
isLightMode yes
nightModeEnabled yes
wallpaperCyclingEnabled yes
idleInhibited no

So this follows the doNotDisturb idiom exactly rather than inventing a mechanism: one spec entry, one property, one setter that saves.

  • SessionSpec.jsidleInhibited: { def: false }
  • SessionData.qml — the property plus setIdleInhibited(enabled)
  • SessionService.qmlenableIdleInhibit() / disableIdleInhibit() write through; the state is restored at startup

Restore goes through two entry points because the singletons are lazily created and their order is not fixed: onLoaded covers "service instantiated before the session file is read", Component.onCompleted covers the opposite. enableIdleInhibit() is idempotent, so whichever runs second is a no-op.

The default stays false and SessionStore.toJson omits defaults, so an untouched session.json gains no new key.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Refactor / internal cleanup
  • Documentation
  • Other

Related issues

None open for this. #1483 (configurable timer) is adjacent: the field added here has the same shape doNotDisturb uses alongside doNotDisturbUntil, so a later idleInhibitedUntil slots in beside it without another migration.

Screenshots / video

No visual change. The bar pill already reflects SessionService.idleInhibited; it simply now shows the state the user left it in. Behaviour is shown as measurements below instead.

How this was verified

In a nested niri with isolated XDG_CONFIG_HOME/XDG_STATE_HOME, so the bench could never touch the live session:

qs ipc --pid <nested> call inhibit enable
→ session.json:  "idleInhibited": true
kill <nested>                 # what systemd does at TimeoutStopSec
relaunch, then: qs ipc --pid <new> call inhibit status
→ Idle inhibit is enabled

Counter-test on the same session.json, with the patch reverted to plain upstream:

after the shell restarts
with this patch Idle inhibit is enabled
upstream as-is Idle inhibit is disabled

Also exercised directly against SessionStore.js + SessionSpec.js: parse({idleInhibited: true})true, toJson{"idleInhibited": true}, and with the value at its default the key is omitted from the output.

Checklist

  • My code follows the conventions in CONTRIBUTING.md
  • I have tested my changes locally
  • New user-facing strings are wrapped in I18n.tr() — n/a, this PR adds no user-facing strings
  • Go changes — n/a, QML/JS only
  • QML changes: ran make lint-qml with no new warnings — could not run locally: the script requires the Quickshell tooling VFS (quickshell/.qmlls.ini + <buildDir>/qs/qmldir), and on quickshell 0.3.0 here the VFS directory only ever contains tooling.lock, so lint-qml refuses to start. The two touched QML files parse cleanly under qmlformat, and the pre-commit hooks that apply were run by hand (no trailing whitespace, single trailing newline, no console.*). Happy to re-run if you can point me at what generates the VFS.
  • Documentation: companion PR opened in dlx-docs: docs(ipc): idle inhibit persists across restarts, plus the two undocumented verbs DankLinux-Docs#138

The idle inhibitor ("Keep Awake") lives only in memory: SessionService holds it
as a plain `property bool idleInhibited: false` and nothing ever writes it out.
Every restart of the shell silently clears it — the user's explicit choice is
dropped with no message and no trace, and the pill just disappears from the bar.

Restarting is not an edge case. `dms restart` is a first-class CLI command, and
the unit shipped in this repo (assets/systemd/dms.service) sets
`Restart=on-failure` with `TimeoutStopSec=10`, so a shell that is slow to stop
is killed and brought back automatically. Each of those paths turns Keep Awake
off without saying so.

Every other Control Center toggle already survives a restart. Measured on
upstream master, over the state read by Modules/ControlCenter/Components/
DragDropGrid.qml and its neighbours:

  doNotDisturb             in SessionSpec: yes
  isLightMode              in SessionSpec: yes
  nightModeEnabled         in SessionSpec: yes
  wallpaperCyclingEnabled  in SessionSpec: yes
  idleInhibited            in SessionSpec: NO

So this follows the doNotDisturb idiom exactly rather than inventing a
mechanism: one spec entry, one property, one setter that saves.

Restore happens through two entry points because the singletons are lazily
created and their order is not fixed: `onLoaded` covers "service instantiated
before the session file is read", `Component.onCompleted` covers the opposite.
enableIdleInhibit() is idempotent, so whichever runs second is a no-op.

The default stays false and SessionStore.toJson omits defaults, so an untouched
session.json gains no new key.

Related: AvengeMedia#1483 asks for a configurable timer. The field added here is the same
shape doNotDisturb uses with doNotDisturbUntil, so a later `idleInhibitedUntil`
slots in beside it without another migration.
@bbedward

Copy link
Copy Markdown
Collaborator

/claude review

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Claude review

No issues found.

Verified the restore path both ways: SessionData declares signal loaded (emitted after Store.parse in both loadSettings and parseSettings), so onLoaded covers "service completes first" and Component.onCompleted covers "session already parsed" — enableIdleInhibit() is idempotent, so the second is a no-op. SessionService has no other Component.onCompleted to clash with, the new idleInhibited key does not collide with an existing spec/property name, and consumers (IdleService._applyMonitorEnableds, DankBarWindow IdleInhibitor, control center, OSD) read the value through bindings or recompute from current state, so an early restore is picked up correctly rather than depending on the inhibitorChanged emission. No new mutual-singleton hazard beyond the existing SessionData -> SessionService Connections.

Checked: restore ordering/idempotency, consumers of SessionService.idleInhibited, spec/JSON round-trip and default omission, no Go-side session schema mirror, no dank-qml-common contract property involved, en.json/template.json untouched, no new user-facing strings. Model: claude-opus-5.

@bbedward
bbedward merged commit 521b56f into AvengeMedia:master Aug 20, 2026
1 check passed
purian23 pushed a commit to AvengeMedia/DankLinux-Docs that referenced this pull request Aug 23, 2026
…138)

Persistence: the inhibitor now survives a shell restart (AvengeMedia/DankMaterialShell#3123).
Before that change it lived only in memory, so `dms restart` — or systemd
bringing the service back under Restart=on-failure — silently turned it off.
Worth stating explicitly, because "Keep Awake" that quietly stops keeping the
machine awake is the kind of thing you only notice when the screen locks.

While in this section, two verbs the shell implements were missing from the
table. Both checked against a running shell rather than read off the source:

  dms ipc call inhibit status      -> "Idle inhibit is enabled"
  dms ipc call inhibit reason ""   -> "Current reason: Keep system awake"

`reason` is documented as taking a required argument because the CLI rejects
the bare form ("Too few arguments provided") even though the QML handler treats
an empty string as a read.

Edited docs/ only, not versioned_docs/: the persistence lands in a release that
has not been cut yet.

Co-authored-by: Mark2Mac <Mark2Mac@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants