feat(client): drag-to-resize panes + default outer-terminal mouse capture (ADR-0035)#142
Merged
Conversation
…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>
086898b to
8331d78
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 existingSET_METADATAlayout path. Adds ADR-0035.Acceptance bars
RouteDecision::Divider{node_path, axis}); button-motion retunes that split'sratioso the divider tracks the pointer; release commits the layout viaSET_METADATA(the same persistence + multi-consumer broadcast path the keyboardresize-paneuses). The drag reusesapply_resize'sMIN_PANE_CELLfloor +clamp_ratio, so a drag stalls at the floor instead of collapsing a pane, and the two resize routes cannot diverge.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 throughRouteDecision::Pane.route_mouse_eventstill takes the inset content rect; the status-bar-inset test now asserts a click on the reserved row resolves toRouteDecision::Miss(the renamedDividerNoOpfor true misses).Default-on behavior
mouseconfig defaults totrue. On attach the client enables its own outer-terminal mouse tracking —?1002h(button-event tracking, not?1003hany-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?1002lbefore?1049l, so the host terminal's mouse state is restored and no?1006h/?1002hleaks.Escape hatch
mouse = falsein[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-paneset-pane mouse offin §7 is recorded as follow-up in ADR-0035 — it needs aset-paneverb + per-pane client state this PR does not introduce. Until thenmouse = falseis 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 = falsefor 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 againstcontent_rect(.., has_bar, ..). For a Vertical split (horizontal, y-driven divider) with a status bar docked, the absolute drag would mis-track.apply_divider_resizenow takeshas_barand uses the same inset the hit-test does; addeddivider_resize_vertical_split_honours_status_bar_budgetto lock it.Tests
Ok(None)), stale-path errors.mouse = falseemits no DECSET.divider_hitscells == painted divider cells, each path resolves to aSplit.just cigreen locally.🤖 Generated with Claude Code