Skip to content

refactor(input): route every keydown through the Ghostty encoder#159

Open
sauyon wants to merge 1 commit into
coder:mainfrom
sauyon:sauyon/input-handler-encoder-upstream
Open

refactor(input): route every keydown through the Ghostty encoder#159
sauyon wants to merge 1 commit into
coder:mainfrom
sauyon:sauyon/input-handler-encoder-upstream

Conversation

@sauyon
Copy link
Copy Markdown

@sauyon sauyon commented Apr 21, 2026

Deletes the "printable character" and "simple special keys" fast paths
from InputHandler.handleKeyDown and routes every keydown through the
Ghostty WASM key encoder.

The old fast paths were a simplified model that diverged from both
xterm.js and Ghostty's encoder for several keys:

  • Home and End ignored DECCKM (application cursor mode). xterm.js
    emits \x1b[H in normal mode and \x1bOH in application mode; the
    fast path emitted \x1b[H always.
  • Shift+Home / Shift+End / Shift+PageUp / Shift+PageDown /
    Shift+F1..F12 dropped the Shift modifier. xterm.js encodes it into
    the CSI sequence (e.g. \x1b[1;2H for Shift+Home); the fast path
    emitted the plain unmodified sequence.
  • Non-BMP characters (surrogate-pair emoji) were dropped entirely
    because of a length === 1 filter in the fallback utf8 path.

Routing through the encoder brings ghostty-web into line with xterm.js
on these. It also picks up three behaviors that xterm.js doesn't
implement:

  • Shift+Enter distinguishable from Enter (\x1b[27;2;13~ instead of
    bare \r). This is a deliberate divergence from xterm.js, which
    emits \r for both. Modern line editors and REPLs use fixterms-style
    encoding and expect Shift+Enter to be distinguishable for
    multi-line input.
  • Kitty keyboard protocol flags affect every key when enabled.
  • xterm modifyOtherKeys state 2 affects every key when enabled.

Consumers who need byte-for-byte xterm.js behavior on specific keys
can intercept in attachCustomKeyEventHandler and emit the desired
bytes via Terminal.input(data, /wasUserInput/ true). README.md's
"Keyboard encoding" note shows the pattern.

Note: preventDefault / stopPropagation fire on any key mapKeyCode
recognizes, before the encode attempt — so a failed or empty encode
drops the keystroke silently rather than letting browser defaults
fire (F11 fullscreen, Ctrl+W close tab, etc.). Deliberate divergence
from native Ghostty's keyCallback policy of returning .ignored on
empty encode; documented in a code comment.

Deletes the "printable character" and "simple special keys" fast paths
from InputHandler.handleKeyDown and routes every keydown through the
Ghostty WASM key encoder.

The old fast paths were a simplified model that diverged from both
xterm.js and Ghostty's encoder for several keys:

- Home and End ignored DECCKM (application cursor mode). xterm.js
  emits \x1b[H in normal mode and \x1bOH in application mode; the
  fast path emitted \x1b[H always.
- Shift+Home / Shift+End / Shift+PageUp / Shift+PageDown /
  Shift+F1..F12 dropped the Shift modifier. xterm.js encodes it into
  the CSI sequence (e.g. \x1b[1;2H for Shift+Home); the fast path
  emitted the plain unmodified sequence.
- Non-BMP characters (surrogate-pair emoji) were dropped entirely
  because of a length === 1 filter in the fallback utf8 path.

Routing through the encoder brings ghostty-web into line with xterm.js
on these. It also picks up three behaviors that xterm.js doesn't
implement:

- Shift+Enter distinguishable from Enter (\x1b[27;2;13~ instead of
  bare \r). This is a deliberate divergence from xterm.js, which
  emits \r for both. Modern line editors and REPLs use fixterms-style
  encoding and expect Shift+Enter to be distinguishable for
  multi-line input.
- Kitty keyboard protocol flags affect every key when enabled.
- xterm modifyOtherKeys state 2 affects every key when enabled.

Consumers who need byte-for-byte xterm.js behavior on specific keys
can intercept in attachCustomKeyEventHandler and emit the desired
bytes via Terminal.input(data, /*wasUserInput*/ true). README.md's
"Keyboard encoding" note shows the pattern.

Note: preventDefault / stopPropagation fire on any key mapKeyCode
recognizes, before the encode attempt — so a failed or empty encode
drops the keystroke silently rather than letting browser defaults
fire (F11 fullscreen, Ctrl+W close tab, etc.). Deliberate divergence
from native Ghostty's keyCallback policy of returning .ignored on
empty encode; documented in a code comment.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
diegosouzapw added a commit to diegosouzapw/ghostty-web that referenced this pull request May 23, 2026
Deletes the "printable character" and "simple special keys" fast paths
from InputHandler.handleKeyDown and routes every keydown through the
Ghostty WASM key encoder.

The old fast paths were a simplified model that diverged from both
xterm.js and Ghostty's encoder for several keys:

- Home and End ignored DECCKM (application cursor mode). xterm.js emits
  \\x1b[H in normal mode and \\x1bOH in application mode; the fast path
  emitted \\x1b[H always.
- Shift+Home / Shift+End / Shift+PageUp / Shift+PageDown / Shift+F1..F12
  dropped the Shift modifier. xterm.js encodes it into the CSI sequence
  (e.g. \\x1b[1;2H for Shift+Home); the fast path emitted the plain
  unmodified sequence.

Going through the encoder also unlocks Kitty keyboard protocol and xterm
modifyOtherKeys state 2 when applications enable them.

Two intentional differences from xterm.js are documented in README.md:

- Shift+Enter is distinguishable from Enter (\\x1b[27;2;13~ via fixterms
  rather than bare \\r), so modern line editors and REPLs can treat
  Shift+Enter as newline-without-submit.
- Kitty keyboard protocol and xterm modifyOtherKeys state 2 are
  supported when apps enable them.

If embedders need byte-for-byte xterm.js behaviour for a specific key,
they can intercept via attachCustomKeyEventHandler and emit the bytes
they want with term.input(bytes, true).

Co-authored-by: Sauyon Lee <git@sjle.co>
Inspired-by: coder#159
diegosouzapw added a commit to diegosouzapw/ghostty-web that referenced this pull request May 23, 2026
Deletes the "printable character" and "simple special keys" fast paths
from InputHandler.handleKeyDown and routes every keydown through the
Ghostty WASM key encoder.

The old fast paths were a simplified model that diverged from both
xterm.js and Ghostty's encoder for several keys:

- Home and End ignored DECCKM (application cursor mode). xterm.js emits
  \\x1b[H in normal mode and \\x1bOH in application mode; the fast path
  emitted \\x1b[H always.
- Shift+Home / Shift+End / Shift+PageUp / Shift+PageDown / Shift+F1..F12
  dropped the Shift modifier. xterm.js encodes it into the CSI sequence
  (e.g. \\x1b[1;2H for Shift+Home); the fast path emitted the plain
  unmodified sequence.

Going through the encoder also unlocks Kitty keyboard protocol and xterm
modifyOtherKeys state 2 when applications enable them.

Two intentional differences from xterm.js are documented in README.md:

- Shift+Enter is distinguishable from Enter (\\x1b[27;2;13~ via fixterms
  rather than bare \\r), so modern line editors and REPLs can treat
  Shift+Enter as newline-without-submit.
- Kitty keyboard protocol and xterm modifyOtherKeys state 2 are
  supported when apps enable them.

If embedders need byte-for-byte xterm.js behaviour for a specific key,
they can intercept via attachCustomKeyEventHandler and emit the bytes
they want with term.input(bytes, true).


Inspired-by: coder#159

Co-authored-by: Sauyon Lee <git@sjle.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant