Skip to content

linux: wire keyboard, mouse, and focus input to libghostty#240

Merged
Jesssullivan merged 3 commits intomainfrom
sid/surface-input-events
Apr 18, 2026
Merged

linux: wire keyboard, mouse, and focus input to libghostty#240
Jesssullivan merged 3 commits intomainfrom
sid/surface-input-events

Conversation

@Jesssullivan
Copy link
Copy Markdown
Owner

Summary

  • Add GTK4 event controllers for all input types to the terminal surface
  • Keyboard: GtkEventControllerKeyghostty_surface_key with GDK keyval→text conversion and modifier translation
  • Mouse buttons: GtkGestureClick (all buttons) → ghostty_surface_mouse_button with position tracking
  • Mouse motion: GtkEventControllerMotionghostty_surface_mouse_pos
  • Scroll: GtkEventControllerScroll (both axes + discrete) → ghostty_surface_mouse_scroll
  • Focus: GtkEventControllerFocus enter/leave → ghostty_surface_set_focus
  • Shared helpers: gtkModsToGhostty (modifier translation), gtkButtonToGhostty (button number mapping)

This is the critical missing piece that makes the terminal interactive — keyboard input and mouse events now reach the terminal process. Combined with PR #237 (close/title/pwd) and PR #238 (actions/clipboard), the Linux port now has a functional input→render→clipboard pipeline.

Test plan

  • CI compilation passes across all Linux distros
  • On honey: launch cmux, verify typing produces output in terminal
  • On honey: verify mouse selection works (click + drag)
  • On honey: verify scroll works in less or man
  • On honey: verify Ctrl+C interrupts a running process

🤖 Generated with Claude Code

Add GTK4 event controllers (key, motion, click, scroll, focus) to
the terminal surface widget and forward events to libghostty via
ghostty_surface_key, ghostty_surface_mouse_button/pos/scroll, and
ghostty_surface_set_focus. Includes GDK→ghostty modifier and button
translation. This makes the terminal interactive — keyboard input
and mouse interaction now reach the terminal process.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR wires all GTK4 input (keyboard, mouse, scroll, focus) to libghostty via event controllers, and adds action handlers for close/quit/maximize/child-exit/config in the Linux port. Signal handler signatures, modifier translation, and button mapping are all correct.

Two P2 items worth tracking before the Linux port stabilises:

  • unshifted_codepoint is derived from gdk_keyval_to_lower, which returns incorrect values for shifted symbol keys (e.g. Shift+1 → ! instead of 1), breaking the kitty keyboard protocol for those keys.
  • The heap-allocated Surface struct is never freed when a terminal panel closes (g_object_set_data has no GDestroyNotify, and onCloseSurface does not call alloc.destroy).

Confidence Score: 4/5

Safe to merge for the MVP milestone; two P2 issues should be tracked before the Linux port reaches broad use.

All new signal wiring is structurally correct and the action handlers are sound. Two P2 findings exist: a correctness gap in keyboard protocol data (unshifted_codepoint) and a per-close memory leak that accumulates over a multiplexer session. Neither blocks basic operation but both affect reliability over time.

cmux-linux/src/surface.zig — unshifted_codepoint computation and missing Surface deallocation on close.

Important Files Changed

Filename Overview
cmux-linux/src/surface.zig Adds GTK4 event controllers for keyboard, mouse, scroll, and focus; unshifted_codepoint is computed with gdk_keyval_to_lower which is incorrect for shifted symbol keys, and the heap-allocated Surface struct is never freed on close.
cmux-linux/src/app.zig Adds close, quit, maximize, child-exit, reload-config, color-change, renderer-health, and set-tab-title action handlers; logic is correct but onCloseSurface does not free the Surface wrapper allocated in surface.zig.
cmux-linux/src/main.zig Wires close_surface_cb into the ghostty runtime config; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant GTK as GTK4 Event Controllers
    participant Surface as surface.zig (Surface)
    participant Ghostty as libghostty

    GTK->>Surface: key-pressed (keyval, keycode, state)
    Surface->>Surface: gtkModsToGhostty(state)
    Surface->>Surface: gdk_keyval_to_unicode(keyval) → text
    Surface->>Ghostty: ghostty_surface_key(key_event)

    GTK->>Surface: GtkGestureClick pressed (n_press, x, y)
    Surface->>Surface: gtk_widget_grab_focus()
    Surface->>Surface: gtkButtonToGhostty(button)
    Surface->>Ghostty: ghostty_surface_mouse_pos(x, y, mods)
    Surface->>Ghostty: ghostty_surface_mouse_button(PRESS, button, mods)

    GTK->>Surface: motion (x, y)
    Surface->>Ghostty: ghostty_surface_mouse_pos(x, y, mods)

    GTK->>Surface: scroll (dx, dy)
    Surface->>Ghostty: ghostty_surface_mouse_scroll(dx, dy, scroll_mods)

    GTK->>Surface: focus enter
    Surface->>Ghostty: ghostty_surface_set_focus(true)

    Ghostty->>Surface: close_surface_cb(userdata)
    Surface->>Surface: ws.removePanel() [Surface struct not freed ⚠]
