Skip to content

feat(client): drag-to-resize panes + default outer-terminal mouse capture (ADR-0035)#142

Merged
phall1 merged 1 commit into
mainfrom
feat/drag-resize-panes
Jun 18, 2026
Merged

feat(client): drag-to-resize panes + default outer-terminal mouse capture (ADR-0035)#142
phall1 merged 1 commit into
mainfrom
feat/drag-resize-panes

Conversation

@phall1

@phall1 phall1 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

What

Implements the documented-but-unbuilt half of docs/consumers/tui.md §7: drag a pane divider to resize the adjacent panes, with the new ratio persisted via the existing SET_METADATA layout path. Adds ADR-0035.

Acceptance bars

  • (a) Drag resizes + persists. A press on a divider cell grabs the split that divider controls (RouteDecision::Divider{node_path, axis}); button-motion retunes that split's ratio so the divider tracks the pointer; release commits the layout via SET_METADATA (the same persistence + multi-consumer broadcast path the keyboard resize-pane uses). The drag reuses apply_resize's MIN_PANE_CELL floor + clamp_ratio, so a drag stalls at the floor instead of collapsing a pane, and the two resize routes cannot diverge.
  • (b) Inner-TUI mouse preserved. Only divider cells change meaning. Every event inside a pane's rectangle is still forwarded to that pane with pane-local coordinates, exactly as before this PR. A pane running vim/htop with its own mouse mode on keeps receiving its events; the server's per-pane encoder (set_options_from_terminal) emits empty bytes when the inner app has no mode, so forwarding is harmless either way. Click-to-focus and scroll still route through RouteDecision::Pane.
  • (c) Hit-test inset regression (PR fix(client): Ctrl+J newline + pane-click routing #136). route_mouse_event still takes the inset content rect; the status-bar-inset test now asserts a click on the reserved row resolves to RouteDecision::Miss (the renamed DividerNoOp for true misses).

Default-on behavior

mouse config defaults to true. On attach the client enables its own outer-terminal mouse tracking — ?1002h (button-event tracking, not ?1003h any-motion which would flood the wire) + ?1006h (SGR coordinates, needed past column 223). That capture is what makes divider drags work in a plain shell: without it the client is deaf to the pointer over a divider whenever the inner program has no mouse mode (the common case). On detach the reset emits ?1006l?1002l before ?1049l, so the host terminal's mouse state is restored and no ?1006h/?1002h leaks.

Escape hatch

mouse = false in [defaults] skips the DECSET entirely and reverts to pure pass-through (the client only sees mouse when an inner program enables it; host native selection untouched). The per-pane set-pane mouse off in §7 is recorded as follow-up in ADR-0035 — it needs a set-pane verb + per-pane client state this PR does not introduce. Until then mouse = false is the opt-out.

Native-text-selection tradeoff

Enabling outer capture suppresses the host terminal's native click-drag text selection inside the phux viewport. Hold Shift to bypass application mouse reporting and restore native selection (a near-universal terminal convention; phux relies on it but does not enforce it). A host that does not honour Shift-bypass needs mouse = false for easy selection. Documented in §7.

Fix made while landing

The original draft computed the divider span with content_rect(.., has_bar=false, ..) while the hit-test routed against content_rect(.., has_bar, ..). For a Vertical split (horizontal, y-driven divider) with a status bar docked, the absolute drag would mis-track. apply_divider_resize now takes has_bar and uses the same inset the hit-test does; added divider_resize_vertical_split_honours_status_bar_budget to lock it.

Tests

  • divider hit-test maps to the controlling split (path + axis).
  • drag delta sets ratio from absolute pointer, both directions, respecting the min-cell floor (Ok(None)), stale-path errors.
  • status-bar budget honoured for a vertical-split drag.
  • mouse-capture enable/disable byte emission + ordering (disable precedes alt-screen leave); mouse = false emits no DECSET.
  • proptest: divider_hits cells == painted divider cells, each path resolves to a Split.

just ci green locally.

🤖 Generated with Claude Code

…ture (ADR-0035)

Dragging a pane divider now resizes the adjacent panes and persists the
new ratio. The client enables its own outer-terminal mouse tracking
(?1002h button-motion + ?1006h SGR) on attach so divider drags work in a
plain shell, and restores the host terminal's mouse state on detach
(?1006l?1002l before ?1049l) so no tracking mode leaks.

Plumbing:
- layout: NodePath addressing + set_ratio_at to retune a specific split.
- rasterize/walk_layout: tag each divider segment with its split's path;
  divider_hits builds the grab map from the same segments + viewport
  clamp rasterize paints, so a hit cell is always a painted divider cell.
- mouse: a divider press returns RouteDecision::Divider{node_path, axis};
  a true miss (reserved chrome, degenerate viewport) returns Miss.
- actions: apply_divider_resize sets the grabbed split's ratio from the
  absolute pointer using the SAME content rect the hit-test used
  (status-bar/sidebar inset honoured) and the keybind resize's
  MIN_PANE_CELL floor + clamp_ratio, so the two routes cannot diverge.
- input_dispatch/driver: a press grabs, button-motion retunes, release
  commits via SET_METADATA (the keyboard-resize persistence path).

Inner TUIs keep their mouse: only divider cells change meaning. Every
pane-interior event is still forwarded with pane-local coords, and the
server's per-pane encoder emits empty bytes when the inner app has no
mouse mode, so forwarding is harmless either way.

Capture is gated by the existing `mouse` config (default true). The
per-pane `set-pane mouse off` escape hatch is follow-up; `mouse = false`
is the current opt-out and restores pure pass-through. Native host text
selection is bypassed with Shift (documented in tui.md §7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@phall1 phall1 force-pushed the feat/drag-resize-panes branch from 086898b to 8331d78 Compare June 18, 2026 01:59
@phall1 phall1 merged commit 76a39dc into main Jun 18, 2026
3 checks passed
@phall1 phall1 deleted the feat/drag-resize-panes branch June 18, 2026 02:10
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