flash

Normal Mode

Normal mode mappings are owned by Sources/flash/App/NormalMode.swift and the default mapping list in Sources/flash/Config/Config.swift.

Defaults map keys to high-level actions, never to raw key chords: each action resolves in the focused app’s context (see source actions), and an app without the action gets nothing. Other tab, pane, archive, find-match, document-URL and mark commands remain available for explicit mappings.

Mouse grid

mouse_grid (F) splits the screen like the left half of your keyboard: 4 rows × 5 columns, each cell labelled with the key at the same position in the hints.keys layout’s number, top, home and bottom rows. On QWERTY:

+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+
| q | w | e | r | t |
+---+---+---+---+---+
| a | s | d | f | g |
+---+---+---+---+---+
| z | x | c | v | b |
+---+---+---+---+---+

Each cell also shows, faintly, the grid of the step that follows it, every sub-cell labelled with the two keys that reach it (qw is q, then w), so the next key can be read before the first is pressed; the step’s own labels are drawn on top and stay the first thing to find. The clicking step and cells too small for a two-letter label show no preview. hints.mouse_grid_preview = false turns it off. The clicking step’s labels are see-through, so the exact point each one clicks shows under it.

Colemak uses 12345 / qwfpg / arstd / zxcvb and Dvorak 12345 / ',.py / aoeui / ;qjkx; a literal hints.keys gets the QWERTY block, and hints.mouse_grid_keys sets any other matrix. Press the key where you want to go: the grid zooms into that cell and tiles it with the same keys, so every step uses the same muscle memory. The last of hints.mouse_grid_steps (default 3) clicks, as does any step whose cells reach 18 points. When the clicking step’s cells are smaller than a label, they are drawn as one glued cluster centred on (and covering) the chosen cell; each click lands on its label.

The grid starts on the display of the focused window and covers Flash’s status bar, or on the pointer’s display when no window is focused. It still stops at the Dock, and at the menu bar when the Flash bar is hidden.

