src/switcher/state/SearchModeResolverSpecs.md
Line coverage:
SearchModeResolver.swift100% · refreshed 2026-05-27 by/coverage-explore
The in-switcher Search feature lets the user filter the window list by typing. SearchModeResolver
is the pure decision kernel for its state machine, extracted from TilesView (same pattern as
SelectionResolver). It decides what should happen; TilesView and ShortcutAction carry out the
AppKit side effects (make the search field first responder, place the caret, refresh the UI, toggle
the Edit menu, call App.cycleSelection).
.off — no search field interaction; the switcher behaves normally..editing — the search field is the first responder and editable; typing filters the list..searchOnRelease style. Search is the session.ProFeature.searchInSwitcher.attemptUse() has side effects (consume the free
pass, surface the upgrade UI), so the caller evaluates it at the real attempt moment and passes a
Bool in. The gate is checked before the state branches, so a denied attempt never mutates mode
(it returns .proGateBlocked). toggle is gate-free — it just routes to the enter/disable path,
which applies its own gate (mirrors the original delegation).enterEditing is only reached from .off).NSSearchField, which handles select-all/copy/paste/cut natively. AltTab does not intercept those.editingShortcutMatch, #5781 generalized):
in search you release the activation modifiers to type, so a when-active shortcut bound to a bare
printable key (e.g. the default closeWindow = W, quitApp = Q) must not steal the keystroke — you
type the letter, and trigger the shortcut by re-pressing the hold modifiers (Cmd+Option+W), exactly
as you would outside search. The whole when-active set (close / minimize / fullscreen / quit / hide /
focus / cancel / search) routes this way; TilesView ORs the per-shortcut match over
Preferences.staticShortcutKeys. The "bare" arm (event == shortcut) is dropped for printable keys,
but only when there are hold modifiers to fall back on (else the shortcut would be untriggerable), and
never for non-printable keys (Escape/Return/arrows) nor bindings that already carry Cmd/Ctrl. The
"with hold modifiers" arm always stands. A Cmd-inclusive hold modifier thus keeps the whole
Option+letter special-character layer (œ, accents) free for typing. The printable/non-printable test
itself (eventProducesText) lives in TilesView, since it reads the NSEvent.previousWindow = ⇧): a bare
modifier is uppercasing/typing input, not a command, so it must not fire while editing; only ⌥⇧
(hold + the modifier) navigates. These arrive as flagsChanged (no keyDown), so they never reach
routeKey; ATShortcut.modifiersMatch feeds them through editingShortcutMatch (with isPrintable: true) while editing instead. Without this they would fire on the bare modifier and you couldn't type
capitals in the search field..editing.TilesView as side effects): caret placement, first-responder
changes, forceDoNothingOnRelease, hover clearing, key-repeat-timer stops, the Edit-menu toggle, and
endSearchSession teardown (which unconditionally resets to .off and is distinct from disable).Mirrors SearchModeResolverTests.swift 1:1.
startMode(startInSearch: true) → .editing.startMode(startInSearch: false) → .off..off → .enterEditing..editing → .disable..off + entitled → .enterEditing..editing + entitled → placeCaretOnly..proGateBlocked..editing → .exitToOff..off → .noOp..exitSearch..closeSwitcher..closeSwitcher..closeSwitcher.cycleSelection(dir)..handled..passToShortcuts..passToField (NSSearchField handles editing natively)..passToField even for an arrow..passToField even for Tab..passToField even for a matching shortcut..editing (off is read-only).cancel = Escape) → bare tap still matches.cancel = Q, bare q with hold released → does NOT match (types). (#5781)Cmd+Option+Q) → matches.Q (typing œ) under a Cmd+Option hold → does NOT match (types).cancel = Cmd+Q) is a command, not text → bare arm honored.closeWindow = W: bare w types; hold+W closes (the generalization of #5781 to the whole when-active set).previousWindow = ⇧: bare Shift uppercases (does not navigate); ⌥⇧ navigates. Modifier-only shortcuts are routed through this kernel from ATShortcut.modifiersMatch because they arrive as flagsChanged, not keyDown.