Skip to content

fix(input): let a custom key handler pass browser chords through - #7

Merged
bilby91 merged 1 commit into
mainfrom
fix/custom-key-handler-passthrough
Aug 18, 2026
Merged

fix(input): let a custom key handler pass browser chords through#7
bilby91 merged 1 commit into
mainfrom
fix/custom-key-handler-passthrough

Conversation

@bilby91

@bilby91 bilby91 commented Aug 18, 2026

Copy link
Copy Markdown
Member

Problem

A focused terminal swallows the page's own keyboard shortcuts, and a consumer has no way to opt out.

InputHandler.handleKeyDown ends every mapped keycode with preventDefault() (and stopPropagation() on the encoder path), modifiers folded in — so Cmd+R is encoded as a SUPER-modified keystroke and the browser never reloads. Cmd+L, Cmd+1..9 and the zoom chords go the same way.

attachCustomKeyEventHandler looks like the escape hatch, but it cannot express "let the browser have this one":

  • return true → the terminal calls preventDefault() for you, so the default is blocked;
  • return false → the encoder path runs, which blocks it too.

Both branches suppress the default, so an embedding page is forced into capture-phase stopPropagation() interception on document to get its chords back — a workaround that depends on this library's internals.

Change

Drop the library-side preventDefault() on the consumed path. Returning true now means the consumer owns this event: the terminal neither encodes it nor touches the browser default.

        if (handled) {
-         // Custom handler consumed the event
-         event.preventDefault();
          return;
        }

This is the contract xterm.js documents for the same API — the handler may "stop propagation and/or prevent the default action" — so it also moves the fork closer to the xterm.js compatibility the README claims.

Returning false is unchanged.

Tests

New Custom Key Event Handler block in lib/input-handler.test.ts:

  • a consumed key (Cmd+R) is not encoded and its browser default is left alone;
  • a consumed key can still be suppressed, by the handler calling preventDefault() itself;
  • a declined key still falls through to the encoder and is suppressed.

Verified non-vacuous: with the source fix stashed, the first test fails (59 pass / 1 fail); with it, 60 pass.

Verification

Full CI gate locally: fmt clean, lint clean, typecheck clean, bun test lib/ 438 pass / 0 fail, build:lib succeeds. build:wasm was not runnable in the dev container (blocked egress to deps.files.ghostty.org); this change is TypeScript-only and CI builds the WASM itself.

Breaking change

Documented in CHANGELOG.md under [Unreleased], with migration: a handler that returned true and relied on the terminal to suppress the default must now call event.preventDefault() itself. Handlers returning false are unaffected.

Also worth flagging for anyone writing such a handler — and shown in the new README example: Cmd+C/Cmd+V are handled by the terminal after the custom handler runs, so a blanket "hand all Cmd chords to the browser" rule breaks copy and paste. Exclude KeyC/KeyV before the reserved-chord test.

Upstream

coder/ghostty-web has the same behaviour (lib/input-handler.ts still documents "Captures all keyboard input (preventDefault on everything)") and no issue tracking it. Worth offering upstream once this settles here.

🤖 Generated with Claude Code

A custom key event handler returning `true` had `preventDefault()` called
on it by the terminal, and returning `false` ran the encoder path, which
calls `preventDefault()` too. Neither outcome left the browser default
intact, so a page embedding the terminal could not keep Cmd+R (reload),
Cmd+L (address bar) or the zoom chords working while the terminal had
focus — the terminal swallowed them with no way to opt out.

Stop calling `preventDefault()` on the consumed path. Returning `true`
now means the consumer owns the event outright: the terminal neither
encodes it nor touches the default, and a handler that wants the default
suppressed calls `event.preventDefault()` itself. This matches the
contract xterm.js documents for the same API.

Handlers returning `false` are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bilby91
bilby91 merged commit 41fd7ed into main Aug 18, 2026
5 checks passed
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