src/switcher/KeyRepeatTimerSpecs.md
KeyRepeatTimerTestable.shouldApplyArtificialRepeat is the pure decision kernel for the artificial
key-repeat (hold-to-cycle for modifier-only shortcuts). KeyRepeatTimer owns the DispatchSource
timer; on each tick it asks this kernel whether the tick should actually advance the selection.
The artificial repeat exists because a modifier-only shortcut (e.g. ⌥⇥ where ⇥ has no keycode of its
own once ⌥ is the hold) gets no OS key-repeat, so AltTab synthesizes one. The timer is armed at
show-invoke time with the user's InitialKeyRepeat delay + KeyRepeat rate.
When the user summons the switcher right after a fullscreen/Space transition, the WindowServer is busy
settling the transition and can't paint AltTab's .canJoinAllSpaces panel until it finishes — the
panel's pixels can land ~500ms after the timer was armed. The timer's background DispatchSource
keeps ticking through that gap; the queued ticks then all fire the instant the panel appears and jump
the selection several tiles, with no input from the user (they'd pressed once and were waiting for the
switcher to show).
So the initial-delay grace must be measured from when the panel was actually visible, not from arm time. A fast, normal summon is unaffected (visible within ~tens of ms of arming).
Anchoring only on SwitcherSession.panelBecameVisibleAt — our own panel's WindowServer orderedIn —
made the fallback the NORMAL path, because that notification never arrives. Order-in is only delivered
for wids in the per-window opt-in set (WindowServerEvents.wsWindows, mandatory since Sequoia), and the
panel is not in it. So every tick fell through and hold-to-cycle started at
armedAt + initialDelay + 1s: measured 1377ms against a system InitialKeyRepeat of 417ms, on
every hold, felt by every user. (Carbon hotkeys fire once per press and ignore auto-repeat keyDowns —
verified in the log: 14 auto-repeat keyDowns produced exactly one state:down — so the artificial repeat
is the only thing that can advance the selection while the key is held.)
The panel was NOT added to the opt-in set to fix this: that would feed our own panel's order/geometry
events into the reducer as inputs for an untracked wid, which is a real risk for a timing nicety.
Instead TilesPanel.show() sets SwitcherSession.panelShownAt as it orders the panel front — an anchor
that cannot go missing. It is slightly EARLY (the WindowServer paints after the order-front returns), so
the visible signal stays above it as the correction rather than replacing it, and the tradeoff is stated
plainly: on a genuinely slow show the grace now starts at the order-front instead of at the paint, which
can let ONE advance land early. That is the price of not being a second late on every ordinary summon.
Inputs are systemUptime timestamps. The anchor is panelBecameVisibleAt ?? panelShownAt:
now - anchor >= initialDelay. The WindowServer's visible
timestamp wins when present, being the true pixels-on-screen moment; the show timestamp is the
guaranteed stand-in.initialDelay + missedVisibleSignalBudget (1s), so a missing anchor can never wedge hold-to-cycle
permanently. A genuine safety net now, rather than the path every tick took.Mirrors KeyRepeatTimerTests.swift 1:1.
panelShownAt, not from arm + 1s.>=).initialDelay + 1s.>= 0.4 + 1), so a missed visible signal doesn't wedge hold-to-cycle.