Skip to content

linux: implement split pane actions, surface close, and input fixes#241

Merged
Jesssullivan merged 8 commits intomainfrom
sid/linux-split-actions
Apr 19, 2026
Merged

linux: implement split pane actions, surface close, and input fixes#241
Jesssullivan merged 8 commits intomainfrom
sid/linux-split-actions

Conversation

@Jesssullivan
Copy link
Copy Markdown
Owner

@Jesssullivan Jesssullivan commented Apr 18, 2026

Summary

  • Implement split pane management actions: new split, goto split, resize split, equalize splits, toggle split zoom (stub)
  • Add move_tab action for keybind-driven tab reordering via AdwTabView
  • Fix split tree update on surface close (shell exit / close-surface callback) — promotes sibling node and rebuilds widget tree
  • Add consumed_mods logic for shift key and Num Lock modifier translation
  • Define GDK_MOD2_MASK locally (C macro not visible through Zig's @cImport)

Greptile findings addressed

  • P1: Use-after-free in applyRatiosIdle — carry workspace ID instead of raw pointer
  • P1: Orphaned panel when splitPane fails — find target node before creating panel, clean up on error
  • P1: buildWidget called before leaf widgets are unparented — close old tab page and detach leaves from old GtkPaneds first
  • P1: Resize delta uses hardcoded 1000px divisor — use actual pane size from gtk_widget_get_width/height
  • P2: toggle_split_zoom returned true before implemented — changed to return false
  • P2: collectLeaves silently drops leaves on OOM — added log.warn

Test plan

  • CI passes on all distros (Arch, Debian, Ubuntu, Fedora, Rocky)
  • Nix flake check passes
  • Manual smoke test on honey: split pane, navigate, resize, close pane, close surface

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.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR implements the full split pane action surface for the Linux client: new_split, goto_split, resize_split, equalize_splits, and move_tab. It also fixes the previously-flagged P1s — orphaned panel on split failure, use-after-free in the idle ratio callback, buildWidget before unparenting leaves, and hardcoded 1000 px divisor — plus adds consumed_mods Shift tracking and Num Lock translation.

  • P1 – focused_panel_id stale after surface close: removePanel (unlike detachPanel) never updates focused_panel_id. After a pane closes via shell exit, the workspace's focused ID still points to the dead panel; every subsequent new_split / goto_split / resize_split silently returns false until the user triggers a GTK focus event by clicking a pane.

Confidence Score: 4/5

Safe to merge after addressing the stale focused_panel_id bug; all other previously-flagged P1s are resolved.

One new P1 found: after a pane closes via surface exit, focused_panel_id is not updated, leaving the workspace unable to perform any further split actions without a manual click. All previously-flagged P1s (use-after-free, orphaned panel, wrong divisor, buildWidget ordering) are correctly addressed. The remaining findings are P2 (OOM leak in splitPane, incomplete consumed_mods for symbols).

cmux-linux/src/app.zig — onCloseSurface split path needs focused_panel_id update after closePane

Important Files Changed

Filename Overview
cmux-linux/src/app.zig Implements six new split pane actions, move_tab reordering, and a rebuildWorkspaceWidget helper; one P1 remains: focused_panel_id is not updated after surface close, breaking all subsequent split actions on the surviving pane.
cmux-linux/src/split_tree.zig Adds collectLeaves, adjacentLeaf, equalize, and applyRatios; correctness is solid except for a minor memory leak in splitPane when createLeaf OOMs after the first node is allocated.
cmux-linux/src/surface.zig Adds consumed_mods Shift detection and GDK_MOD2_MASK Num Lock translation; Shift marking is incomplete for symbol keys (Shift+1 → ! etc.) but is a clear improvement over always-NONE.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[onAction] --> B{Action type}
    B -->|NEW_SPLIT| NS[handleNewSplit]
    B -->|GOTO_SPLIT| GS[handleGotoSplit]
    B -->|RESIZE_SPLIT| RS[handleResizeSplit]
    B -->|EQUALIZE_SPLITS| ES[handleEqualizeSplits]
    B -->|TOGGLE_SPLIT_ZOOM| TZ[handleToggleSplitZoom no-op]
    B -->|MOVE_TAB| MT[handleMoveTab]
    NS --> NS1[findNodeByPanel before create]
    NS1 --> NS2[createTerminalPanel]
    NS2 --> NS3[split_tree.splitPane]
    NS3 -->|error| NS4[removePanel + restore focused_id]
    NS3 -->|ok| NS5[rebuildWorkspaceWidget]
    NS5 --> NS6[gtk_widget_grab_focus]
    GS --> GS1[adjacentLeaf]
    GS1 --> GS2[ws.focused_panel_id = leaf.panel_id]
    GS2 --> GS3[gtk_widget_grab_focus]
    RS --> RS1[findResizeSplit]
    RS1 --> RS2[delta = amount / paned_size]
    RS2 --> RS3[clamp ratio 0.1 to 0.9]
    RS3 --> RS4[applyRatios idle callback carries ws_id]
    MT --> MT1[adw_tab_view_reorder_page]
    MT1 --> MT2[tm.selected_index = target_idx]
    onClose[onCloseSurface] --> OC1{leafCount > 1?}
    OC1 -->|yes| OC2[closePane + removePanel focused_panel_id NOT updated]
    OC2 --> OC3[rebuildWorkspaceWidget]
    OC1 -->|no| OC4[removePanel close workspace if empty]
    RBW[rebuildWorkspaceWidget] --> R1[close old AdwTabPage]
    R1 --> R2[detachLeavesFromParents]
    R2 --> R3[split_tree.buildWidget]
    R3 --> R4[adw_tab_view_insert]
    R4 --> R5[g_idle_add applyRatiosIdle by ws_id]
Loading

Reviews (6): Last reviewed commit: "fix: grab GTK focus on new pane after sp..." | Re-trigger Greptile

Comment thread cmux-linux/src/app.zig Outdated
Comment thread cmux-linux/src/app.zig
Comment thread cmux-linux/src/app.zig
Comment thread cmux-linux/src/split_tree.zig
Comment thread cmux-linux/src/app.zig Outdated
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.
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.
Comment thread cmux-linux/src/app.zig Outdated
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.
- 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.
- 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.
@Jesssullivan Jesssullivan changed the title linux: implement split pane management actions linux: implement split pane actions, surface close, and input fixes Apr 19, 2026
Comment thread cmux-linux/src/app.zig
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.
Comment thread cmux-linux/src/app.zig
Comment on lines +803 to +810
if (ws.root_node) |root| {
if (split_tree.leafCount(root) > 1) {
_ = split_tree.closePane(ws.alloc, root, panel_id);
ws.removePanel(panel_id);
rebuildWorkspaceWidget(tm, ws);
return;
}
}
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 focused_panel_id after pane close

removePanel removes the panel from both maps but never updates focused_panel_id (confirmed — only detachPanel does that). After this path returns, ws.focused_panel_id still equals the now-dead panel_id. On the next handleNewSplit / handleGotoSplit / handleResizeSplit, findNodeByPanel(root, focused_id) returns null and every action silently returns false. The user is left unable to split or navigate until a mouse click or other GTK focus event fires.

The fix is to find an adjacent leaf before closing and transfer focus to it:

// Find a sibling to transfer focus to before closing the pane
const next_leaf = split_tree.adjacentLeaf(root, panel_id, .next, ws.alloc);

_ = split_tree.closePane(ws.alloc, root, panel_id);
ws.removePanel(panel_id);

// Update data-model focus so split actions keep working
if (next_leaf) |l| {
    ws.focused_panel_id = l.panel_id;
    if (l.widget) |w| _ = c.gtk.gtk_widget_grab_focus(w);
} else {
    ws.focused_panel_id = null;
}

rebuildWorkspaceWidget(tm, ws);
return;

@Jesssullivan Jesssullivan merged commit eafbfef into main Apr 19, 2026
25 of 29 checks passed
@Jesssullivan Jesssullivan deleted the sid/linux-split-actions branch April 19, 2026 04:09
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