PTY input handling gaps: cursor shape (DECSCUSR), Ctrl+V forwarding, and mouse scroll
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 58/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 静か
- 技術スタック
- typescript, wasm
- 領域
- frontend
調査の方向性
まず、名前の挙がっている3つのエントリポイント、lib/ghostty.ts、lib/input-handler.ts、src/Terminal.tsから始め、コンテキストとしてADRs 013、014、017を使用します。PTY境界で各挙動を確認してください。カーソル形状の更新、Ctrl+Vの転送、マウストラッキング中のSGRホイールイベントです。完了とは、テキストの貼り付けや通常のビューポートスクロールを退行させることなく、3つすべての修正が機能することを意味します。
索引モデルが issue の本文から書いたものです。
説明
We hit three input handling gaps while building webtty — a browser terminal that uses ghostty-web as its renderer. All three are places where ghostty-web behaves differently from xterm.js, and all three have clean fixes in the JS layer.
Bug 1: DECSCUSR cursor shape sequences are silently dropped
Symptom: Applications like vim, neovim, and fish emit DECSCUSR (ESC [ Ps SP q) to switch cursor shape at runtime (bar in insert mode, block in normal mode). With ghostty-web, the cursor shape never changes — it stays fixed at whatever was set at construction time.
Root cause: GhosttyTerminal.getCursor() in lib/ghostty.ts hardcodes style: 'block' with a TODO comment instead of reading cursor_visual_style from the WASM render state. The WASM binary processes DECSCUSR correctly and updates RenderState.Cursor.visual_style — the JS wrapper just never reads it back.
Workaround we ship in webtty: We intercept DECSCUSR sequences client-side before term.write() and set term.options.cursorStyle directly. It works, but it is the wrong layer.
Proposed fix: In getCursor(), read cursor_visual_style (data key 10) via ghostty_render_state_get and map it to the renderer style strings. The render loop already calls getCursor() after each write() and passes the result to renderer.setCursorStyle() — so this one change is sufficient.
// lib/ghostty.ts — GhosttyTerminal.getCursor()
// data key 10 = cursor_visual_style (0=block, 1=underline, 2=bar)
const visualStyle = this.exports.ghostty_render_state_get(this.handle, 10);
const style = visualStyle === 2 ? 'bar' : visualStyle === 1 ? 'underline' : 'block';
return { x: ..., y: ..., visible: ..., blinking: ..., style };
Full analysis and context: ADR 013 — Fix to ghostty-web
Bug 2: Ctrl+V keydown is not forwarded to the PTY
Symptom: Pasting non-text content (e.g. an image) into a TUI application running under ghostty-web does nothing. The same setup works correctly under ttyd (xterm.js). Specifically, opencode's image paste flow — which relies on receiving \x16 in the PTY stream and then reading the clipboard natively via osascript / powershell / wl-paste — never triggers.
Root cause: InputHandler.handleKeyDown in lib/input-handler.ts returns early on Ctrl+V / Cmd+V without emitting \x16 to onDataCallback:
if ((A.ctrlKey || A.metaKey) && A.code === "KeyV")
return; // \x16 never reaches the PTY
xterm.js forwards the keydown unconditionally; ghostty-web does not. For text paste this is invisible (the paste event fires and handlePaste covers it), but when the clipboard has no text/plain, handlePaste drops the event silently and the PTY sees nothing.
Workaround we ship in webtty: We add a capture-phase paste listener that sends \x16 when clipboardData has no text/plain. It works, but intercepting the paste event to infer a missed keydown is a hack.
Proposed fix: Emit the encoded keydown before the early return, so \x16 reaches the PTY. The paste event still fires afterwards, so text paste via handlePaste is completely unaffected.
// lib/input-handler.ts — InputHandler.handleKeyDown
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') {
const encoded = this.encoder.encode({ key: Key.V, mods: Mods.CTRL, action: KeyAction.PRESS });
if (encoded.length > 0) {
this.onDataCallback(new TextDecoder().decode(encoded)); // \x16 → PTY
}
return; // browser paste event fires next → handlePaste covers text
}
Full analysis and context: ADR 014 — Fix to ghostty-web
Bug 3: Mouse wheel sends arrow keys instead of SGR scroll sequences when mouse tracking is active
Symptom: When a TUI app (e.g. vim with set mouse=a) enables mouse tracking, scrolling the mouse wheel moves the cursor up/down instead of scrolling the buffer. The same vim in xterm.js-based terminals (VSCode, iTerm2) scrolls correctly.
Root cause: Terminal.handleWheel in src/Terminal.ts is registered on the canvas with capture: true and calls stopPropagation() unconditionally — which prevents InputHandler.handleWheel (the handler that correctly checks hasMouseTracking() and sends SGR sequences) from ever running. It then sends arrow keys regardless of whether the app has requested mouse events:
// src/Terminal.ts — Terminal.handleWheel (current, broken)
this.handleWheel = (e: WheelEvent) => {
e.preventDefault();
e.stopPropagation(); // ← blocks InputHandler
if (this.customWheelEventHandler?.(e)) return;
if (this.wasmTerm?.isAlternateScreen()) {
const dir = e.deltaY > 0 ? 'down' : 'up';
const lines = Math.min(Math.abs(Math.round(e.deltaY / 33)), 5);
for (let i = 0; i < lines; i++)
this.dataEmitter.fire(dir === 'up' ? '\x1B[A' : '\x1B[B'); // ← always, ignores mouse tracking
} else {
// scroll viewport
}
};
xterm.js checks ctx.requestedEvents.wheel before falling back to arrow keys — ghostty-web skips this check entirely.
Workaround we ship in webtty: We use attachCustomWheelEventHandler to intercept wheel events when hasMouseTracking() is true and send SGR scroll sequences (\x1b[<64;col;rowM / \x1b[<65;col;rowM) directly to the PTY, returning true to prevent the arrow-key path from running.
Proposed fix: In the isAlternateScreen() branch, guard the arrow-key loop with hasMouseTracking(). When mouse tracking is active, emit the SGR scroll sequence via dataEmitter instead. The canvas and renderer are already available on this:
// src/Terminal.ts — Terminal.handleWheel (fixed)
if (this.wasmTerm?.isAlternateScreen()) {
if (this.wasmTerm.hasMouseTracking()) {
// App negotiated mouse tracking — send SGR scroll sequence, not arrow keys.
const metrics = this.renderer?.getMetrics();
if (metrics && this.canvas) {
const rect = this.canvas.getBoundingClientRect();
const col = Math.max(1, Math.floor((e.clientX - rect.left) / metrics.width) + 1);
const row = Math.max(1, Math.floor((e.clientY - rect.top) / metrics.height) + 1);
const btn = e.deltaY < 0 ? 64 : 65;
this.dataEmitter.fire(`\x1b[<${btn};${col};${row}M`);
}
return;
}
// No mouse tracking: arrow-key fallback for apps like `less`.
const dir = e.deltaY > 0 ? 'down' : 'up';
const lines = Math.min(Math.abs(Math.round(e.deltaY / 33)), 5);
for (let i = 0; i < lines; i++)
this.dataEmitter.fire(dir === 'up' ? '\x1B[A' : '\x1B[B');
} else {
// scroll viewport (unchanged)
}
Full analysis and context: ADR 017 — Fix to ghostty-web
Contribution
All three fixes are small and self-contained. We are happy to submit PRs for any or all of them if the approach looks right to you — just say the word.
- 主要言語
- TypeScript
- スター
- 2.9k
- フォーク
- 174
- PR マージ指標
- 30日以内にマージされた PR はありません
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
coder/ghostty-web のほかの issue
-
attachCustomKeyEventHandler inverts xterm.js's return-value contract (silently swallows all input) オープン
難易度 3/5 1〜2日 初心者へのやさしさ 52/100
coder/ghostty-web#192 ·
-
A single throw inside render() permanently stops the render loop, and a consumer cannot restart it オープン
難易度 4/5 3〜5日 初心者へのやさしさ 65/100
coder/ghostty-web#189 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 70/100
coder/ghostty-web#188 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 45/100
coder/ghostty-web#187 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
coder/ghostty-web#186 · コメント 1 件 ·
coder/ghostty-web の issue をすべて見る
似ている issue
-
bug(cli): hapi doctor inline-media prints a fabricated B:\ helper-script path in packaged installs オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
-
Crush オープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
catppuccin/catppuccin#3125 ·
-
Add a SECURITY.md オープン
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
ElementsProject/cln-application#167 · コメント 1 件 · リアクション 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
Quantco/pnpm-licenses#17 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100