Input, Keys, and Autocomplete
Keyboard handling is split into three pure modules under
src/ui: key decoding (keys.ts), action mapping (keybindings.ts), and completion (autocomplete.ts). All three are re-exported fromindusagi/tui(src/ui/index.ts). React dialogs read keys through Ink'suseInput; the editor reads them through these modules.
Two input paths
- Dialogs (
indusagi/react-ink) handle keys with Ink'suseInput((input, key) => …)fromindusagi/react-host/ink. They branch onkey.upArrow,key.downArrow,key.return,key.escape,key.backspace/key.delete, andkey.ctrl && input === "u". - The editor / toolkit decodes raw stdin with
parseKey/matchesKeyand maps it to a namedEditorActionthroughEditorKeybindingsManager.
Key Decoding
File: src/ui/keys.ts (exported via indusagi/tui).
Public surface:
parseKey(data: string): string | undefined— decode raw input into its key id.matchesKey(data: string, keyId: KeyId): boolean— test whether raw input is a key.Key— a builder yielding strongly-typedKeyIds.KeyId— the union of recognized key identifiers;KeyEventTypeis"press" | "repeat" | "release".isKeyRepeat(data)/isKeyRelease(data)— classify Kitty-protocol event types.setKittyProtocolActive(active)/isKittyProtocolActive()— the shared Kitty-keyboard-protocol flag the terminal layer flips once the protocol is confirmed.
The module decodes both traditional escape sequences and the Kitty keyboard protocol, including punctuation and ctrl/shift combinations. A handful of ctrl+symbol presses collide with ASCII control codes (e.g. ctrl+[ is indistinguishable from ESC), which is inherent to legacy encoding.
import { Key, matchesKey, parseKey } from "indusagi/tui";
if (matchesKey(data, Key.ctrl("c"))) abort();
const id = parseKey(data); // e.g. "up", "ctrl+u", or undefined
Keybindings
File: src/ui/keybindings.ts (exported via indusagi/tui).
EditorAction— the union of editor actions: caret moves (cursorUp,cursorWordLeft,cursorLineStart,pageUp, …), deletions (deleteCharBackward,deleteWordForward,deleteToLineEnd, …), text entry (newLine,submit,tab), completion-popup driving (selectUp,selectDown,selectConfirm,selectCancel, …),copy, the emacs kill ring (yank,yankPop),undo,expandTools, and more.DEFAULT_EDITOR_KEYBINDINGS— the defaultEditorAction → KeyId[]mapping.EditorKeybindingsConfig— a partial override map.EditorKeybindingsManager— resolves keys to actions:matches(data, action, componentId?)— does rawdatatriggeraction?getKeys(action)— everyKeyIdwired to an action.registerAction(action, keys),getRegisteredActions().setConfig(config, preset?)— swap in a fresh config.setComponentOverride(componentId, config)/clearComponentOverride(componentId)— per-component (per keybinding-scope) overrides.detectConflicts()— list binding conflicts.
getEditorKeybindings()/setEditorKeybindings(manager)— the process-wide singleton manager.
import { getEditorKeybindings } from "indusagi/tui";
const keys = getEditorKeybindings();
if (keys.matches(data, "submit")) commit();
A vim preset and KeybindingPreset type exist inside the module, but the public
indusagi/tui barrel re-exports only the default-mapping symbols above.
Editor Component Contract
File: src/ui/editor-component.ts (exported via indusagi/tui).
EditorComponent composes a Component with four concern-split interfaces so a
custom editor (vim/emacs mode, custom keybindings) stays compatible:
EditorReadAccess—getText(), optionalgetExpandedText().EditorWriteAccess—setText(text), optionalinsertTextAtCursor(text),addToHistory(text).EditorDisplayConfig— optionalborderColor(str),setPaddingX(n),setAutocompleteProvider(provider),getKeybindingScopeId().EditorEventCallbacks— optionalonSubmit(text),onChange(text).
Autocomplete
File: src/ui/autocomplete.ts (exported via indusagi/tui).
AutocompleteItem—{ value, label, description? }.SlashCommand—{ name, description?, getArgumentCompletions?(prefix) }.AutocompleteProvider—getSuggestions(lines, cursorLine, cursorCol)returning{ items, prefix } | null, andapplyCompletion(lines, cursorLine, cursorCol, item, prefix).CombinedAutocompleteProvider— the default provider.new CombinedAutocompleteProvider(commands?, basePath?, fdPath?). ItsgetSuggestionstries, in order: a@-prefix fuzzy file attachment search, then slash-command name/argument completion, then plain path completion.
@-prefix fuzzy file search and plain path completion both traverse the tree. When
an fdPath is provided, the provider shells out to the external fd binary
(--type f --type d --full-path, gitignore-aware) via spawnSync; otherwise it
falls back to a readdirSync/statSync walk. Completions are cached per query.
import { CombinedAutocompleteProvider } from "indusagi/tui";
const provider = new CombinedAutocompleteProvider(
[{ name: "model", description: "Pick a model" }],
process.cwd(),
"/usr/bin/fd",
);
const result = provider.getSuggestions(["@src/"], 0, 5);
Fuzzy Matching
File: src/ui/fuzzy.ts (exported via indusagi/tui).
fuzzyMatch(query, text): FuzzyMatch— returns{ matches, score }.fuzzyFilter(items, query, getText)— filters and ranks items by score.
(The dialogs use a simpler matchesSearchQuery from
src/react-ink/utils/selection-dialog.ts for their substring filtering, distinct
from fuzzyMatch.)
Text Width Helpers
File: src/ui/utils.ts (exported via indusagi/tui).
visibleWidth(str)— ANSI-aware terminal column width.truncateToWidth(...)— clip a string to a column budget.wrapTextWithAnsi(text, width)— wrap to a width while preserving ANSI runs.
