Skip to content

Turn Always On Display into a timed auto-lock setting - #338

Merged
kitknox merged 4 commits into
kitknox:mainfrom
o-borovets:feature/always-on-display-duration
Aug 27, 2026
Merged

Turn Always On Display into a timed auto-lock setting#338
kitknox merged 4 commits into
kitknox:mainfrom
o-borovets:feature/always-on-display-duration

Conversation

@o-borovets

Copy link
Copy Markdown
Contributor

Always On Display was all-or-nothing: on meant the idle timer stayed disabled for as
long as rootshell was in front, so letting the screen lock again meant remembering to
switch it back off. It is now a duration on one slider — Off, 1 to 30 minutes, or
Always — under Settings -> Appearance -> Window -> Display.

A timed value behaves like a custom auto-lock interval rather than a countdown that
runs out. The idle timer is disabled and a one-shot timer runs for the chosen span,
and every interaction restarts it: walk away from a streaming log and the screen locks
that many minutes after the last touch or key press, keep working and it never locks.
The window is never spent, so the next interaction arms a fresh one. Always is the old
behavior, Off hands the screen back to the system's auto-lock schedule, and the row's
caption states what the selected value does.

Interaction sources

Four, because no single one covers everything:

  • A window-level gesture recognizer for taps. It fails on the first touch it sees,
    with cancelsTouchesInView and both touch delays off, so it cannot claim a gesture,
    delay delivery or cancel touches below it. Attached as windows become visible, which
    also picks up the keyboard-hosting windows absent from UIWindowScene.windows.
  • TerminalView.pressesBegan for hardware typing: key events go to the responder
    chain, never to touchesBegan.
  • UITextField and UITextView text-change notifications, for typing in Quick
    Connect, search fields and dialogs.
  • The app-wide command fallback, which receives chords while TerminalView is out of
    the responder chain.

Rearming is coalesced to once a second. Two paths still do not restart the window: a
UIKeyCommand handled by a responder that is neither the terminal nor the fallback,
and dictation into the terminal.

Lifecycle

The timer is scheduled in the common run-loop mode, so a long scroll or selection drag
cannot postpone the deadline, and it fires synchronously. Going through a Task would
let a rearm land between the fire and the callback, and the stale callback would then
tear down the newer hold: Always selected while the screen locks anyway. A
timer-identity check covers that shape regardless.

Entering the background drops the assertion and the pending timer while keeping the
setting, and arming is refused while the app is backgrounded, since
ProtectedDataGuard runs during background launches and unlocks by design. UIKit also
asks for isIdleTimerDisabled to be reset once it is no longer needed.

Migration and scope

A persisted alwaysOnDisplayEnabled = true becomes Always on first launch and the old
key is removed. The migration runs only where protected data is available, so a locked
launch cannot mistake an unreadable UserDefaults for "never configured" and overwrite
a real setting.

iOS and iPadOS only, as before. Mac Catalyst exposes isIdleTimerDisabled but it does
nothing for Mac display sleep, which power assertions govern, and visionOS has no
auto-lock idle timer; the #else branch keeps the App body and the key-input path
compiling on both.

@kitknox

kitknox commented Aug 26, 2026

Copy link
Copy Markdown
Owner

It looks like we could avoid some of the tracker work when this is in an off state which is currently creating some additional MainActor load and gesture recognizers. ie getting to a point that the overhead is only the conditional check and nothing more.

@o-borovets

Copy link
Copy Markdown
Contributor Author

Pushed three commits on top.

Interaction tracking is no longer installed at launch. armHold() installs it when a timed value is selected and removes it when one is not, so Off and Always leave no gesture recognizers on the windows, no notification observers and no timer; the input paths that report interaction return on a stored Bool. The manager's internals are @ObservationIgnored, so the per-touch reads no longer go through the observation registrar.

The two lifecycle observers (didBecomeActive, didEnterBackground) stay unconditional: they fire a handful of times per session rather than per input, and they are what applies the persisted setting after a launch or an unlock. Both are synchronous on the main queue now instead of hopping through a Task, so the assertion is dropped before the process can be suspended.

Software-keyboard, IME and dictation text reaches TerminalView.insertText, not pressesBegan, and the terminal is not a UITextField, so on-screen typing only restarted the window when the keyboard's own window had picked up a gesture. It now calls the same hook as the hardware-key path, which closes the dictation gap listed in the description.

@kitknox

kitknox commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Thanks. Looks good to me now.

@kitknox
kitknox merged commit 43ec46c into kitknox:main Aug 27, 2026
@o-borovets
o-borovets deleted the feature/always-on-display-duration branch August 27, 2026 18:41
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