Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

PTY input handling gaps: cursor shape (DECSCUSR), Ctrl+V forwarding, and mouse scroll

Abierto
#145 1 comentario 0 reacciones 0 asignados Ver en GitHub

@jesse23 ya está trabajando en esto.

Desde el 28/3/2026.

  • #147 de @jesse23 — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
58/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Tranquilo
Stack tecnológico
typescript, wasm
Área
frontend

Línea de trabajo

Comienza con los tres puntos de entrada indicados: lib/ghostty.ts, lib/input-handler.ts y src/Terminal.ts, usando las ADRs 013, 014 y 017 como contexto. Confirma cada comportamiento en el límite de PTY: actualizaciones de la forma del cursor, reenvío de Ctrl+V y eventos de rueda SGR durante el seguimiento del ratón; se considera terminado cuando las tres correcciones funcionan sin introducir regresiones en el pegado de texto ni en el desplazamiento normal de la ventana gráfica.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
TypeScript
Estrellas
2.9k
Forks
182
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de coder/ghostty-web

Todos los issues de coder/ghostty-web

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.