터미널 바이트부터 타입된 액션까지 — 키 입력을 명령으로 변환, 코드 시퀀스, 사용자 오버라이드, 예약된 단축키 보호.
터미널에서 사용자가 키를 누르면 바이트 시퀀스가 생성됩니다. 이 바이트를 의미 있는 액션으로 변환하는 것이 키바인딩 시스템의 역할입니다. 단순한 매핑 테이블이 아니라, 터미널 프로토콜 해석, 코드 시퀀스 해석, 사용자 오버라이드, 보호 키 관리를 포함하는 5단계 파이프라인입니다.
keybindings/decoder.ts → keybindings/config.ts → keybindings/matcher.ts → keybindings/chords.ts → keybindings/dispatch.ts
5단계 파이프라인:
터미널 에뮬레이터마다 키 입력을 바이트로 인코딩하는 프로토콜이 다릅니다. 키바인딩 시스템은 3가지 프로토콜을 지원합니다.
가장 오래된 프로토콜로, 대부분의 터미널이 지원합니다. 수정자 키(Ctrl, Alt)가 바이트 값에 직접 인코딩되어 정보가 제한적입니다. 예를 들어 Ctrl+C는 0x03(ETX)이지만, Ctrl+Shift+C를 구분할 수 없습니다.
// Legacy VT: Ctrl+키는 0x01-0x1A로 인코딩
function decodeLegacyVT(byte: number): KeyEvent | null {
if (byte >= 0x01 && byte <= 0x1A) {
return {
key: String.fromCharCode(byte + 0x60), // 'a'-'z'
ctrl: true,
shift: false, // 구분 불가
alt: false
}
}
return null
}
모던 터미널(kitty, ghostty, WezTerm 등)이 지원하는 확장 프로토콜입니다. CSI codepoint ; modifiers u 형식으로 키를 정확하게 인코딩하여 Ctrl+Shift+키 같은 조합도 구분할 수 있습니다.
// CSI u: \x1b[codepoint;modifiers u
function decodeCsiU(seq: string): KeyEvent | null {
const match = seq.match(/\x1b\[(\d+);(\d+)u/)
if (!match) return null
const codepoint = parseInt(match[1])
const modifiers = parseInt(match[2]) - 1
return {
key: String.fromCodePoint(codepoint),
shift: !!(modifiers & 1),
alt: !!(modifiers & 2),
ctrl: !!(modifiers & 4),
}
}
xterm 호환 터미널에서 사용하는 중간 단계 프로토콜입니다. 기능 키(F1-F12), 화살표 키 등을 CSI 시퀀스로 인코딩합니다.
같은 키가 다른 상황에서 다른 동작을 해야 합니다. 예를 들어 Escape는 입력 중에는 Vim NORMAL 모드 전환, 대화상자에서는 닫기, 자동완성 중에는 목록 숨기기를 수행합니다. 이를 위해 18개의 키바인딩 컨텍스트가 정의되어 있습니다.
// 키바인딩 컨텍스트 — 상황에 따라 다른 동작
type KeybindingContext =
| 'global' // 어디서든 동작
| 'input' // 텍스트 입력 중
| 'input.vim.normal' // Vim NORMAL 모드
| 'input.vim.insert' // Vim INSERT 모드
| 'dialog' // 대화상자 열림
| 'autocomplete' // 자동완성 목록 표시 중
| 'permission' // 권한 요청 대화상자
| 'select' // 선택 메뉴
| 'streaming' // AI 응답 스트리밍 중
| 'idle' // 입력 대기
// ... 8개 추가 컨텍스트
키 입력이 들어오면 현재 활성화된 컨텍스트 스택의 위에서부터 매칭을 시도합니다. 가장 구체적인 컨텍스트의 바인딩이 우선합니다.
코드 시퀀스(chord sequence)는 두 개 이상의 키를 순서대로 눌러 하나의 명령을 실행하는 방식입니다. 예를 들어 Ctrl+K, Ctrl+C는 "주석 토글"을 의미할 수 있습니다.
// 코드 시퀀스 해석 — 5가지 결과 타입
type ChordResult =
| { type: 'match'; action: Action } // 완전 매칭 → 액션 실행
| { type: 'partial' } // 부분 매칭 → 다음 키 대기
| { type: 'noMatch' } // 매칭 없음 → 키 통과
| { type: 'ambiguous'; candidates: Binding[] } // 모호 → 타임아웃 대기
| { type: 'timeout' } // 타임아웃 → 부분 매칭 취소
코드 시퀀스 해석의 핵심 과제는 ambiguous(모호) 상태입니다. 예를 들어 Ctrl+K가 단독 바인딩이면서 동시에 Ctrl+K, Ctrl+C 코드의 시작이라면, 시스템은 짧은 타임아웃 동안 다음 키를 기다립니다. 타임아웃 내에 다음 키가 오면 코드로, 오지 않으면 단독 바인딩으로 처리합니다.
~/.claude/keybindings.json 핫 리로드사용자는 ~/.claude/keybindings.json 파일에서 키바인딩을 커스터마이징할 수 있습니다. 이 파일은 핫 리로드를 지원하여 Claude Code를 재시작하지 않고도 변경 사항이 즉시 적용됩니다.
// ~/.claude/keybindings.json 예시
{
"bindings": [
{
"keys": ["ctrl+shift+p"],
"action": "command-palette",
"context": "global"
},
{
"keys": ["ctrl+k", "ctrl+c"],
"action": "toggle-comment",
"context": "input"
}
]
}
일부 키바인딩은 사용자가 오버라이드할 수 없도록 보호됩니다. 오버라이드를 허용하면 사용자가 도구를 사용할 수 없게 되거나 안전 문제가 발생할 수 있기 때문입니다.
Ctrl+C(인터럽트), Ctrl+D(EOF) — 프로세스 제어에 필수적인 키Ctrl+Z(서스펜드), Ctrl+\(SIGQUIT) — 터미널 에뮬레이터가 인터셉트하는 키사용자가 보호된 키를 오버라이드하려고 하면 keybindings.json은 조용히 무시됩니다. 에러 메시지를 표시하지 않으므로, 커스텀 바인딩이 동작하지 않을 때 보호 키 목록을 먼저 확인해야 합니다.
~/.claude/keybindings.json은 핫 리로드를 지원하여 재시작 없이 키바인딩을 변경할 수 있습니다Q1. CSI u 프로토콜이 Legacy VT보다 나은 점은 무엇인가요?
\x1b[codepoint;modifiers u 형식으로 키와 수정자를 별도로 인코딩하여 모든 조합을 정확히 표현합니다.Q2. 코드 시퀀스에서 ambiguous 결과가 발생하는 상황은?
ambiguous는 입력된 키가 단독 바인딩이면서 동시에 더 긴 코드 시퀀스의 접두사일 때 발생합니다. 시스템은 짧은 타임아웃 동안 다음 키를 기다려 코드인지 단독 키인지 결정합니다.Q3. 18개 키바인딩 컨텍스트가 스택으로 관리되는 이유는?
Q4. Ctrl+C가 보호 키로 지정된 이유는?
Ctrl+C는 실행 중인 프로세스에 SIGINT를 보내는 시스템 필수 키입니다. 이를 오버라이드하면 사용자가 무한 루프에 빠진 프로세스를 중단할 수 없게 되어 위험합니다. 따라서 보호 키로 지정되어 사용자 오버라이드가 불가능합니다.Q5. keybindings.json의 핫 리로드는 어떻게 동작하나요?
keybindings.json에 대한 파일 시스템 감시자(watcher)가 설정되어 있어, 파일이 수정되면 즉시 감지하고 바인딩 설정을 다시 로드합니다. 사용자는 파일을 저장하기만 하면 변경 사항이 즉시 적용됩니다.