Kitty Keyboard Protocol Support
Silvery fully supports the Kitty keyboard protocol for unambiguous key identification, modifier detection, and key event types.
What is the Kitty Keyboard Protocol?
The Kitty keyboard protocol is a modern terminal keyboard protocol that solves fundamental limitations of traditional terminal input handling. It was created by Kovid Goyal for the Kitty terminal emulator and has since been adopted by many other terminals.
The Problem with Traditional Terminal Input
Traditional terminals encode keypresses using ASCII control codes and escape sequences. This creates ambiguities:
| Keys | Both Send | Why? |
|---|---|---|
Ctrl+I / Tab | 0x09 | Tab is ASCII character 9, same as Ctrl+I |
Ctrl+M / Enter | 0x0D | Carriage return is ASCII 13, same as Ctrl+M |
Ctrl+[ / Escape | 0x1B | Escape is ASCII 27, same as Ctrl+[ |
Ctrl+H / Backspace | 0x08 | Backspace is ASCII 8, same as Ctrl+H |
Additionally, many key combinations are simply undetectable:
Ctrl+Shift+<letter>- Shift state is lostCtrl+<number>- Most produce no output- Key release events - Not reported at all
Super/Hypermodifiers - Not transmitted
Progressive Enhancement Mode
The Kitty protocol uses progressive enhancement - applications opt-in to enhanced features using a flags bitmask. This allows:
- Applications to request only the features they need
- Graceful fallback when features aren't supported
- Backward compatibility with legacy applications
Enhancement flags (binary bitmask):
| Bit | Value | Feature |
|---|---|---|
| 0b1 | 1 | Disambiguate escape codes - All keys use unambiguous CSI u format |
| 0b10 | 2 | Report event types - Distinguish press, repeat, and release |
| 0b100 | 4 | Report alternate keys - Include shifted/alternate key variants |
| 0b1000 | 8 | Report all keys as escape codes - Even plain letters |
| 0b10000 | 16 | Report associated text - Include Unicode text for the key |
For Silvery, flags 1 (disambiguate) and 2 (event types) are the most valuable.
Key Encoding Format
Keys are encoded as:
CSI unicode-key-code : alternates ; modifiers : event-type ; text uOnly unicode-key-code is mandatory. For example:
akey:CSI 97 u(97 = Unicode for 'a')Ctrl+a:CSI 97 ; 5 u(5 = 1 + ctrl modifier)Tab:CSI 9 u(9 = tab key code)Ctrl+i:CSI 105 ; 5 u(105 = 'i', with ctrl - DISTINGUISHABLE from Tab!)
Modifier Encoding
Modifiers use a bitmask with an offset of +1:
Value = 1 + modifiers
where modifiers bits are:
shift = 0b1 (1)
alt = 0b10 (2)
ctrl = 0b100 (4)
super = 0b1000 (8)
hyper = 0b10000 (16)
meta = 0b100000 (32)
caps_lock = 0b1000000 (64)
num_lock = 0b10000000 (128)For example, Ctrl+Shift = 1 + 1 + 4 = 6.
Event Types
When flag 2 is enabled, event types are reported:
| Type | Code | Description |
|---|---|---|
| Press | 1 | Key pressed (default, often omitted) |
| Repeat | 2 | Key held down, repeating |
| Release | 3 | Key released |
Example: CSI 97 ; 1 : 3 u = 'a' key released.
Terminal Support
Terminals with Full Support
| Terminal | Platform | Notes |
|---|---|---|
| Kitty | Linux, macOS | The reference implementation |
| WezTerm | Linux, macOS, Windows | Full support |
| foot | Linux (Wayland) | Full support |
| Ghostty | macOS, Linux | Full support |
| Alacritty | Cross-platform | Full support (added 2024) |
| iTerm2 | macOS | Full support |
| rio | Cross-platform | Full support |
Terminals Without Support
| Terminal | Platform | Notes |
|---|---|---|
| macOS Terminal.app | macOS | No plans for support |
| GNOME Terminal | Linux | Uses VTE, no support yet |
| Konsole | Linux | No support |
| Windows Terminal | Windows | No support (may add in future) |
Terminal Multiplexers
| Multiplexer | Support | Notes |
|---|---|---|
| tmux | Partial | Must enable passthrough mode |
| screen | No | Legacy protocol only |
| Zellij | Yes | Full passthrough support |
Applications Using the Protocol
Major applications that have adopted the protocol:
- Editors: Neovim, Vim, Helix, Kakoune, dte
- Shells: fish, nushell
- File managers: Yazi, far2l
- Libraries: notcurses, crossterm, textual, bubbletea, vaxis
Detecting Terminal Support
Query-based Detection
Send the query sequence and check for a response:
// Query current keyboard mode
stdout.write("\x1b[?u")
// Terminal will respond with:
// CSI ? flags u (if supported)
// Nothing (if not supported, will show garbage or be ignored)Robust Detection Pattern
async function detectKittyProtocol(stdin, stdout): Promise<boolean> {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
cleanup()
resolve(false)
}, 100)
function onData(data: Buffer) {
const str = data.toString()
// Look for CSI ? <number> u response
if (/\x1b\[\?\d+u/.test(str)) {
cleanup()
resolve(true)
}
}
function cleanup() {
clearTimeout(timeout)
stdin.removeListener("data", onData)
}
stdin.on("data", onData)
// Query current mode, then query device attributes (fallback)
stdout.write("\x1b[?u\x1b[c")
})
}Using Primary Device Attributes as Fallback
If the terminal doesn't respond to CSI ? u, it will respond to CSI c (device attributes). By sending both, you can detect support with a timeout:
- Send
\x1b[?u\x1b[c - If you get
CSI ? <n> ubeforeCSI ? <attrs> c, protocol is supported - If you only get device attributes, protocol is not supported
Usage in Silvery
Enabling the Protocol
run() auto-detects Kitty protocol support and enables it by default on supported terminals (Ghostty, Kitty, WezTerm, foot):
import { run } from "@silvery/term/runtime"
// Auto-enabled — ⌘ and ✦ modifiers just work
await run(<App />)
// Opt out if needed
await run(<App />, { kitty: false })
// Specific flags for advanced features:
import { KittyFlags } from "silvery"
await run(<App />, { kitty: KittyFlags.DISAMBIGUATE | KittyFlags.REPORT_EVENTS })Silvery handles the full lifecycle: detect support, enable on startup, disable on exit (including crash/SIGINT).
Enhanced Key Fields
When the protocol is active, the Key object includes additional fields:
| Field | Type | Description |
|---|---|---|
super | boolean | Cmd/Super modifier (Kitty bit 3) |
hyper | boolean | Hyper modifier (Kitty bit 4) |
eventType | 1 | 2 | 3 | Press (1), repeat (2), release (3). Requires REPORT_EVENTS flag. |
capsLock | boolean | CapsLock is active |
numLock | boolean | NumLock is active |
shiftedKey | string | Character produced when Shift is held |
baseLayoutKey | string | Key on standard US layout (for non-Latin keyboards) |
associatedText | string | Decoded text from REPORT_TEXT mode |
Protocol Control Functions
import { enableKittyKeyboard, disableKittyKeyboard, queryKittyKeyboard, KittyFlags } from "silvery"
enableKittyKeyboard(KittyFlags.DISAMBIGUATE) // CSI > flags u
disableKittyKeyboard() // CSI < u (pop stack)
queryKittyKeyboard() // CSI ? u (detect support)Detection
import { detectKittySupport, detectKittyFromStdio } from "silvery"
// Low-level: send query, parse response
const supported = await detectKittySupport(write, read, timeout)
// Convenience: detect using real stdio
const supported = await detectKittyFromStdio(stdout, stdin, timeout)API Design Examples
Basic Usage (Auto-detection)
import { render, useInput } from "silvery"
function App() {
useInput((input, key) => {
// With Kitty protocol, these are now distinguishable!
if (key.tab && !key.ctrl) {
// User pressed Tab
}
if (key.ctrl && input === "i") {
// User pressed Ctrl+I (NOT Tab!)
}
})
return <Text>Tab and Ctrl+I are now different!</Text>
}
// Enable Kitty protocol (falls back gracefully)
using term = createTerm()
await render(<App />, { kittyKeyboard: true })Key Release Events
function Game() {
const [isJumping, setIsJumping] = useState(false)
useInput((input, key) => {
if (input === " ") {
if (key.eventType === "press") {
setIsJumping(true)
} else if (key.eventType === "release") {
setIsJumping(false)
}
}
})
return <Text>{isJumping ? "Jumping!" : "On ground"}</Text>
}
using term = createTerm()
await render(<Game />, {
kittyKeyboard: { reportRelease: true },
})Checking Protocol Support
function App() {
const rt = useRuntime()
// Kitty support is auto-detected at startup — run() enables it by default
// on supported terminals (Ghostty, Kitty, WezTerm, foot)
return (
<Box flexDirection="column">
<Text>Kitty protocol: check terminal capabilities</Text>
<Text dimColor>Tip: Use Kitty, WezTerm, or iTerm2 for enhanced keyboard support</Text>
</Box>
)
}Backward Compatibility
Graceful Degradation
When Kitty protocol is requested but not supported:
- Detection returns false
kittyProtocolEnabledcontext value is false- Input parsing uses legacy escape sequences
- All existing code continues to work
API Stability
The existing Key interface properties remain unchanged. New properties are optional and only populated when the protocol is active:
// Existing code continues to work
useInput((input, key) => {
if (key.tab) {
// Still works - might be Tab or Ctrl+I
}
})
// Enhanced code can check for disambiguation
useInput((input, key) => {
if (key.kittyProtocol) {
// Can trust that Tab and Ctrl+I are distinct
}
})Before/After Comparison
Before (Legacy Protocol)
useInput((input, key) => {
// PROBLEM: Cannot distinguish these
if (key.tab) {
// Could be Tab OR Ctrl+I - no way to know
handleIndent() // User wanted Ctrl+I for something else!
}
// PROBLEM: Cannot detect key release
if (input === "w") {
moveForward() // Keeps triggering on repeat
// No way to know when user lifts finger
}
// PROBLEM: No Super/Hyper modifiers
if (key.meta && input === "s") {
// This is Alt+S, but user pressed Cmd+S
// Cmd is intercepted by terminal
}
})After (Kitty Protocol)
useInput((input, key) => {
// SOLVED: Tab and Ctrl+I are distinct
if (key.tab && !key.ctrl) {
handleIndent()
}
if (key.ctrl && input === "i") {
showInfo() // Separate action!
}
// SOLVED: Key release detection
if (input === "w") {
if (key.eventType === "press") {
startMovingForward()
} else if (key.eventType === "release") {
stopMovingForward()
}
}
// SOLVED: Super modifier available (if terminal supports)
if (key.super && input === "s") {
save() // Actually Cmd+S on macOS
}
})Testing
Use kittyMode: true on createRenderer to route press() through Kitty encoding, and keyToKittyAnsi() to generate raw sequences:
import { createRenderer, keyToKittyAnsi } from "@silvery/test"
const render = createRenderer({ cols: 80, rows: 24, kittyMode: true })
test("Super+j triggers action", async () => {
const app = render(<App />)
await app.press("Super+j")
expect(app.text).toContain("action triggered")
})
// Generate raw sequences for direct stdin writing
keyToKittyAnsi("Super+j") // '\x1b[106;9u'
keyToKittyAnsi("Meta+Enter") // '\x1b[13;3u'