Skip to content

linux: handle 11 more libghostty actions#238

Merged
Jesssullivan merged 3 commits intomainfrom
sid/action-handlers-batch2
Apr 18, 2026
Merged

linux: handle 11 more libghostty actions#238
Jesssullivan merged 3 commits intomainfrom
sid/action-handlers-batch2

Conversation

@Jesssullivan
Copy link
Copy Markdown
Owner

@Jesssullivan Jesssullivan commented Apr 18, 2026

Summary

Commit 1: Action handlers (11 new)

  • Tab management: NEW_TAB, NEW_WINDOW (→ new workspace), GOTO_TAB (previous/next/last/direct), CLOSE_TAB
  • Desktop integration: DESKTOP_NOTIFICATION (→ GNotification), OPEN_URL (→ xdg-open via GLib), TOGGLE_FULLSCREEN
  • Terminal UX: RING_BELL (visual flash counter + GDK beep fallback), RENDER (queue GL redraw)
  • Cursor: MOUSE_SHAPE (28 CSS cursor names), MOUSE_VISIBILITY (hide/show)

Commit 2: Clipboard support

  • onReadClipboard: async GDK clipboard read → ghostty_surface_complete_clipboard_request
  • onWriteClipboard: set GDK clipboard text from terminal selection/OSC 52
  • onConfirmReadClipboard: auto-confirm (dialog can be added later)
  • Supports both standard and primary (selection) clipboards

Brings handled action count from 2 → 13 and clipboard from stub → functional.
Builds on PR #237 which added SET_TITLE, PWD, and onCloseSurface.

Test plan

  • CI compilation passes across all Linux distros (Arch, Debian, Ubuntu, Fedora, Rocky)
  • Verify GOTO_TAB wraps correctly at boundaries (previous on first → last, next on last → first)
  • Verify CLOSE_TAB prevents closing the last workspace
  • Verify Ctrl+Shift+C / Ctrl+Shift+V work for copy/paste
  • Verify notification forwarding via printf '\e]9;test notification\e\\'

🤖 Generated with Claude Code

…reen, cursor)

Wire NEW_TAB, NEW_WINDOW, GOTO_TAB, CLOSE_TAB, DESKTOP_NOTIFICATION,
OPEN_URL, TOGGLE_FULLSCREEN, RING_BELL, RENDER, MOUSE_SHAPE, and
MOUSE_VISIBILITY to the existing tab manager, notification system,
and GTK window APIs. Brings handled action count from 2 to 13.
Wire onReadClipboard, onConfirmReadClipboard, and onWriteClipboard
to GTK4's async clipboard API. Read uses gdk_clipboard_read_text_async
with a completion callback that feeds text back to libghostty via
ghostty_surface_complete_clipboard_request. Write sets clipboard text
via gdk_clipboard_set_text. Supports both standard and selection
(primary) clipboards.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR adds 11 libghostty action handlers (tab navigation, notifications, fullscreen, cursor shape/visibility, bell, render) and functional clipboard read/write via GTK4's GdkClipboard. Two previously flagged issues remain open (silent URL-launch failure, bell swallowed in fallback path), and the new clipboard path introduces a use-after-free risk when a surface is closed during a pending async clipboard read.

Confidence Score: 3/5

Mergeable for functionality, but the async clipboard UAF and two previously flagged silent-failure paths should be addressed before shipping to users.

Two prior P1 findings (silent bell, URL error discarding) remain open, and the new clipboard path introduces a use-after-free if a surface is destroyed while a paste is in flight. The UAF is low-probability but carries crash risk on the main path (Ctrl+Shift+V then closing a tab).

cmux-linux/src/app.zig — specifically the onReadClipboard/onClipboardReadComplete pair and the handleBell fallback returns.

Important Files Changed

Filename Overview
cmux-linux/src/app.zig Adds 11 action handlers (tabs, notifications, fullscreen, cursor) and clipboard read/write. Two previously flagged issues remain open (bell and URL error suppression), plus a new UAF risk in the async clipboard path and a MIME type gap in onWriteClipboard.

Sequence Diagram

sequenceDiagram
    participant G as libghostty
    participant A as app.zig onAction
    participant TM as TabManager
    participant GTK as GTK4/GDK

    G->>A: onAction(NEW_TAB / NEW_WINDOW)
    A->>TM: createWorkspace()
    TM-->>A: ok
    A->>GTK: sidebar.refresh()

    G->>A: onAction(GOTO_TAB prev/next/idx)
    A->>TM: selectWorkspace(idx)

    G->>A: onAction(CLOSE_TAB)
    A->>TM: closeWorkspace(idx) [guards last workspace]

    G->>A: onAction(DESKTOP_NOTIFICATION)
    A->>GTK: sendNotification (GNotification)

    G->>A: onAction(RING_BELL)
    A->>TM: panel.flash_count +|= 1
    note over A,GTK: fallback: gdk_surface_beep

    G->>A: onAction(MOUSE_SHAPE / MOUSE_VISIBILITY)
    A->>GTK: gdk_cursor_new_from_name + gtk_widget_set_cursor

    G->>A: onReadClipboard(surface, type, ctx)
    A->>GTK: gdk_clipboard_read_text_async(cancellable=null)
    note over A: No cancellable — UAF if surface closed before callback
    GTK-->>A: onClipboardReadComplete(source, result, ctx)
    A->>G: ghostty_surface_complete_clipboard_request

    G->>A: onWriteClipboard(type, contents, count)
    A->>GTK: gdk_clipboard_set_text(contents[0].data)
    note over A: comment says text/plain but no MIME filter
