Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.1 kB · 244 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245import type { Key } from '../ink.js'import { getKeyName, matchesBinding } from './match.js'import { chordToString } from './parser.js'import type { KeybindingContextName, ParsedBinding, ParsedKeystroke,} from './types.js'
export type ResolveResult = | { type: 'match'; action: string } | { type: 'none' } | { type: 'unbound' }
export type ChordResolveResult = | { type: 'match'; action: string } | { type: 'none' } | { type: 'unbound' } | { type: 'chord_started'; pending: ParsedKeystroke[] } | { type: 'chord_cancelled' }
/** * Resolve a key input to an action. * Pure function - no state, no side effects, just matching logic. * * @param input - The character input from Ink * @param key - The Key object from Ink with modifier flags * @param activeContexts - Array of currently active contexts (e.g., ['Chat', 'Global']) * @param bindings - All parsed bindings to search through * @returns The resolution result */export function resolveKey( input: string, key: Key, activeContexts: KeybindingContextName[], bindings: ParsedBinding[],): ResolveResult { // Find matching bindings (last one wins for user overrides) let match: ParsedBinding | undefined const ctxSet = new Set(activeContexts)
for (const binding of bindings) { // Phase 1: Only single-keystroke bindings if (binding.chord.length !== 1) continue if (!ctxSet.has(binding.context)) continue
if (matchesBinding(input, key, binding)) { match = binding } }
if (!match) { return { type: 'none' } }
if (match.action === null) { return { type: 'unbound' } }
return { type: 'match', action: match.action }}
/** * Get display text for an action from bindings (e.g., "ctrl+t" for "app:toggleTodos"). * Searches in reverse order so user overrides take precedence. */export function getBindingDisplayText( action: string, context: KeybindingContextName, bindings: ParsedBinding[],): string | undefined { // Find the last binding for this action in this context const binding = bindings.findLast( b => b.action === action && b.context === context, ) return binding ? chordToString(binding.chord) : undefined}
/** * Build a ParsedKeystroke from Ink's input/key. */function buildKeystroke(input: string, key: Key): ParsedKeystroke | null { const keyName = getKeyName(input, key) if (!keyName) return null
// QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts). // This is legacy terminal behavior - we should NOT record this as a modifier // for the escape key itself, otherwise chord matching will fail. const effectiveMeta = key.escape ? false : key.meta
return { key: keyName, ctrl: key.ctrl, alt: effectiveMeta, shift: key.shift, meta: effectiveMeta, super: key.super, }}
/** * Compare two ParsedKeystrokes for equality. Collapses alt/meta into * one logical modifier — legacy terminals can't distinguish them (see * match.ts modifiersMatch), so "alt+k" and "meta+k" are the same key. * Super (cmd/win) is distinct — only arrives via kitty keyboard protocol. */export function keystrokesEqual( a: ParsedKeystroke, b: ParsedKeystroke,): boolean { return ( a.key === b.key && a.ctrl === b.ctrl && a.shift === b.shift && (a.alt || a.meta) === (b.alt || b.meta) && a.super === b.super )}
/** * Check if a chord prefix matches the beginning of a binding's chord. */function chordPrefixMatches( prefix: ParsedKeystroke[], binding: ParsedBinding,): boolean { if (prefix.length >= binding.chord.length) return false for (let i = 0; i < prefix.length; i++) { const prefixKey = prefix[i] const bindingKey = binding.chord[i] if (!prefixKey || !bindingKey) return false if (!keystrokesEqual(prefixKey, bindingKey)) return false } return true}
/** * Check if a full chord matches a binding's chord. */function chordExactlyMatches( chord: ParsedKeystroke[], binding: ParsedBinding,): boolean { if (chord.length !== binding.chord.length) return false for (let i = 0; i < chord.length; i++) { const chordKey = chord[i] const bindingKey = binding.chord[i] if (!chordKey || !bindingKey) return false if (!keystrokesEqual(chordKey, bindingKey)) return false } return true}
/** * Resolve a key with chord state support. * * This function handles multi-keystroke chord bindings like "ctrl+k ctrl+s". * * @param input - The character input from Ink * @param key - The Key object from Ink with modifier flags * @param activeContexts - Array of currently active contexts * @param bindings - All parsed bindings * @param pending - Current chord state (null if not in a chord) * @returns Resolution result with chord state */export function resolveKeyWithChordState( input: string, key: Key, activeContexts: KeybindingContextName[], bindings: ParsedBinding[], pending: ParsedKeystroke[] | null,): ChordResolveResult { // Cancel chord on escape if (key.escape && pending !== null) { return { type: 'chord_cancelled' } }
// Build current keystroke const currentKeystroke = buildKeystroke(input, key) if (!currentKeystroke) { if (pending !== null) { return { type: 'chord_cancelled' } } return { type: 'none' } }
// Build the full chord sequence to test const testChord = pending ? [...pending, currentKeystroke] : [currentKeystroke]
// Filter bindings by active contexts (Set lookup: O(n) instead of O(n·m)) const ctxSet = new Set(activeContexts) const contextBindings = bindings.filter(b => ctxSet.has(b.context))
// Check if this could be a prefix for longer chords. Group by chord // string so a later null-override shadows the default it unbinds — // otherwise null-unbinding `ctrl+x ctrl+k` still makes `ctrl+x` enter // chord-wait and the single-key binding on the prefix never fires. const chordWinners = new Map<string, string | null>() for (const binding of contextBindings) { if ( binding.chord.length > testChord.length && chordPrefixMatches(testChord, binding) ) { chordWinners.set(chordToString(binding.chord), binding.action) } } let hasLongerChords = false for (const action of chordWinners.values()) { if (action !== null) { hasLongerChords = true break } }
// If this keystroke could start a longer chord, prefer that // (even if there's an exact single-key match) if (hasLongerChords) { return { type: 'chord_started', pending: testChord } }
// Check for exact matches (last one wins) let exactMatch: ParsedBinding | undefined for (const binding of contextBindings) { if (chordExactlyMatches(testChord, binding)) { exactMatch = binding } }
if (exactMatch) { if (exactMatch.action === null) { return { type: 'unbound' } } return { type: 'match', action: exactMatch.action } }
// No match and no potential longer chords if (pending !== null) { return { type: 'chord_cancelled' } }
return { type: 'none' }}