ink-cartridge
    Preparing search index...

    Class KeyboardEngine<TComponent>

    Framework-agnostic keyboard state machine.

    Owns all mutable keyboard state — bindings, layers, focus targets, global keys, modes, conditions, and the processor pipeline — without depending on any specific UI framework. A host framework (React, Vue, Blessed, etc.) creates an instance, calls sync on each render to push page-path, layer, and modal-layer state, and calls processKey for every keyboard event.

    The generic TComponent represents the host framework's component type. It defaults to unknown so the engine never constrains the host — all framework-specific detail lives in the normalizeKeyNames adapter and the custom processors.

    const engine = useRef(new KeyboardEngine({
    modes: ['normal', 'insert'],
    normalizeKeyNames,
    })).current;

    engine.sync({
    pagePath: ['app'],
    layers: [],
    modalLayers: [],
    });
    useInput((input, key) => engine.processKey(input, key));
    import { KeyboardEngine } from '@cartridge-engine/keyboard-engine';
    import * as readline from 'node:readline';

    function isSpecialKey(key: unknown): boolean {
    const k = key as Record<string, unknown>;
    return !!(k.ctrl || k.meta || k.shift || k.name === 'escape' || k.name === 'tab');
    }

    const engine = new KeyboardEngine({
    isNormalChar: isSpecialKey,
    normalizeKeyNames: (input, key) => {
    const k = key as Record<string, unknown>;
    if (k.ctrl && k.name) return [`ctrl+${k.name}`];
    return [k.name ? String(k.name) : input];
    },
    });

    engine.sync({ pagePath: ['app'], layers: [], modalLayers: [] });

    engine.boundKeyboard(['ctrl+c'], () => {
    console.log('Goodbye!');
    process.exit(0);
    });

    readline.emitKeypressEvents(process.stdin);
    if (process.stdin.isTTY) process.stdin.setRawMode(true);
    process.stdin.on('keypress', (_input, key) => {
    engine.processKey(_input ?? '', key);
    });

    // Mouse events (from the bundled Mouse helper) are fed the same way:
    // mouse.on('click', (event) => engine.processMouseEvent(event));

    Type Parameters

    • TComponent = unknown

      The host framework's component reference type.

    Index
    • Create a new engine instance.

      Builds the internal state tree: EngineState (path, layer/modal-layer ids, modes, conditions, global keys/sequences, operation registries, owner stack), LayerManager, PipelineManager (the built-in 9-stage chain plus any custom processors), BindingService, OperationRegistry and the CompositionEngine (initialized with defaultTimeout and valueSchema).

      The instance is meant to persist for the lifetime of the host component — store it in a stable reference (e.g. useRef in React, a class field in Vue/Svelte).

      Type Parameters

      • TComponent = unknown

        The host framework's component reference type.

      Parameters

      • props: EngineProps<TComponent>

        Engine configuration. normalizeKeyNames is required — the engine has no built-in default so each framework must provide its own adapter.

      Returns KeyboardEngine<TComponent>

      const engine = new KeyboardEngine({
      normalizeKeyNames: (input, key) => {
      const k = key as Record<string, unknown>;
      return [k.name ? String(k.name) : input];
      },
      isNormalChar: (key) => {
      const k = key as Record<string, unknown>;
      return !!(k.ctrl || k.meta || k.escape || k.tab || k.upArrow || k.downArrow);
      },
      modes: ['normal', 'insert'],
      defaultMode: 'normal',
      });
    • get composition(): CompositionEngine<TComponent>

      The composition engine for composing multi-key compound actions.

      Returns CompositionEngine<TComponent>

    • Cancel the current composition chain immediately.

      Returns void

    • Activate a focus target in a group that currently has no active focus.

      Unlike focusSet — which replaces a group's active target — this method only succeeds when the group has no active entry yet. It is designed for lazy activation: register focus targets early, then call activateFocusGroup to give a group its initial focus on demand without overwriting focus that was already established.

      Returns false (no-op) when the group already has an active target, or when the owner, layer, group, or focus target is absent. Use focusSet when you need to switch a group's active target regardless of its current state.

      Parameters

      Returns boolean

      true if the target was activated, false if the group already had an active target or the target/group/layer could not be found.

    • Re-activate a previously kicked built-in processor by removing it from the disabled list. The processor resumes normal operation on the next processKey call.

      When a processor is actively processing events (i.e. not in the disabled list), calling activeProcessor is a no-op that returns false.

      Parameters

      Returns boolean

      true if the processor was re-activated, false if it was already active or the id was not found in the disabled list.

    • Add a single shortcut action.

      Parameters

      Returns void

      If the actionId already exists.

    • Register a named condition for when: "conditionId" references.

      Parameters

      • id: string
      • defaultVal: boolean

      Returns boolean

      true if registered, false if the id already exists.

    • Register a mapping key entry. See CompositionEngine#addMapping.

      Parameters

      Returns boolean

      true if registered, false if base is empty, any target key is not registered, or an identical base already exists.

    • Register a mode name. Modes must be registered before use in setMode, nextMode, or prevMode.

      Parameters

      • mode: string

      Returns boolean

      true if added, false if already registered.

    • Insert a processor into this instance's pipeline at a specified position.

      Options (checked in order):

      • { index: n } — insert at 0-based index
      • { before: "id" } / { after: "id" } — insert relative to a named processor
      • omitted — append to the end

      Parameters

      Returns void

      If the processor id duplicates an existing one or the target is not found.

    • Add a single sequence action.

      Parameters

      Returns void

      If the id already exists.

    • Allow specific keys to pass through the modal barrier. By default the active modal consumes every key event — even unbound keys. Adding a key to the allow list releases it to lower pipeline stages.

      This is the only mechanism by which keys can escape the modal barrier. In the modal pipeline stage (stage 0), allowedKeys is checked before any other processing: a matching key (whose when evaluates to true) makes the modal processor return false, so the event continues to the global/layer/page stages below. Keys released this way count as unhandled ("miss") for useModalMissListener when nothing else consumes them.

      Parameters

      Returns () => void

      A function that removes the allow entry, restoring the default behavior (the modal blocks the key again).

      If not called on a modal layer.

      // Allow arrow keys through the modal so the underlying screen can still navigate
      engine.allowModal(['up', 'down', 'left', 'right']);

      // Allow escape only when a condition is met
      engine.allowModal(['escape'], {
      when: () => !isCriticalOperation,
      });

      // Focus-scoped allow
      engine.allowModal(['enter'], { focusId: 'transferInput' });
    • Bind one or more keys to a handler on the current owner's layer.

      Supports three calling conventions:

      1. boundKeyboard(keys, handler, options?) — explicit keys and callback
      2. boundKeyboard(keys, actionId, options?) — explicit keys, shortcut action by id
      3. boundKeyboard(actionId, options?) — uses the shortcut action's preset keys

      Storage follows the options: with elementId the binding is stored on that element's keyboard data; with focusId on the named FocusTarget.bindings array; with neither, on the page layer's bindings array (or the element keyboard's bindings inside a layer/modal layer). Element bindings are evaluated in the layer broadcast stage and the modal stage; page-level bindings in the screen-stack stage, after global keys and layer broadcast. Within a keyboard layer, focus-target bindings are checked before layer-level bindings.

      Options behavior:

      • once — auto-remove after the first invocation; the unbind happens before the handler runs
      • times / observer — the handler fires after times presses; the counter resets after the handler runs, and observer is called on each press while counting (requires times)
      • when — a condition function or a registered condition id (see addCondition); the binding is skipped when it evaluates to false
      • mode — restricts the binding to a specific mode; skipped when the active mode doesn't match
      • stopsWorkingAfterLayerAppearing — page-level bindings only: when any layer is present the page binding stops responding; has no effect inside a layer

      Parameters

      Returns () => void

      An unbind function. Removes the binding from the layer immediately; safe to call multiple times.

      If no current owner exists, times < 1, or observer without times.

      // Inline handler
      const unbind = engine.boundKeyboard('return', (input, key) => {
      console.log('submit');
      });

      // Via shortcut action
      engine.boundKeyboard('ctrl+s', 'save');

      // Via action preset keys
      engine.boundKeyboard('confirm');

      // With options — one-shot, focus-scoped, press-counted
      engine.boundKeyboard('escape', handleCancel, {
      once: true,
      focusId: 'dialog',
      times: 2,
      when: () => isDirty,
      mode: 'normal',
      });
    • Register a multi-key sequence binding on the current owner's layer.

      When the first key of a registered sequence is pressed, the layer enters a pending state. Subsequent key presses are matched against the remaining keys; when all match within the timeout the handler fires. A mismatched key cancels the sequence (default) or is silently consumed (exclusive: true). The when condition (callback or registered condition id) is checked at each key press — if it returns false, the pending sequence is cancelled.

      Supports three calling conventions:

      1. boundSequence(keys, handler, options?) — explicit keys and callback
      2. boundSequence(keys, actionId, options?) — explicit keys, sequence action by id (the action's callback is resolved at registration time)
      3. boundSequence(actionId, options?) — uses the action's preset keys

      The sequence timeout defaults to 500 ms — the timer starts on the first key and resets on each match. Unbinding while a sequence is pending does not cancel it.

      Throws if fewer than 2 keys are provided, if the first key conflicts with a global sequence that has cover: false, or if observer is set without times.

      Parameters

      Returns () => void

      An unbind function.

      // Explicit keys with handler
      engine.boundSequence(['g', 'g'], () => {
      scrollToTop();
      });

      // With timeout and exclusive mode
      engine.boundSequence(['ctrl+w', 'q'], handleQuit, {
      timeout: 1000,
      exclusive: true,
      mode: 'normal',
      });

      // Via sequence action
      engine.defineSequenceAction([{
      sequenceActionId: 'vim-goto-top',
      action: () => scrollToTop(),
      keys: ['g', 'g'],
      timeout: 600,
      }]);
      engine.boundSequence('vim-goto-top');

      // Via sequence action with explicit keys (the action's callback is used;
      // the action's preset timeout acts as a default, overridable per call)
      engine.boundSequence(['ctrl+g', 'g'], 'vim-goto-top', { timeout: 800 });
    • Number of completed sequences available for undo.

      Returns number

    • Build a snapshot of all mutable state needed to process a single key event through the pipeline.

      Called by processKey once per key event. All values are read synchronously to produce a consistent frozen-in-time view.

      The returned object is cast to PipelineContext because the engine's generic TComponent may not match the legacy React.ComponentType in the typed interface — this is a bridge point that will be resolved when the pipeline types are fully generic.

      Parameters

      • input: string
      • key: unknown

      Returns PipelineContext<TComponent>

    • Remove keyboard layers for screens that are no longer in the current path.

      This is the cleanup side of the sync lifecycle: sync pushes new state, these methods remove stale state from the previous render cycle. Designed to be called in a post-render effect (e.g. useEffect in React) so they compare against the state pushed by the most recent sync.

      Also clears any pending sequence timers on removed layers to prevent stale timeouts from firing after the layer is gone.

      Returns void

      useEffect(() => { engine.cleanLayers(); }, [currentPath, engine]);
      useEffect(() => { engine.cleanOverlayLayers(); }, [allLayers, engine]);
      useEffect(() => { engine.cleanModalLayers(); }, [allModalLayers, engine]);
    • Remove element keyboards for modal layers that have been closed. Only cleans keyboards whose modal-layer owner is no longer present in the synced modalLayers state.

      Returns void

    • Remove element keyboards for layers that have been closed. Only cleans keyboards whose layer owner is no longer present in the synced layers state — pages and modal layers are left untouched.

      Returns void

    • Clear all registered composition keys.

      Returns void

    • Clear all buffered undo history.

      Returns void

    • Clear all registered sequence operations.

      Returns void

    • Clear all registered shortcut operations.

      Returns void

    • Check whether the current owner's layer has an active pending multi-key sequence (registered via boundSequence). Unlike thereGlobalQueueWaiting, this only checks the layer belonging to the current owner — the page, layer, or modal layer that owns the active keyboard data. Use this to show sequence-progress hints (like Vim's pending key display).

      When a sync callback is provided it is added to the same pending-sync set used by thereGlobalQueueWaiting and fires after each processKey invocation.

      Parameters

      • Optionalsync: () => void

        Optional callback invoked after each processKey so the host can re-render.

      Returns boolean

      true if the current layer has a pending sequence, false otherwise.

      If there is no current owner (no active page, layer, or modal layer).

      // Show a hint while a local sequence is pending
      if (engine.currentScreenHasSequenceWaiting()) {
      // Display partial sequence indicator
      }
    • Register named sequence actions.

      Parameters

      Returns void

      If any id is duplicated.

    • Register named shortcut actions that can be referenced by key bindings via string identifier instead of inline callbacks.

      Parameters

      Returns void

      If any actionId is duplicated.

    • Enable wildcard-priority mode. In this mode, "*" (wildcard) bindings take absolute priority over exact-key matches.

      By default exact-key bindings ("return", "ctrl+s") are checked before wildcard bindings; with this mode the order is reversed so "*" bindings fire first. Essential for screens that must intercept every key press (e.g. a text input capturing all printable characters).

      Uses reference counting: multiple callers can enable independently. Mode disables when all callers have called the returned disable function.

      Returns () => void

      A disable function. When the reference count reaches 0, wildcard priority is turned off.

      const d1 = engine.enableWildcardPriority();
      const d2 = engine.enableWildcardPriority();
      // Wildcard priority is on
      d1();
      // Still on — d2 hasn't released yet
      d2();
      // Now off
    • Query the currently active focus target for a group.

      Returns a discriminated union rather than a bare id so callers can distinguish the "no owner / no layer / no focus / result" cases without guessing. Check .result?.id for the active focus id, or one of .noOwner / .noLayer / .noFound for the empty cases.

      Parameters

      Returns FocusCurrentResult

    • Cycle to the next focus target within a group (Tab semantics).

      Wraps around. When group is omitted, cycles the default group's defaultFocusOrder; otherwise cycles the named group's registration order. Only switches the active target — does not activate a group that has no current focus.

      Parameters

      Returns void

    • Cycle to the previous focus target within a group (Shift+Tab semantics).

      Wraps around. See focusNext for the group parameter behavior.

      Parameters

      Returns void

    • Activate a named focus target on the current owner's layer.

      When group is omitted, the target is looked up in the layer's default focus group (defaultTargetsSymbol). When group is provided, the target is looked up in the named group — each group tracks its own active focus independently, so multiple groups can hold focus simultaneously.

      Parameters

      Returns void

      If the current owner has no layer, the group is not registered, or the focus target is not found within the group.

    • Remove a focus target from the current owner's layer.

      If the removed target was the active one for its group, the first remaining target (in registration order) is auto-activated. When no targets remain in the group, that group's focus slot is cleared.

      Silently no-ops when the target or group is absent on the current layer — during unmount, sync() has already advanced the path to the new screen, so the focusId lives on the unmounting screen's layer (which cleanLayers() removes shortly after).

      Parameters

      Returns void

    • Return a copy of the current composition context.

      Returns CompositionContext<unknown>

    • Returns string | null

      The active mode, or null in no-mode state.

    • Returns ResolvedGlobalKeyEntry[]

      A shallow copy of all registered global key entries, with string operate values already resolved to functions.

    • Returns GlobalPendingSequence | null

      The current GlobalPendingSequence if one is active — between the first key press and completion or timeout — or null.

      const pending = engine.getGlobalPendingSequence();
      if (pending) {
      console.log(`Waiting for key ${pending.nextIndex + 1}/${pending.sequences.length}`);
      }
    • Returns ResolvedGlobalSequenceEntry[]

      A shallow copy of all registered global sequence entries.

    • Returns HoveredRegion | null

      The currently hovered mouse region, or null.

    • Return the most recent composition event. See CompositionEngine#getLastEvent.

      Returns CompositionEvent | null

    • Return the most recent mapping-key event. See CompositionEngine#getLastMappingEvent.

      Returns MappingKeyEvent | null

    • Register global key bindings. Global keys fire independently of the screen stack, subject to category whitelist and affectLayer placement.

      Evaluated at pipeline stages 3 and 7 — above the layer stage when affectLayer: true, below it otherwise. Entries are matched in registration order — the first match wins. Use this for application-wide shortcuts (quit, toggle dev tools, switch language) that should work on every screen. When cover is false, screens and layer elements cannot override the key via boundKeyboard.

      When operate is a string, it is resolved to a registered shortcut action. Press-count tracking (times/pressCount) is initialized for entries with times.

      Parameters

      • entries: GlobalKeyEntry[]
      • Optionaloptions: { mode?: "replace" | "add" }
        • Optionalmode?: "replace" | "add"

          'replace' (default) replaces all global keys; 'add' appends without removing existing entries.

      Returns void

      If times < 1 or observer without times.

      engine.defineShortcutAction([{
      actionId: 'quit',
      action: () => process.exit(0),
      keys: ['ctrl+q'],
      }]);

      // Replace all global keys
      engine.globalKeys([
      { key: 'ctrl+q', operate: 'quit' },
      { key: 'f1', operate: () => toggleHelp(), category: '*' },
      { key: 'escape', operate: handleEscape, when: () => isModalOpen, mode: 'normal' },
      ]);

      // Add without removing existing entries
      engine.globalKeys([
      { key: 'ctrl+shift+p', operate: openCommandPalette },
      ], { mode: 'add' });
    • Register global sequence key bindings. Global sequences fire independently of the screen stack with higher priority than global keys.

      Evaluated at pipeline stages 2 and 6 — just above the global-key stages. When the first key of any registered sequence matches, the engine creates a pending global sequence and waits for subsequent keys within the sequence timeout (default 500 ms). On a full match the handler fires and the pending state clears; a mismatched key cancels the sequence (default) or is silently consumed (exclusive: true). Use these for application-wide key chords (like Vim-style g g to scroll to top).

      When operate is a string, it resolves to a registered sequence action. In 'replace' mode (default), any active pending global sequence is cancelled before replacement.

      Parameters

      Returns void

      If any sequence has fewer than 2 keys.

      engine.globalSequence([
      { keys: ['g', 'g'], operate: () => scrollToTop(), timeout: 600 },
      { keys: ['ctrl+w', 'q'], operate: 'quit-all', exclusive: true },
      { keys: ['ctrl+b', 'd'], operate: toggleDebug, mode: 'normal' },
      ]);

      // Add without replacing
      engine.globalSequence([
      { keys: ['ctrl+k', 'ctrl+k'], operate: openQuickMenu },
      ], { mode: 'add' });
    • Parameters

      • actionId: string

      Returns boolean

      true if the shortcut action is registered.

    • Whether a composition chain is currently pending.

      Returns boolean

    • Parameters

      • sequenceActionId: string

      Returns boolean

      true if the sequence action is registered.

    • Remove a group's active focus entry from the current owner's layer.

      Kicks the entire group out of the active focus slots — the specific focusId doesn't matter. After removal the group has no active focus until activateFocusGroup, focusSet, or an auto-select re-establishes one.

      Returns false when the owner has no layer, the group is not registered, or the group is not currently active. Does not unregister the group's focus targets — bindings remain intact.

      Parameters

      Returns boolean

      true if the group was removed from active focus, false if the group was not active or could not be found.

    • De-activate a built-in processor by adding it to a disabled list. The processor is skipped on the next processKey call — its process() method returns false immediately without running any logic. Later pipeline stages receive key events as if the kicked stage did not exist.

      This does NOT remove the processor from the pipeline — it only disables its runtime behavior. The processor still appears in getProcessors. A kicked processor can be re-enabled at any time via activeProcessor.

      Use this for temporarily suppressing a pipeline stage (e.g. disable the modal barrier, mute global keys) without permanently altering the pipeline structure.

      Parameters

      Returns boolean

      true if the processor was kicked, false if it was already in the disabled list.

    • Modify the default keys of an existing shortcut action.

      Parameters

      • actionId: string
      • keys: string[]

      Returns void

      If the action does not exist or was not registered with a keys field.

    • Modify the keys (and optionally timeout) of an existing sequence action.

      Parameters

      • actionId: string
      • keys: string[]
      • Optionaltimeout: number

      Returns void

      If the action does not exist or has no preset keys/timeout.

    • Cycle to the next mode in registration order. Wraps around.

      Returns void

    • Mark keys as transparent on the current layer. When a transparent key reaches the layer (or the named focus target), the layer's own bindings are skipped and the key continues to layers below.

      Penetration means pass-through, not blocking — the key is only released, never consumed. Penetration rules are checked first during key matching (layer broadcast and screen-stack stages), so a key that is both stopped and penetrated on the same layer passes through. A wildcard "*" entry marks all keys transparent, and a when condition (callback or registered condition id) gates the rule — when it evaluates to false the penetration rule is ignored.

      Parameters

      Returns () => void

      A function that removes the transparency markers.

      If there is no current owner.

      // Make arrow keys transparent so the parent screen handles them
      engine.penetration(['up', 'down', 'left', 'right']);

      // Focus-scoped with condition
      engine.penetration(['tab'], {
      focusId: 'searchInput',
      when: () => !isEditing,
      });

      // Wildcard — all keys pass through
      engine.penetration(['*']);
    • Remove the most recent matching owner from the stack. Uses lastIndexOf so nested owners of the same layer unwind correctly.

      Parameters

      Returns void

    • Cycle to the previous mode in registration order. Wraps around.

      Returns void

    • Process a keyboard event through the full processor pipeline.

      Builds a snapshot context from the engine's current state, then runs each processor in order. The first processor that returns true (event consumed) stops the chain. The pipeline order is:

      modal → composition (affectOverlay: true) → global sequence (affectLayer: true) → global keys (affectLayer: true) → layer broadcast → composition (affectOverlay: false) → global sequence (affectLayer: false) → global keys (affectLayer: false) → screen stack.

      processKey itself only orchestrates the chain — side effects are produced by the individual processors, which may mutate layers (e.g. unbind once bindings), pending sequence state, focus targets, composition context, or press-count counters. After the chain finishes, pending sync callbacks registered via thereGlobalQueueWaiting and currentScreenHasSequenceWaiting are notified so the host framework can re-render.

      Parameters

      • input: string

        Raw character string from the host framework's keyboard event.

      • key: unknown

        Full key descriptor from the host framework (shape defined by normalizeKeyNames).

      Returns boolean

      true if any processor consumed the event, false if it fell through.

      // Engine-level: call for every key event from the host framework
      useInput((input, key) => {
      const handled = engine.processKey(input, key);
      if (!handled) {
      // Key fell through the entire pipeline — host may handle it or ignore
      }
      });
    • Process a mouse event through the mouse region hit-testing.

      move events drive hover transitions (onEnter/onLeave); click events fire onClick; wheel events fire onWheel. press/drag/ release events drive the drag lifecycle: a press inside a region arms a drag capture that the first drag event promotes (onDragStart/ onDragMove), and release fires onDragEnd — only when a real drag happened; plain clicks stay silent.

      Parameters

      • event: MouseEvent

        A mouse event from the host framework's mouse adapter.

      Returns boolean

      true if the event hit a registered region, false otherwise.

    • Push a new owner onto the owner stack so that keyboard bindings in a layer or modal element are attributed to that layer rather than the current top layer.

      The "current owner" is the top of the stack — every call to boundKeyboard, boundSequence, penetration, stop, etc. registers on the layer belonging to the current owner. Layer and modal-layer rendering code pushes the layer/modal-layer id while rendering its children and pops it afterwards.

      Parameters

      Returns void

      // When rendering a layer:
      engine.pushOwner(layerId);
      // ... bindings inside the layer element register on the layer's keyboard
      engine.popOwner(layerId);
    • Read a layer without creating it. Returns undefined when no layer exists for the given owner.

      Unlike the other binding functions (which lazily create a layer when the owner is missing), readLayer is strictly read-only.

      Parameters

      Returns ElementKeyboard | undefined

    • Read a layer without creating it. Returns undefined when no layer exists for the given owner.

      Unlike the other binding functions (which lazily create a layer when the owner is missing), readLayer is strictly read-only.

      Parameters

      • layerId: string

      Returns ElementKeyboard | undefined

    • Read a layer without creating it. Returns undefined when no layer exists for the given owner.

      Unlike the other binding functions (which lazily create a layer when the owner is missing), readLayer is strictly read-only.

      Parameters

      • layerId: string
      • elementId: string

      Returns ElementKeyboard | undefined

    • Register a mouse region for hit-testing.

      The region's layerId must match a synced layer id so hit priority follows the same modal > layer > root order as keyboard events: while any modal is open, only the topmost modal layer is hit-tested, and a miss on it is dead (no fall-through). regionId is a caller-chosen unique identifier for the region within that layer — it is independent of keyboard element ids. rect must be in 1-based terminal coordinates.

      Within a layer, later registrations win; a priority in the region entry overrides registration order (used for child controls like buttons).

      Parameters

      • entry: MouseRegionEntry

      Returns () => void

      An unregister function.

    • Register a composition key entry. See CompositionEngine#registryCompositionKey.

      Parameters

      Returns void

    • Remove a registered shortcut action.

      Parameters

      • actionId: string

      Returns void

      If not registered.

    • Remove all composition entries registered under key. See CompositionEngine#removeCompositionKey.

      Parameters

      • key: string

      Returns boolean

    • Parameters

      • target: string

      Returns boolean

      true if the condition existed and was removed.

    • Remove all mapping key entries whose head key matches firstKey. See CompositionEngine#removeMapping.

      Parameters

      • firstKey: string

      Returns boolean

      true if any entries were removed, false if the head key was not registered.

    • Remove a mapping key entry by its exact key sequence. See CompositionEngine#removeMappingKey.

      Parameters

      • keys: string[]

      Returns boolean

      true if found and removed, false otherwise.

    • Parameters

      • mode: string

      Returns boolean

      true if the mode existed and was removed.

    • Remove a processor from this instance's pipeline by its id.

      Parameters

      • processorId: string

      Returns boolean

      true if found and removed, false if not found.

    • Remove a registered sequence action.

      Parameters

      • sequenceActionId: string

      Returns void

      If not registered.

    • Restore the processor pipeline to the default 9-stage chain.

      Returns void

    • Update a condition's value. Bindings referencing this condition via when: "id" use the new value on the next key event.

      Parameters

      • target: string
      • value: boolean

      Returns boolean

      true if updated, false if the condition is not registered.

    • Switch to a specific mode. Pass null to exit all modes.

      Parameters

      • mode: string | null

      Returns boolean

      true if the switch succeeded, false if the mode is not registered.

    • Set or replace the runtime value schema for composition chain validation. See CompositionEngine#setValueSchema.

      Parameters

      • schema: ValueSchema

      Returns void

    • Prevent keys from propagating beyond the current layer. A "stop barrier" means: once a key reaches this layer, even if no binding handles it, it does not fall through to layers below. Stop rules are checked after bindings and penetrations, and a when condition (callback or registered condition id) gates the rule — when it evaluates to false the key propagates normally. A wildcard "*" entry stops all keys.

      stopAction: true treats keys as shortcut action IDs: the stop rule is stored against the action id and resolved to the action's current bound keys at match time, so re-binding the action moves the barrier automatically.

      Parameters

      Returns () => void

      A function that removes the stop barrier.

      If there is no current owner, or stopAction: true with an action id that has no bound keys.

      // Stop arrow keys — parent screens never see them
      engine.stop(['up', 'down', 'left', 'right']);

      // Stop via action ID resolution
      engine.stop(['submit', 'cancel'], { stopAction: true });

      // Focus-scoped with condition
      engine.stop(['escape'], {
      focusId: 'modal',
      when: () => hasUnsavedChanges,
      });
    • Subscribe to composition state changes. See CompositionEngine#subscribe.

      Parameters

      • fn: () => void

      Returns () => void

      An unsubscribe function.

    • Subscribe to focus changes. Returns an unsubscribe function. Use this in UI frameworks to track when the active focus target moves (e.g. Tab navigation, programmatic focusSet).

      Parameters

      • listener: () => void

      Returns () => void

    • Subscribe to mapping-key state changes. See CompositionEngine#subscribeMapping. Independent from subscribeComposition — mapping events do not fire composition subscribers and vice versa.

      Parameters

      • fn: () => void

      Returns () => void

      An unsubscribe function.

    • Push page-path, layer, and modal-layer state into the engine.

      The engine does not observe the host framework's component tree — it relies on sync being called on every render to build an accurate snapshot. Call this synchronously on every render (before any keyboard events) so that processKey reads a fresh snapshot. Cleanup methods (cleanLayers, etc.) should be called in a post-render effect so they compare the pre- and post-sync state.

      The write is a direct field assignment — no merging, no diffing, no incremental update. layers and modalLayers are expected sorted by zIndex ascending.

      Parameters

      • state: SyncState<TComponent>

        Current screen system state from the host framework.

      Returns void

      // Call synchronously on every render — before any processKey() calls
      engine.sync({
      pagePath: getCurrentPath(),
      layers: getLayers(),
      modalLayers: getModalLayers(),
      });

      // Post-render — remove keyboard data for detached pages/layers
      engine.cleanLayers();
      engine.cleanOverlayLayers();
      engine.cleanModalLayers();
    • Check whether a global multi-key sequence is currently pending (i.e. the first key was pressed and the engine is waiting for subsequent keys or a timeout).

      This is a "pull" API — it reads the pending state on demand, equivalent to getGlobalPendingSequence() !== null. Pass a sync callback to make it "push": the engine invokes the callback after every processKey invocation, letting the host framework re-render when the pending state changes.

      Parameters

      • Optionalsync: () => void

        Optional callback invoked after each processKey so the host can re-render (e.g. a useState updater).

      Returns boolean

      true if a global sequence is pending, false otherwise.

      // Polling — check on each render
      if (engine.thereGlobalQueueWaiting()) {
      // Show "g _" hint — first key of "g g" was pressed
      }

      // Reactive — force a re-render when the pending state changes
      function useGlobalPendingState() {
      const [, forceUpdate] = useState(0);
      return engine.thereGlobalQueueWaiting(() => forceUpdate(n => n + 1));
      }
    • Undo one or more completed composition sequences. See CompositionEngine#undo.

      Parameters

      • Optionalsteps: number

        Number of past sequences to undo. Defaults to 1.

      • Optionaloptions: { byKey?: boolean; isolated?: boolean }
        • OptionalbyKey?: boolean
        • Optionalisolated?: boolean

          When true, each sequence's ctx is isolated.

      Returns CompositionContext<unknown> | null

      The final context after undo, or null if nothing was undone.

      If steps exceeds the number of buffered sequences.

    • Remove a mouse region by layerId + regionId (idempotent). Needed for unmount cleanup when a region may have been re-registered multiple times via registerMouseRegion.

      Parameters

      • layerId: string
      • regionId: string

      Returns void

    • Update a composition entry identified by key + flag. See CompositionEngine#updateCompositionKey.

      Parameters

      • key: string
      • flags: Flags
      • updates: Partial<Omit<CompositioKey<TComponent>, "key" | "flags">>

      Returns boolean

    • Subscribe to unhandled key presses inside a modal. The callback receives { miss: false } when the key was handled, or { miss: true, key, input, eventNames } when nothing consumed it.

      Parameters

      Returns () => void

      An unsubscribe function.

      If not called on a modal layer.