Loading

Reviews (2): Last reviewed commit: "fix: use @intCast for C enum types in ac..." | Re-trigger Greptile

Comment thread cmux-linux/src/app.zig
Comment on lines +157 to +158
_ = c.gtk.g_app_info_launch_default_for_uri(url_z.ptr, null, null);
return true;
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 Silent URL launch failure

g_app_info_launch_default_for_uri returns a gboolean indicating success, and accepts a GError** parameter (currently null) that captures the failure reason. Discarding both means the function always returns true even when the system has no handler or the URI is malformed — the user gets no feedback and ghostty is told the action was handled.

Suggested change
_ = c.gtk.g_app_info_launch_default_for_uri(url_z.ptr, null, null);
return true;
var g_error: ?*c.gtk.GError = null;
const launched = c.gtk.g_app_info_launch_default_for_uri(url_z.ptr, null, &g_error);
if (g_error) |e| {
log.warn("Failed to open URL: {s}", .{e.message});
c.gtk.g_error_free(e);
}
return launched != 0;

Comment thread cmux-linux/src/app.zig
Comment on lines +200 to +205
// Fall back to system bell
const gtk_app = c.gtk.g_application_get_default() orelse return true;
const win = c.gtk.gtk_application_get_active_window(@ptrCast(@alignCast(gtk_app))) orelse return true;
const gdk_surface = c.gtk.gtk_native_get_surface(@ptrCast(win));
if (gdk_surface) |s| c.gtk.gdk_surface_beep(s);
return true;
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 Bell swallowed when window unavailable

The fallback path returns true even when neither gtk_application_get_active_window nor gtk_native_get_surface succeed, telling ghostty the bell was handled while nothing was played. The same pattern appears in the early orelse block at line 180. Returning false in the window-not-found cases allows ghostty to invoke its own fallback rather than silently dropping the bell.

Suggested change
// Fall back to system bell
const gtk_app = c.gtk.g_application_get_default() orelse return true;
const win = c.gtk.gtk_application_get_active_window(@ptrCast(@alignCast(gtk_app))) orelse return true;
const gdk_surface = c.gtk.gtk_native_get_surface(@ptrCast(win));
if (gdk_surface) |s| c.gtk.gdk_surface_beep(s);
return true;
// Fall back to system bell
const gtk_app2 = c.gtk.g_application_get_default() orelse return false;
const win2 = c.gtk.gtk_application_get_active_window(@ptrCast(@alignCast(gtk_app2))) orelse return false;
const gdk_surface2 = c.gtk.gtk_native_get_surface(@ptrCast(win2));
if (gdk_surface2) |s| c.gtk.gdk_surface_beep(s);
return gdk_surface2 != null;

Zig C translator maps C enums to c_int/c_uint, not Zig enums.
Replace @intFromEnum with @intcast for goto_tab, and remove
@enumFromInt for clipboard_type — compare against constants directly.
Comment thread cmux-linux/src/app.zig
Comment on lines +319 to +320
c.gtk.gdk_clipboard_read_text_async(clipboard, null, &onClipboardReadComplete, ctx);
return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stale surface handle in async clipboard callback

ClipboardReadContext stores a raw ghostty_surface_t by value. If onCloseSurface runs while gdk_clipboard_read_text_async is still pending (e.g. the user closes the tab mid-paste), onClipboardReadComplete fires with a freed surface handle and calls ghostty_surface_complete_clipboard_request on it — a use-after-free.

Pass a GCancellable* so the pending read can be cancelled when the surface is torn down, or store a weak/refcounted reference instead of the raw handle:

// Allocate context for the async callback
const alloc = std.heap.c_allocator;
const cancellable = c.gtk.g_cancellable_new();  // caller must cancel on surface close
const ctx = alloc.create(ClipboardReadContext) catch {
    c.gtk.g_object_unref(cancellable);
    return false;
};
ctx.* = .{
    .surface = ghostty_surface,
    .completion_context = context,
    .cancellable = cancellable,
};

c.gtk.gdk_clipboard_read_text_async(clipboard, cancellable, &onClipboardReadComplete, ctx);
return true;

onCloseSurface would then call g_cancellable_cancel(surface_data.clipboard_cancellable) before freeing the surface, ensuring the callback either never fires or detects cancellation via g_cancellable_is_cancelled.

@Jesssullivan Jesssullivan merged commit 943cffd into main Apr 18, 2026
24 of 29 checks passed
@Jesssullivan Jesssullivan deleted the sid/action-handlers-batch2 branch April 18, 2026 17:53
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