@cartridge-engine/keyboard-engine
    Preparing search index...

    Class CompositionEngine<TComponent>

    State machine for multi-key composition chains (vim-style key sequences).

    Builds "flag → needs → execute" chains: pressing key "A" sets lastFlag: "A" and produces a value; the next key "B" (with needs: ["A"]) receives that value via its execute(ctx) callback, transforms it, and passes it forward. The chain continues until a key with no matching entry is pressed or the timeout expires.

    Entry resolution uses needs matching: when a pending chain exists, only keys whose needs include lastFlag are eligible; when no chain is pending, only keys with optional: true or empty needs can start one.

    Owns the key mapping table, pending chain state, mapping-key sequences, and undo buffers. Driven by the composition pipeline processors; exposed on KeyboardEngine for direct registration and inspection.

    Type Parameters

    • TComponent = unknown
    Index
    • Cancel the current pending chain immediately (no timeout).

      Clears the pending timer (no stale timeout callback will fire), resets the context to { value: undefined, lastFlag: null, steps: [] }, and sets the engine's compositionEngineHandle flag to false so pipeline processors stop treating the chain as pending. No-op when no chain is pending.

      Returns void

    • Register a mapping key entry — vim-style key mapping that maps an external key sequence (base) to an internal composition key chain (target).

      Entries are stored in this.mapping (a Map<string, Set<MappingKeyEntry>>) keyed by base[0]. When a key event matches base[0]:

      • single-key mappings (base.length === 1) execute their target chain immediately;
      • multi-key mappings create a pending entry and wait for the subsequent keys.

      Mapping-key pending and ordinary composition pending are mutually exclusive, and mapping keys take priority: they are checked inside startPending before single-key composition startup.

      Parameters

      • base: string[]

        The external trigger key sequence (what the user presses).

      • target: string[]

        The internal composition key chain to execute in order.

      • Optionaloptions: Omit<MappingKeyEntry<TComponent>, "keys" | "target">

        Optional fields forwarded to the stored MappingKeyEntry: exclusive, KeyReleaseWhenChainInterrupted, when, affectOverlay, mode, category, executeWhenNoOverlay.

      Returns boolean

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

      engine.registryCompositionKey({
      key: 't', flags: [], alternativeFlag: 'times',
      optional: true, needs: [],
      execute: (ctx) => ({ ...ctx, lastFlag: 'times', steps: [...ctx.steps, 't'] }),
      });
      engine.registryCompositionKey({
      key: 'd', flags: [], alternativeFlag: 'action',
      needs: ['times'],
      execute: (ctx) => ({ ...ctx, lastFlag: 'action', steps: [...ctx.steps, 'd'] }),
      });

      // Map 'g b' → 't d'
      engine.composition.addMapping(['g', 'b'], ['t', 'd']);

      // Single-key mapping and exclusive multi-key mapping
      engine.composition.addMapping(['q'], ['t']);
      engine.composition.addMapping(['g', 'd'], ['t'], { exclusive: true });

      // Remove a mapping
      engine.composition.removeMappingKey(['g', 'b']);
    • Whether the engine currently has an active pending chain.

      A chain is "pending" from the moment the first key starts it until it completes naturally, a key is pressed with no matching entry, or the timeout expires.

      Returns boolean

    • Register a composition key entry. Semantically equivalent duplicates (same fingerprint) are skipped.

      Each entry is a node in a composition chain: when the entry's key is pressed, the engine either continues the pending chain (if the entry's needs are satisfied by the preceding lastFlag) or starts a new chain (if the entry is a head key — optional: true or empty needs). Multiple entries can share the same key name; they are stored in a Map<string, Set<CompositioKey>> (duplicate identities are not added twice) and the best match is resolved at runtime via resolveCompositionKey.

      Parameters

      Returns void

      engine.registryCompositionKey({
      key: '3',
      alternativeFlag: 'times',
      needs: [],
      optional: true,
      execute: (ctx) => ({
      value: 3,
      lastFlag: 'times',
      steps: [...ctx.steps, '3'],
      }),
      });

      engine.registryCompositionKey({
      key: '3',
      alternativeFlag: 'action',
      needs: ['times'],
      execute: (ctx) => {
      const count = ctx.value as number;
      // Fire the compound action after the first timed press
      console.log(`Repeated ${count} times`);
      return null; // End the chain
      },
      });
    • Remove all entries registered under key — the entire set is deleted from the mapping table.

      Does NOT cancel an active pending chain: if a chain is pending when its entries are removed, it continues with its already-started context until the timeout expires. Call abort to cancel it.

      Parameters

      • key: string

      Returns boolean

      true if an entry was removed, false if none existed.

    • Remove all mapping key entries whose head key matches firstKey.

      Parameters

      • firstKey: string

      Returns boolean

      true if any entries were removed, false otherwise.

    • Remove a mapping key entry by its exact key sequence.

      Parameters

      • keys: string[]

      Returns boolean

      true if found and removed, false otherwise.

    • Set or replace the runtime value schema for composition chain validation.

      The schema replaces the previous one entirely (no merging). For each composition key event the engine validates the input value before execute runs (against the lastFlag's guard) and the output value afterwards (against the current flag's guard). Flags without a guard entry pass through silently. Validation failures clear the pending chain and emit a console.warn in development.

      Parameters

      • schema: ValueSchema

        Guard functions keyed by flag name.

      Returns void

    • Subscribe to composition state changes. The callback fires whenever the chain starts, advances, breaks, completes, or is undone. Use it to trigger a framework re-render (e.g. React useState setter).

      Parameters

      • fn: () => void

      Returns () => void

      An unsubscribe function.

    • Subscribe to mapping-key state changes. The callback fires whenever a mapping-key sequence starts, advances, breaks, is consumed (exclusive), or completes. Independent from subscribe so composition subscribers are not notified by mapping-key events.

      Parameters

      • fn: () => void

      Returns () => void

      An unsubscribe function.

    • Record the normalized key names of the current event for matching against registered composition and mapping keys.

      Parameters

      • eventName: string[]

      Returns void

    • Undo one or more completed composition sequences.

      Each completed chain is stored as a separate entry in the undo buffer. Passing steps undoes that many sequences, executing every key's CompositioKey#undoAction in reverse order.

      Parameters

      • steps: number = 1

        Number of past sequences to undo. Defaults to 1. When options.byKey is true, steps counts individual keys instead.

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

          When true, steps counts individual keys instead of whole sequences. Orthogonal to isolated. Defaults to false.

        • Optionalisolated?: boolean

          When true, each sequence's undo starts from its own saved context — ctx does NOT propagate across sequences. Defaults to false (flat propagation).

      Returns CompositionContext<unknown> | null

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

      If steps exceeds the number of buffered sequences (or keys, when byKey is true).

      engine.undo(2);
      
      engine.undo(2, { isolated: true });
      
      engine.undo(3, { byKey: true });  // undo 3 keys, not 3 sequences
      
    • Update a registered entry identified by key + flags.

      The old entry is removed and a merged entry (old fields + updates, preserving key and flags) is re-registered. Together key and flags uniquely identify the entry (compared via areFlagsEqual), so entries sharing a key name can be targeted individually instead of removing all entries for that key.

      Parameters

      Returns boolean

      true if the entry was found and updated, false if no entry matched.