Skip to content

Input Handling Limitations ​

Terminal input handling is fundamentally constrained by how terminals communicate with applications. This page documents the known limitations when using useInput() in Silvery.

Keyboard Protocol Limitations ​

Traditional terminals use a simple protocol where each keypress is sent as a character or escape sequence. This protocol predates modern keyboard conventions and has fundamental ambiguities.

Indistinguishable Keys ​

Several key combinations produce identical byte sequences:

KeysBoth SendReason
Ctrl+I / Tab0x09Tab is ASCII character 9, same as Ctrl+I
Ctrl+M / Enter0x0DCarriage return is ASCII 13, same as Ctrl+M
Ctrl+[ / Escape0x1BEscape is ASCII 27, same as Ctrl+[
Ctrl+H / Backspace0x08Backspace is ASCII 8, same as Ctrl+H
Shift+Enter / Enter\rMost terminals don't distinguish

This means your useInput handler cannot tell these apart:

tsx
useInput((input, key) => {
  // These are IDENTICAL - you cannot distinguish them
  if (key.tab) {
    // Could be Tab OR Ctrl+I
  }
  if (key.return) {
    // Could be Enter OR Ctrl+M
  }
})

Kitty Keyboard Protocol ​

The Kitty keyboard protocol solves these ambiguities by encoding modifier state explicitly. Silvery fully supports this protocol:

  • Supported terminals: Kitty, WezTerm, foot, Ghostty, Alacritty, iTerm2, rio
  • Auto-enabled by run() on supported terminals — falls back gracefully on others
  • When active, Tab vs Ctrl+I, Enter vs Ctrl+M, and all modifier combinations are fully distinguishable

See Kitty Protocol for details.

Undetectable Key Combinations ​

Some key combinations cannot be detected at all in traditional terminal mode:

  • Ctrl+Shift+<letter> - Shift state is lost
  • Ctrl+<number> - Most produce no output
  • Ctrl+, / Ctrl+. / Ctrl+; - No assigned control codes
  • Cmd/Super+<key> - Usually intercepted by the OS/window manager

CJK and IME Input ​

Input Method Editors (IMEs) for Chinese, Japanese, and Korean present challenges for terminal applications.

IME Composition Window ​

When typing with an IME, a composition window shows candidate characters. In terminal applications:

  • The composition window may flicker during rapid input
  • Positioning of the composition window varies by terminal
  • Some terminals overlay it at the cursor, others use a separate position

Synchronized Update Mode ​

Silvery uses Synchronized Update Mode (SUM) to reduce flicker:

\x1b[?2026h  // Begin synchronized update
... render output ...
\x1b[?2026l  // End synchronized update

This helps with IME flicker, but:

  • Not all terminals support SUM (macOS Terminal does not)
  • Terminal multiplexers may have partial support

Recommendations for CJK Input ​

  1. Use a terminal with good IME support (iTerm2, Kitty, WezTerm)
  2. Avoid rapid re-renders during composition
  3. Test your app with actual IME input, not just ASCII

Terminal-Specific Behavior ​

Different terminals send different escape sequences for the same keys. Silvery handles the most common variants, but edge cases exist.

Function Key Variations ​

Function keys F1-F12 have multiple encodings:

Terminal StyleF1F5
xterm (O-style)\x1bOP-
xterm ([~style)\x1b[11~\x1b[15~
Cygwin/libuv\x1b[[A\x1b[[E

Silvery recognizes all these variants, but some obscure terminals may use others.

Home/End keys also vary:

TerminalHomeEnd
xterm (standard)\x1b[H\x1b[F
xterm (alternate)\x1b[1~\x1b[4~
rxvt\x1b[7~\x1b[8~

Terminal Comparison ​

FeaturemacOS TerminaliTerm2KittyWezTerm
Synchronized UpdateNoYesYesYes
Kitty ProtocolNoNoYesYes
Function keys F1-F12YesYesYesYes
Function keys F13-F24PartialYesYesYes
Meta/Alt keyOption+EscConfigurableYesYes
IME supportBasicGoodGoodGood

Modifier Key Handling ​

Meta/Alt Key ​

The Meta (Alt on PC, Option on Mac) key behavior varies:

  • macOS Terminal: Option key types special characters by default
  • iTerm2: Configurable - can send Esc+<key> or special characters
  • Linux terminals: Usually send Esc+<key>

Silvery detects meta when it receives \x1b followed by a character:

tsx
useInput((input, key) => {
  if (key.meta && input === "a") {
    // Alt+A or Option+A (when configured to send escape)
  }
})

Ctrl+Shift Combinations ​

Most terminals cannot distinguish Ctrl+Shift+Letter from Ctrl+Letter:

tsx
useInput((input, key) => {
  // key.shift may be false even if Shift was held with Ctrl
  if (key.ctrl && input === "a") {
    // Could be Ctrl+A OR Ctrl+Shift+A
  }
})

Shift Detection for Letters ​

Shift is reliably detected for regular letter input (uppercase vs lowercase):

tsx
useInput((input, key) => {
  if (input === "A" && key.shift) {
    // Shift+A - this works reliably
  }
})

Terminal Multiplexers ​

Using tmux, screen, or similar multiplexers adds another layer:

Additional Limitations in tmux ​

  • Some escape sequences are intercepted by tmux
  • Passthrough mode may be required for certain sequences
  • Synchronized Update support varies by version
  • Function keys may need special configuration

Recommendations ​

  1. Test your app both inside and outside tmux
  2. Document any tmux-specific configuration needed
  3. Use simpler key bindings that work universally

Keys That Work Universally ​

Despite these limitations, many keys work reliably everywhere:

KeyReliability
Arrow keysExcellent
Enter/ReturnExcellent
EscapeExcellent
TabExcellent (but indistinguishable from Ctrl+I)
BackspaceExcellent
DeleteGood
Home/EndGood
Page Up/DownGood
F1-F12Good
Ctrl+A through Ctrl+ZGood (except Ctrl+I, Ctrl+M, Ctrl+[)
Shift+TabGood
Letters and numbersExcellent
Common punctuationExcellent

Best Practices ​

Design for Compatibility ​

tsx
// Good: Use keys that work everywhere
useHotkeyMap({
  ArrowUp: moveUp,
  k: moveUp,
  ArrowDown: moveDown,
  j: moveDown,
  Enter: select,
  Escape: quit,
  q: quit,
})

// Risky: Relies on Ctrl combinations that may conflict
useHotkey("ctrl+i", () => {
  // A legacy terminal reports the same bytes for Ctrl+I and Tab.
})

Provide Alternative Bindings ​

When using keys with known limitations, offer alternatives:

tsx
useHotkeyMap({
  // Multiple ways to trigger the same action.
  "?": showHelp,
  h: showHelp,
  "ctrl+h": showHelp,
})

Document Your Keybindings ​

Be explicit about which keys your app uses and any known limitations:

tsx
function HelpScreen() {
  return (
    <Box flexDirection="column">
      <Text bold>Keybindings:</Text>
      <Text>j/Down - Move down</Text>
      <Text>k/Up - Move up</Text>
      <Text>Enter - Select</Text>
      <Text>Escape - Back</Text>
      <Text dimColor>Note: Ctrl+M is the same as Enter</Text>
    </Box>
  )
}

Enhanced Protocol Support ​

Silvery ships with full support for modern terminal protocols that resolve the limitations above:

  1. Kitty keyboard protocol — Enables full modifier detection (Ctrl+Shift, Super, Hyper), key release events, and unambiguous key identification. Auto-enabled by run(). See Kitty Protocol.
  2. Bracketed paste mode — Distinguishes pasted text from typed input. Built into the runtime with usePaste() hook.
  3. Mouse input — Click, drag, and scroll events via SGR protocol (modes 1003/1006, upgraded to 1016 SGR-Pixels when run() can probe terminal cell metrics). Auto-enabled by run(). Set mouse: false to restore native copy/paste. Components receive DOM-style onClick, onWheel, etc.
  4. Focus events — Detect when the terminal gains/loses focus via the focus system and useFocusable() hook. Auto-enabled by run().

All features are auto-enabled by run() and gracefully degrade in unsupported terminals. Pass false to opt out.