Loading

Reviews (2): Last reviewed commit: "linux: add close, quit, maximize, child-..." | Re-trigger Greptile

const key_event = c.ghostty.ghostty_input_key_s{
.action = action,
.mods = mods,
.consumed_mods = c.ghostty.GHOSTTY_MODS_NONE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 consumed_mods always zero may cause double-modifier effects

Hardcoding consumed_mods = GHOSTTY_MODS_NONE means ghostty never learns that a modifier was "consumed" producing the text character. For Shift+letter ghostty would see both the shift modifier and the uppercase letter, and may emit an extra modifier keypress sequence. Ideally, when text_len > 0 and the keyval differs from its lowercase version, GHOSTTY_MODS_SHIFT should be included in consumed_mods. This matches the approach in ghostty/src/apprt/gtk/Surface.zig where consumed mods are derived from the GTK key event's consumed_modifiers.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, I think we should take note of this

Comment thread cmux-linux/src/surface.zig
Cast action param and mods return with @intcast to match
ghostty C API unsigned types. Discard gtk_widget_grab_focus
return value.
Handle 10 more libghostty actions: close_window, close_all_windows,
quit, toggle_maximize, set_tab_title, show_child_exited,
renderer_health, color_change, reload_config, config_change.

Total coverage now 23 of ~40 runtime-relevant actions.
@Jesssullivan Jesssullivan merged commit cdedb8a into main Apr 18, 2026
22 of 27 checks passed
@Jesssullivan Jesssullivan deleted the sid/surface-input-events branch April 18, 2026 23:12
Jesssullivan added a commit that referenced this pull request Apr 19, 2026
Address Greptile P2 findings from PR #240:
- Set consumed_mods to include SHIFT when text was produced from a
  shifted keyval, preventing double-modifier effects in ghostty.
- Translate GDK_MOD2_MASK (Num Lock) to GHOSTTY_MODS_NUM so
  numpad-aware programs see correct modifier state.
Jesssullivan added a commit that referenced this pull request Apr 19, 2026
…241)

* linux: implement split pane actions (new, goto, resize, equalize)

Wire 5 split management actions from libghostty:
- new_split: create terminal in focused pane via split_tree.splitPane
- goto_split: navigate between panes (next/prev/directional)
- resize_split: adjust split ratio via findResizeSplit
- equalize_splits: reset all ratios to 0.5
- toggle_split_zoom: placeholder (logs, returns true)

Add split_tree helpers: collectLeaves, adjacentLeaf, equalize,
applyRatios. Rebuild workspace widget tree after mutations via
AdwTabView page replacement with idle ratio application.

* fix: use named TraversalDirection enum for cross-file type compatibility

* fix: address Greptile P1/P2 findings on split actions

P1: fix use-after-free in applyRatiosIdle — carry workspace ID
instead of raw root pointer, re-lookup in callback.
P1: fix orphaned panel on splitPane OOM — find target node before
creating panel, clean up on failure path.
P2: toggle_split_zoom returns false (unhandled) until implemented.
P2: log OOM in collectLeaves instead of silent swallow.

* linux: fix consumed_mods for shift and add Num Lock translation

Address Greptile P2 findings from PR #240:
- Set consumed_mods to include SHIFT when text was produced from a
  shifted keyval, preventing double-modifier effects in ghostty.
- Translate GDK_MOD2_MASK (Num Lock) to GHOSTTY_MODS_NUM so
  numpad-aware programs see correct modifier state.

* fix: define GDK_MOD2_MASK locally for Num Lock translation

The GDK_MOD2_MASK constant is a C macro not always visible through
Zig's @cImport. Define it locally as 0x10 (1 << 4), the standard
value for Num Lock on Linux.

* fix: address Greptile P1 findings on widget reparenting and resize delta

- Unparent leaf widgets before buildWidget so GTK4's parent assertion
  is satisfied (close old tab page first, then detach leaves from
  old GtkPaneds before building the new tree).
- Use actual pane size instead of hardcoded 1000px divisor for
  resize_split delta-to-ratio conversion.

* linux: update split tree on surface close, add move_tab action

- onCloseSurface now updates the split tree when a pane is closed
  (shell exit, close-surface callback), promoting the sibling node
  and rebuilding the widget tree.
- Add MOVE_TAB action handler for tab reordering via keybinds.

* fix: grab GTK focus on new pane after split

Greptile P1: after new_split, the data model set focused_panel_id
to the new panel but GTK keyboard focus remained on the old pane.
Add gtk_widget_grab_focus on the new widget after rebuild.
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