Key Action
a grid key Zoom into that cell; on the last step, click its centre
space Zoom into the centre; on the last step, click the centre
return Click the centre of the current region now
backspace Undo the last grid key (zoom, move, display switch); with nothing left, back to the labels the grid was opened from
cmd-backspace / alt-backspace Start over on the whole display
arrows Slide the region by its own size, stopping at the display edge
shift-arrows Put the pointer on that edge of the region, so a drag or a held button can start on a grid line
tab / shift-tab Move to the next / previous display
` Toggle cursor-follow (hints.mouse_grid_cursor_follow)
escape Cancel; a pointer moved by cursor-follow goes back
any other key Cancel

Modifiers held on the key that clicks ride the click (hints.magic_modifiers, and Shift always), so Shift-1 Shift-clicks the 1 cell. Command, Control or Option chords outside the magic modifiers cancel. Every click flag works with the grid: --drag and --select pick a first point, then restart on the whole display for the second; Backspace from there returns to the first point’s step. --multi restarts on the same display after each click.

mouse_grid --bisect halves the region instead: h / j / k / l keep the left, bottom, top or right half, and y / u / b / n (drawn as quadrants) keep the top-left, top-right, bottom-left or bottom-right quarter. It ignores the step count and clicks once the kept region is at most 18 points on both sides, or on return; the other grid keys work as above. mouse_grid --zoom-to-depth=N starts N steps deep on the cell under the pointer, on the pointer’s display, stopping while one selection remains; Backspace walks back out.

HOLD: dragging and selecting

HOLD drags something with a mouse button held down. enter_hold_mode grabs what is under the pointer with the left button (--secondary or --middle for another). ⌘⌃H toggles HOLD from NORMAL or INSERT. Then:

The badge is yellow and shows mode.labels.hold (default HOLD). The keys live in [mode.hold.mappings], read before NORMAL’s. An all-mode chord enters HOLD from INSERT as well, and the drop returns to NORMAL.

[mode.all.mappings]
"cmd+ctrl+h" = ["flash", "enter_hold_mode"]

How Flash does it

HOLD is a mouse button Flash holds down (mouse_button; enter_hold_mode is its toggle). Every pointer move Flash makes while a button is held is a drag of it. The press, drag and release do not inherit modifiers from the shortcut that started the hold, so a Control shortcut does not turn the press into a context click. The mode follows the hold exactly: whichever path presses the button from NORMAL or INSERT enters HOLD, whichever releases it leaves, and leaving the mode any other way (enter_normal_mode, INSERT, the command line, leave_mode, quitting Flash, a committed click) releases the button. mouse_pointer’s v holds a drag the same way. A half-typed NORMAL sequence is dropped when the button goes down or up, since the tables differ. After a scroll Flash re-posts the held button’s drag where the pointer is, once after the scroll and once after the app’s own smooth scroll settles, so the app sees the pointer over what scrolled under it.

Movement history

ctrl+o and ctrl+i move backward and forward across apps and source locations, including browser tabs and tmux windows. App focus and location-catalog changes feed the same history. Repeated observations of the same location coalesce; returning to a location after visiting another remains a chronological stop. Choosing a new destination after moving backward discards the forward branch.

Locations restore through their owning source. A browser tab restores through its flash-browser://tab?pid=…&url=… route: the browser process and the tab’s URL, or its title when the tab exposes no URL, so a title change keeps the destination and a restarted browser’s stops fail rather than land elsewhere. Chromium browsers and Safari select the first tab with that URL by AppleScript; Firefox re-walks its tab strip and presses the match through Accessibility. Tmux keeps stable window IDs so reordering a window does not change its history destination. Apps without a more precise source restore application focus. The stack does not capture page scroll positions, editor cursor positions, or terminal scrollback offsets. History is in memory and bounded to 20 stops in each direction.

Source actions

Every verb that asks the focused app to do something is a high-level source action: tabs, panes, reload, archive, back/forward, gg / G, and the app’s generic commands — app_undo, app_redo, app_find, app_save, app_print, document_open, window_new, window_close, clipboard_copy, clipboard_cut and clipboard_paste, which back u, ctrl+r, /, :w, :print, :e, :new, :q, :copy, :cut, :paste, the yank fallback when no Accessibility selection is exposed, and the paste that delivers a flashlight answer. No default or bundled mapping sends a raw chord instead (send_key remains an explicit escape hatch). Each action runs one policy that knows no app and no chord:

  1. a source that performs it in the focused app: tmux for the windows and panes of the tmux client hosting the focused terminal, the browsers plugin, the Accessibility tab strip for tab_select;
  2. for window_close, the focused window’s close button (Flash’s own step);
  3. else the binding a plugin declares for the action in that app (action_bindings, see plugin protocol): a chord, a chord sequence, a menu-bar item pressed through Accessibility, or false for an app without the action;
  4. else Flash’s own scrolling for resource_next / resource_previous (wheel lines) and scroll_top / scroll_bottom (the focused-window scroller);
  5. else nothing.

The macOS conventions — Cmd-Shift-[ / Cmd-Shift-] switch tabs, Cmd-T opens one, Cmd-W closes it, Cmd-1…Cmd-9 select one, Cmd-[ / Cmd-] go back/forward, Cmd-Z / Cmd-Shift-Z, Cmd-F, Cmd-S, Cmd-P, Cmd-O, Cmd-N, Cmd-C / Cmd-X / Cmd-V — are "" bindings of the bundled defaults plugin, which also carries the apps that differ (editors, Messages, Xcode, Notes and friends). Browsers, VS Code and terminals override them in their own plugins. With [plugins] disabled = ["defaults"] those actions do nothing. Reload, reopen, the last tab, tab moves, panes and archiving have no shared binding: their chords mean different things across apps, so they act only where a source or an app-specific binding knows the app. The inspector’s Mappings page lists how each action resolves in the focused app.

A source that claims an action and fails reports .failed, and no binding follows it; a menu item that is missing or disabled fails the same way. App knowledge lives in manifest data (the browsers, defaults, terminals and vscode plugins), never in host conditionals. A terminal treats a Command chord a plugin binds for that emulator as bound, like the chords every emulator binds; any other Command chord is refused (see below), while a menu press is always allowed.

Shared mode exit

leave_mode, enter_insert_mode, enter_command_mode, enter_terminal_mode, and focus_input ship without default mappings in every scope except TERMINAL, whose Command-W is leave_mode. Bare a, A, i, I, o, O, and gi do not enter INSERT. Users explicitly choose their shortcuts, including bindings that prefill the command line with :flashlight.

NORMAL is hermetic: every unmapped key and modifier chord is swallowed, and only explicit mappings act. Map a key to the high-level action it stands for; for a chord no action covers, send_key is the explicit escape hatch, or the user enters INSERT first. The release of a swallowed key is swallowed with it, because a terminal running the Kitty keyboard protocol encodes key releases to its pty. A held key’s repeats follow its press: a key whose press entered INSERT repeats nothing into the app, and a key the app received keeps repeating there.

A synthesized chord reaches an iOS app (Mac Catalyst or iPad) with its modifiers pressed and released around the key, as a keyboard sends it: UIKit matches key commands against the modifier state it tracks from those presses and ignores a lone key event carrying modifier flags.

Hermeticity also bounds what NORMAL synthesizes. A terminal emulator does not ignore a Command chord it has no binding for: its encoder falls through to the plain-text path and writes the chord’s base character, so an unbound cmd+g types a literal g into the shell, hardware or synthetic. In a terminal Flash therefore refuses to synthesize any Command chord outside the set every emulator binds (copy, paste, close, new tab, new window, quit, find, the tab digits, and Shift-bracket tab traversal); a chord a plugin binds for that emulator specifically, such as the bare brackets of the emulators whose splits live on them, is added. A plugin-wide binding for every app never is, so app_save does nothing in a terminal rather than type an s. A refused mapping does nothing and NORMAL stays.

The four vertical scroll bindings synthesize line-based mouse-wheel events in every app, including terminals, at the current pointer position. They use no app-specific scrolling action or Accessibility scroll fallback. The receiving app handles the event just as it handles a physical wheel. With [mode] scroll_smooth_ms set, one keypress becomes several smaller line events spread over that duration (at most 300 ms): the first moves at once, the rest follow at least a frame apart on the click queue, and they add up to the same lines, so terminals scroll exactly as far. A new scroll, including gg / G and h / l, drops what the previous one has left. [mode] scroll_step continues to set horizontal movement in pixels. gg and G retain their source-aware edge behavior. Inside tmux they follow tmux’s own wheel routing: a pane in a mode scrolls that mode (history-top, cancel); a program that tracks the mouse, such as a full-screen CLI, owns its scrolling and receives 1,000 wheel reports; any other pane enters copy-mode at the top of its history and is already at its live bottom. Another terminal scrolls 1,000 lines as a line wheel, like the four vertical bindings.

NORMAL is persistent by default; enter_normal_mode takes no persistence option. Opening Find (/) and a new tab (t) are the two actions that ask to type next: once the action reached the app (a source performed it or its chord was sent) Flash enters INSERT, once however large the count. A claiming source that failed, an app without the action, a missing app or a chord a terminal would refuse leave NORMAL in place. Switching tabs and focus_input preserve NORMAL even when the app focuses an editable field. Accessibility focus notifications never change the mode. A primary f hint click enters INSERT only when its selected target is an input; other hint targets keep NORMAL. F follows the same rule: the grid point is hit-tested before the click, and only a primary, double or triple click on a text input enters INSERT. Physical app clicks and an explicit enter_insert_mode mapping also hand typing to the app. A text input is a text-field, text-area, combo-box or search-field role, or any element with the search-field subrole. In an iOS app (Mac Catalyst or iPad, such as Messages and WhatsApp) it must also accept keyboard focus: UIKit reports read-only text such as a message bubble with a text-area role.

The status pill resolves its mode label and palette from the current mode together. Background status evaluations preserve the live #{flash.mode} reference, so plugin updates and queued renders cannot repaint an old label with the new mode’s colors. Mode labels update without a fade.

Bind ["flash", "leave_mode"] in [mode.all.mappings] to enable advanced mode with one exit shortcut. It returns INSERT to NORMAL, closes command-line, finder, and terminal surfaces using their recorded return mode, and does nothing in idle NORMAL or with advanced mode disabled. enter_normal_mode remains the explicit action for selecting NORMAL even when a command panel was opened with --restore-mode.

Advanced-mode eligibility follows the base mode through command, finder, and terminal surfaces. Opening a surface while disabled cannot enable NORMAL. Changing the enabling binding while a surface is open updates its return mode: enabling returns to NORMAL, and disabling returns to the disabled base mode. A context menu Flash saw open keeps the keyboard until it closes: a recapture a system event asks for — an app launching, a Space change, the panel resigning key — never ends it, while its close notification, an explicit mode entry or another app coming forward does. A surface that posts no menu notifications keeps the bounded window that stood in for a close signal. While a context menu is open its app’s NORMAL mappings still fire, so f labels the menu’s items rather than the window behind it: the walk starts at the menus the app reported opening (submenus included; a browser’s context menu is reachable no other way) and keeps targets inside their frames. A submenu closing leaves the menu it hangs from in charge of the keyboard. The hint session takes the keyboard back from the menu while it runs, so labels and Escape reach Flash, and gives it back when it ends. This needs the session tap: Flash’s key-window fallback would close the menu by becoming key, so under secure input f walks the window as before. A label or configuration refresh preserves native-menu capture suspension; only an explicit mode entry resets that interaction context. Reentrant events wait until the current mode effect batch finishes, preserving transition order.

Modified bindings in [mode.normal.mappings], [mode.insert.mappings], and [mode.command.mappings] override the same physical chord from the all-mode map. The command map is empty by default, so the shared exit works without duplicating it per mode.

Command surfaces try the configured mapping matcher before native editing or finder navigation. Their local performKeyEquivalent path uses the same precedence as Carbon. The global tap still passes command typing.

The all-mode and scoped Carbon registries share an event dispatcher. Their hotkey identities must be distinct, and a handler must decline events owned by the other registry. Reused identities or consuming unowned events can break command shortcuts while NORMAL/INSERT still work through the keyboard tap.

Input capture and latency

NORMAL and hint input normally arrives through KeyboardCaptureTap, so the overlay can remain non-key and the focused application keeps its active window appearance. Command-line and modal surfaces still use the panel’s key-window path, as does the fallback when macOS refuses the Accessibility-backed tap. Startup resolves tap availability before entering NORMAL or a capturing surface. Starting the tap after NORMAL renders would activate Flash through the fallback path and leave the previous app inactive until another app switch.

Each hint or grid session fixes its capture path when it starts (KeyboardCaptureTap.sessionCapture). Under secure input (a focused password field) macOS hides keys from every tap, so that session takes the key window instead and typed labels reach Flash, never the password field. A commit hands focus back by raising its target; a cancel yields activation to the covered app. #{flash.secure_input} and flash status show the state. The tap refreshes it when a key still reaches Flash. While the flag is on the tap receives nothing, so core:secure-input polls it (low priority, every 2 s) for the life of the resident; there is no banner. A focused secure field, and a text field in the app that holds the flag, are left alone. Anything else is a stuck hold, and Flash asks that app to release it once: a Chromium browser blurs its focused element and, if the flag stays on, is sent to its address bar; Firefox and Safari are sent to the address bar; any other holding app is activated and the app you were in is restored. The first browser blur can ask for Automation. Flash does not lock the screen and does not show a dialog. One attempt per time the flag turns on. A password field that is focused when the attempt would start is left alone; one that appears after the attempt has started does not abort a release already in progress.

Interpreters read keys through [app] keyboard_layout’s reference table (KeyCharacters.read, one lookup per key): labels, the grid, pointer and adjustment keys and NORMAL mappings match by key position under a non-Latin input source, while --search keeps the typed text. Which characters those positions spell comes from [keyboard] layout, not from this input-source table. The table is rebuilt on input-source changes and config loads, never per key, and the swallow decision never reads it.

Handing activation back needs the cooperative API: NSApp.yieldActivation(to:) then app.activate(from: .current). NSApp.deactivate() and a bare activate(options:) are ignored on current macOS, and they leave Flash active with no key window: the next command-line open cannot make the panel key, so AppKit draws no caret. Closing the command line without running anything hands activation back this way; a submit that opens an app hands it over through that app’s own activation. The command line’s caret is AppKit’s insertion point, and it is live only while the panel is key with the field editor as first responder. An earlier INSERT handoff may finish after a rapid command open and take key status back; the panel’s key-resign event schedules a guarded recapture while the command field is still visible and no other Flash window owns focus.

The tap source, Carbon callbacks, AX observer sources, and mode coordinator all share the main run loop. Treat that loop as the input latency budget:

MainRunLoopStallObserver records a main_busy warning, at any log level, for every main run-loop busy stretch over 250 ms. It needs no timer: the run loop reports each wake and each return to sleep. The warning carries last_activity_ms_ago: the most recent coarse main-thread units of work (tap_key, mode_effects, mode_overlay, effective_mappings, activation, hint_commit, config_reload) with their age, so a stall names what main was doing. Call MainThreadActivity.note at the top of any new coarse main-thread unit of work. A timeout-disabled event tap also logs before being re-enabled; either message is evidence of main-thread work that needs moving or narrowing.

Two debug-level probes measure the path itself: [latency] tap_to_route is the delay from the HID timestamp to the main-thread turn that routes a swallowed key, and [latency] normal_dispatch is the synchronous cost of one normal-mode action. At info, [latency] hints_visible times each hint activation from its trigger to the Core Animation commit that shows it; see performance. Scripts/measure-footprint.sh samples the resident and its children (CPU, idle wakeups, memory, descriptors) and summarises stalls and log volume for a before/after comparison.

Interaction ownership

ActivationLifecycle distinguishes discovery, a pending commit, and an active gesture. A newer hint request replaces discovery or cancels a commit before its input starts. Once a gesture starts, it finishes its mouse release before the latest queued activation runs. Cancellation suppresses the old gesture’s mode and UI outcome; an old completion cannot clear a newer operation’s state. Dock and scroll-area discovery use the same ownership tokens as ordinary hints.

HintSession owns the selected action and target data. ActionDispatcher owns the one button mouse_button or mouse_pointer’s v holds: a pure MouseButtonHold decides which press and release events each transition posts, so a press is released exactly once. While it is held, every pointer move Flash makes posts that button’s dragged event; a session reset or replacement keeps it, so mouse_button --state=down then mf drags to a hint. Cancelling a Flash overlay, leave_mode, application shutdown and a committed click, drag or selection release it. Shutdown also waits for finite synthetic gestures to post their release events. Click repetition records a pending click only when its generation is still current and input actually starts.

CandidateFinderSession owns warm snapshots, query answers, live results, and their scoring caches. CandidateFinderCoordinator manages the prompt and publication lifecycle. Live source results stay separate from the warm catalog: each exact source/query has a token checked before background preparation and again at publication. Changing or leaving that query clears its rows and rejects both stale replies and timeout results. Initial snapshot publication never overwrites the current live-query result.

Hyper

Holding mode.normal.hyper enters HYPER until that key is released. The default is <leader>, which follows mode.normal.leader. When the key is <leader> and the leader is not set (leader = false), HYPER stays off. Any other single key works in its place (a, <space>, cmd+space). The status pill is purple and shows mode.labels.hyper (default HYPER). Keys pressed while it is held are read from [mode.hyper.mappings], not from NORMAL. The defaults are the Apple function row on 1–9, 0, -, and =: brightness, Mission Control, Spotlight, Dictation, Focus, media, mute, and volume. Shift plus the same key sends F1–F12. Arrows sit on the physical keys where QWERTY has e s d f (up, left, down, right), spelled as the letters those keys type in [keyboard] layout: QWERTY e s d f, Colemak f r s t, Dvorak . o e u, Bépo p u i e. F is the home-row index-finger mark, and it is right. Shift posts a shifted arrow on that same letter, so Colemak down is shift+s. A punctuation key uses the character Shift types there, so Dvorak up is >. A mapping or false on one of those keys wins. The arrow is posted into the focused app’s key window, downstream of the session tap. postToPid carries no window, so a terminal drops it while the hyper key is still down. Volume is a system event and does not take this path. F-keys use the same window posting. Pointer movement and bisect stay on the hjkl columns.

While the hyper key is the leader, a key with no hyper mapping can still complete a NORMAL <leader> sequence when that key continues one. Unmapped keys are swallowed.

A hold enters HYPER right after a repeatable mapping too: ]t arms a repeat of t, which any other key drops, the hyper key included. Only a mapping whose final key is the hyper key keeps that press as its repeat. Entering HYPER drops whatever NORMAL had pending, with its timeout.

Releasing the key returns to NORMAL. The session tap can drop that key-up after it swallowed the press and a synthetic event (a volume key, a posted chord) lands in between; HYPER would otherwise stay up until the key is pressed again. While HYPER is latched, core:hyper-hold reads the key from the HID state table (CGEventSource.keyState(.hidSystemState), and every modifier in a chord such as cmd+space) and leaves once the key has been up for two ticks. Combined session state is the wrong table: a swallowed press can look up there. The next key sees the same reading, leaves first, and is routed as a NORMAL key. A delivered key-up still leaves immediately. The watch is registered only while the mode is HYPER. Leaving HYPER any other way — leave_mode, opening the command line, a focus change — ends the hold and drops its pending keys the same way: the mode transition is the one reset point, so no exit can leave a latched hold behind.

If nothing else was pressed and the hyper key is the leader, that tap arms the <leader> prefix: press it, release it, then the next key, and the sequence runs as before. Holding it again enters HYPER while that prefix is armed. A mapped <leader><leader> sequence still takes the second press. Any other pending sequence stays in NORMAL. INSERT, the command line, a popup, and an open hint session keep their own keys. See configuration.

Every popup is a terminal. TERMINAL sits beside NORMAL, INSERT, COMMAND, and the momentary HYPER layer: enter_terminal_mode [--name=<popup>] enters it by showing the popup standalone (the built-in terminal popup without a name), and clicking a popup’s body or pinning it from the status bar enters it too. Either way the popup’s local terminal view gets focus. leave_mode leaves it, closing the popup. Entering the popup already focused is a no-op; entering another replaces it. The overlay owns no keyboard input in this mode: the existing global tap passes keys through, every Carbon registration is suspended, and only [mode.terminal.mappings] can intercept keys in the popup. The label is configured with mode.labels.terminal and defaults to TERMINAL.

Terminal mappings inherit only the effective INSERT-active bindings whose winning action is enter_normal_mode or leave_mode. Scope and plugin precedence are resolved before this inheritance; an explicit terminal mapping overrides an inherited binding with the same canonical key. Other all, normal, and insert bindings are inactive. Plugins may contribute terminal mappings using the same priority rules.

The local sequence recognizer accepts the shared key syntax, including modified chords and explicit sequences, but has no implicit Escape behavior, counts, register prefixes, or <leader>. Only known sequence prefixes wait for mode.sequence_timeout_ms. An exact mapping that also starts a longer sequence waits for that timeout; a mismatch resolves the longest completed mapping and reprocesses the remaining keys. Unmatched events retain their original modifiers and are replayed exactly once to the terminal that received them; a key press replayed to a fresh popup whose process has ended closes the popup instead (lifecycles). Focus and configuration changes flush unresolved events without dispatching a pending command. repeat = true retains the explicit final-key repetition behavior.

Local mappings run before native copy/paste and terminal key encoding. Text popups use the same focus mode for selection, copying, and scrolling. Leaving via enter_normal_mode dismisses the popup and activates the captured external app before NORMAL recapture. Losing popup focus restores the prior base mode without activating a different app. The app captured on entry is part of the TERMINAL mode value and is forgotten with it: the next popup captures its own, and no other exit can activate an app a popup once covered. A popup’s lifecycle, not its focus or visibility, decides how long its process lives; see popups.

leave_mode provides one configured exit across surfaces. It dismisses a popup, text or terminal alike, and restores its prior base mode/app (the default [mode.terminal.mappings] bind it to Command-W), restores the saved mode from command or finder input, and leaves INSERT for NORMAL. In NORMAL or disabled mode it only dismisses active hints and is otherwise a no-op. An all-scope binding to either enter_normal_mode or leave_mode enables advanced mode. For a shifted bracket, use "cmd+shift+[" = ["flash", "leave_mode"]; the key matcher handles the { character produced by Shift.

Rejected commands

Unknown commands and unsupported subcommands do nothing visible: no toast, only a warning log naming the invocation and pointing to the mapping or configuration. Invalid mapping arrays report their source location during configuration loading. Malformed built-in commands cannot silently become plugin calls. Plugin execution failures still raise the error toast.

The CLI accepts --key=value and boolean --flag arguments, rejects stray positional subcommands, and returns status 2 when parsing or resident dispatch rejects an invocation. Successful dispatch does not imply an asynchronous plugin operation completed successfully; later failures appear in the toast and logs. An empty command prompt remains quiet.

Explicit INSERT entry

The default mapping set has no i, I, a, A, o, or O insert aliases. INSERT is entered by:

Other normal commands, focus changes and app activation preserve NORMAL. Moving the pointer, dragging, or selecting with the grid does not request INSERT. A multi-click session ends when its click enters INSERT. Otherwise mouse_target --multi discovers the app’s targets again after each click, so controls the click revealed get hints and removed ones lose theirs; a target that is still there keeps its label. The previous labels stay drawn until the new set replaces them, and an app left with no targets ends the session. Use leave_mode / enter_normal_mode to return from INSERT to NORMAL.

[mode.all.mappings]
"cmd+ctrl+i" = ["flash", "enter_insert_mode"]
"cmd+ctrl+[" = ["flash", "enter_normal_mode"]
"alt+space" = ["flash", "enter_terminal_mode"]

Closing a popup or command surface may restore its saved INSERT mode.

INSERT’s keys belong to the focused app, so Flash never stays the active app in INSERT. Whenever Flash shows INSERT or becomes active while in it, it hands activation back to the app it tracks as focused. Otherwise every key the tap passes, Command chords included, would reach Flash instead. About, a hint session and a physical click that is still activating its own app keep it.