Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 103 additions & 5 deletions cmux-linux/src/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,77 @@ const window = @import("window.zig");
/// Returns true if handled, false to let libghostty handle it.
pub fn onAction(
_: c.ghostty_app_t,
_: c.ghostty.ghostty_target_s,
_: c.ghostty.ghostty_action_s,
target: c.ghostty.ghostty_target_s,
action: c.ghostty.ghostty_action_s,
) callconv(.c) bool {
// TODO: dispatch actions (new_tab, close_tab, set_title, etc.)
return switch (action.tag) {
c.ghostty.GHOSTTY_ACTION_SET_TITLE => handleSetTitle(target, action.action.set_title),
c.ghostty.GHOSTTY_ACTION_PWD => handlePwd(target, action.action.pwd),
else => false,
};
}

/// Update the panel/workspace title from terminal escape sequences.
fn handleSetTitle(target: c.ghostty.ghostty_target_s, title: c.ghostty.ghostty_action_set_title_s) bool {
const tm = window.getTabManager() orelse return false;
const title_str = if (title.title) |t| std.mem.span(t) else return false;

// Find the surface from target and update its workspace title.
const surface_ud = getSurfaceUserdata(target) orelse return false;
const widget: *c.GtkWidget = @ptrCast(@alignCast(surface_ud));

for (tm.workspaces.items) |ws| {
var it = ws.panels.valueIterator();
while (it.next()) |panel_ptr| {
const panel = panel_ptr.*;
if (panel.widget) |pw| {
if (pw == widget) {
// Set panel title
panel.title = ws.alloc.dupe(u8, title_str) catch null;
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 Memory leak: old panel.title not freed before replacement

panel.title is overwritten on every set_title escape sequence (which fires on each shell prompt change) without freeing the previous allocation. Over a long session this grows unboundedly. Compare with ws.setTitle() which correctly frees first.

Suggested change
panel.title = ws.alloc.dupe(u8, title_str) catch null;
if (panel.title) |old| ws.alloc.free(old);
panel.title = ws.alloc.dupe(u8, title_str) catch null;

// If no custom workspace title, propagate to workspace
if (ws.custom_title == null) {
ws.setTitle(title_str);
tm.updateTabTitle(ws);
}
return true;
}
}
}
}
return false;
}

/// Update the workspace's current directory from shell integration.
fn handlePwd(target: c.ghostty.ghostty_target_s, pwd: c.ghostty.ghostty_action_pwd_s) bool {
const tm = window.getTabManager() orelse return false;
const pwd_str = if (pwd.pwd) |p| std.mem.span(p) else return false;

const surface_ud = getSurfaceUserdata(target) orelse return false;
const widget: *c.GtkWidget = @ptrCast(@alignCast(surface_ud));

for (tm.workspaces.items) |ws| {
var it = ws.panels.valueIterator();
while (it.next()) |panel_ptr| {
const panel = panel_ptr.*;
if (panel.widget) |pw| {
if (pw == widget) {
panel.directory = ws.alloc.dupe(u8, pwd_str) catch null;
ws.current_directory = ws.alloc.dupe(u8, pwd_str) catch null;
Comment on lines +67 to +68
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 Memory leak: old panel.directory / ws.current_directory not freed

Both slices are overwritten without freeing the previous allocation, leaking memory on every shell PWD integration update. Same fix pattern as ws.setTitle():

Suggested change
panel.directory = ws.alloc.dupe(u8, pwd_str) catch null;
ws.current_directory = ws.alloc.dupe(u8, pwd_str) catch null;
if (panel.directory) |old| ws.alloc.free(old);
panel.directory = ws.alloc.dupe(u8, pwd_str) catch null;
if (ws.current_directory) |old| ws.alloc.free(old);
ws.current_directory = ws.alloc.dupe(u8, pwd_str) catch null;

return true;
}
}
}
}
return false;
}

/// Extract the surface userdata pointer from a ghostty target.
fn getSurfaceUserdata(target: c.ghostty.ghostty_target_s) ?*anyopaque {
if (target.tag != c.ghostty.GHOSTTY_TARGET_SURFACE) return null;
const surface = target.target.surface orelse return null;
return c.ghostty.ghostty_surface_userdata(surface);
}

/// Read clipboard callback: libghostty wants clipboard contents.
/// Returns false when clipboard data is not available.
pub fn onReadClipboard(
Expand Down Expand Up @@ -45,7 +109,41 @@ pub fn onWriteClipboard(
) callconv(.c) void {}

/// Close surface callback: terminal process exited or user requested close.
/// The userdata is the GtkWidget pointer set during surface creation.
/// Walk the tab manager to find the owning panel and remove it.
/// If a workspace becomes empty after removal, close it.
pub fn onCloseSurface(
_: ?*anyopaque,
userdata: ?*anyopaque,
_: bool,
) callconv(.c) void {}
) callconv(.c) void {
const widget: *c.GtkWidget = @ptrCast(@alignCast(userdata orelse return));
const tm = window.getTabManager() orelse return;

// Find which workspace/panel owns this widget.
// Capture the panel ID first, then remove outside the iterator
// to avoid iterator invalidation.
var found_panel_id: ?u128 = null;
var found_ws_idx: usize = 0;
outer: for (tm.workspaces.items, 0..) |ws, ws_idx| {
var it = ws.panels.valueIterator();
while (it.next()) |panel_ptr| {
const panel = panel_ptr.*;
if (panel.widget) |pw| {
if (pw == widget) {
found_panel_id = panel.id;
found_ws_idx = ws_idx;
break :outer;
}
}
}
}

const panel_id = found_panel_id orelse return;
const ws = tm.workspaces.items[found_ws_idx];
ws.removePanel(panel_id);

// If the workspace is now empty, close it (unless it's the last one).
if (ws.panelCount() == 0 and tm.workspaces.items.len > 1) {
tm.closeWorkspace(found_ws_idx);
}
}
4 changes: 4 additions & 0 deletions cmux-linux/src/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ pub const Surface = struct {
} };
surface_config.scale_factor = scale;

// Pass the GtkWidget pointer as surface userdata so the
// close_surface_cb can identify which panel to remove.
surface_config.userdata = @ptrCast(@alignCast(gl_area));

surface.ghostty_surface = c.ghostty.ghostty_surface_new(
surface.ghostty_app,
&surface_config,
Expand Down
Loading