Skip to content

Mouse support across every screen - #2

Merged
madhermit merged 5 commits into
mainfrom
mouse-support
Jul 16, 2026
Merged

madhermit merged 5 commits into
mainfrom
mouse-support

Conversation

@madhermit

@madhermit madhermit commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

First-class mouse interaction for all six screens (diff, log, stash, stage, tests lens, menu):

  • Wheel scrolls the pane under the pointer — previews via the viewport's native wheel handling (vertical and horizontal), lists by moving the selection (so the preview follows).
  • Click selects the row it lands on and focuses the pane it hit — without entering the collapsed reading layout, which stays a keyboard gesture (⇥/⏎); clicking the one-row peek still expands back to the list. Rows are resolved against the layout at click time, so clicking the one-row peek while reading returns to the list on the same item instead of re-windowing under the pointer.
  • Menu: a click selects; a click on the selected entry launches it.
  • Drag-to-copy: dragging across a diff preview selects display lines (highlighted live, reverse video re-asserted across the content's own SGR resets) and releasing copies them — ANSI-stripped, right-trimmed, no borders/gutters — via the same clipboard path as y, so OSC 52 covers SSH. Selection anchors at press time and the click's focus change defers to a clean release, so the layout never shifts under an in-progress drag.
  • Opt-out: RIFT_MOUSE=off (also 0/none/false) disables tracking entirely for users who prefer native terminal text selection (shift-click bypasses tracking in most terminals regardless).

Implementation: bubbletea v2 requests mouse mode per view, so tui.ScreenView (alt-screen + cell-motion mouse, honoring the opt-out) is the single place every screen builds its tea.View — SplitList covers four screens with one handler; stage (horizontal split, X-based hit-testing) and menu get small handlers reusing their existing selection/focus paths, with the windowed click→row mapping shared via tui.ClickedRow and pane switching unified per screen behind a focusPane helper the keyboard paths also use. Modal states mirror the keyboard: help overlay and confirm prompts ignore the mouse; the wheel still works while filtering. No motion tracking, no drag, no new dependencies.

Review

The branch went through /code-review high --fix (8 finder angles → recall-biased verification, all 10 reported findings CONFIRMED) and a /simplify pass. The review caught two serious bugs in the original commit: stage's views never requested mouse mode (its whole mouse handler was unreachable from a real terminal — the SGR-injection testing used initially couldn't detect a missing mode request), and log drilldowns leaked mouse events to the hidden commit list (selection drifted invisibly; a later cherry-pick/revert would have hit the wrong commit). Also fixed: menu clicks activating entries clipped off short terminals, no-op stage clicks snapping the diff scroll back, doubled diff loads on stage file clicks, boundary wheel churn, and — from the simplify pass — cancellation of superseded stage diff loads (a wheel flick no longer piles up doomed difftastic subprocesses).

Verification

  • mise run check green; unit tests cover the click→row mapping (windowed, scrolled, border/past-end) and focus-driven relayout.
  • Mouse-mode requests verified at the terminal level: raw output captured via script(1) shows the cell-motion enable sequence (\e[?1002h) on every screen including stage, and its absence under RIFT_MOUSE=off.
  • Exercised live by injecting SGR mouse sequences into tmux sessions: click-to-select and wheel on the splitlist screens, stage file-row clicks and diff-pane wheel, menu click-select + click-launch, click-to-focus peek collapse and return — and the drilldown fix: wheeling inside a drilled commit moves the drilled view while the commit selection stays put on esc.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EB5anNK6czSCdjg51CH7VD

The wheel scrolls the pane under the pointer — over a list it moves the
selection, over a preview it scrolls by three lines per notch. A left
click selects the row it lands on and focuses the pane it hit; clicked
rows resolve against the layout at click time, before a focus change
can re-window them (clicking the one-row peek while reading returns to
the list on the same item). In the menu launchpad a click on the
already-selected entry launches it, matching what a click on a menu
means everywhere else.

Mouse mode is requested per-view (bubbletea v2's View.MouseMode) via the
new tui.ScreenView helper every screen now builds its view through, so
alt-screen and mouse state stay consistent across diff, log, stash,
stage, the tests lens, and the menu. Cell-motion tracking only — no
motion events. RIFT_MOUSE=off opts out entirely for users who prefer
the terminal's native text selection; most terminals also bypass
tracking with shift-click, so copying from a diff works either way.

Modal states behave like the keyboard: the help overlay and inline
confirm prompts ignore the mouse; the wheel still works while
filtering.
The big one: stage's three view sites were never converted to
tui.ScreenView, so MouseMode stayed unset and — since bubbletea v2
drives terminal mouse tracking from the returned view — the stage
screen never enabled tracking and its whole mouse handler was
unreachable from a real terminal. The SGR-injection testing used to
verify the feature couldn't catch this (bubbletea parses injected
sequences whether or not tracking was requested); verification now
greps the raw terminal output for the cell-motion enable sequence
per screen, including the RIFT_MOUSE=off suppression.

Also:
- Route mouse input to the drilled view while a log drilldown is open.
  It previously fell through to the hidden commit list, silently
  moving its selection — esc landed on a different commit and a
  subsequent cherry-pick/revert would have targeted it.
- Bound menu clicks by the panel's visible height, not just the item
  count, so border/footer clicks on a short terminal can't select (or
  launch) entries that are clipped out of view.
- Make no-op stage clicks no-ops: clicking an already-focused pane no
  longer re-runs the layout (which snapped the diff scroll back to the
  selected hunk), and a selection-changing click issues one diff load
  instead of two (selectTo's was always discarded when applyLayout
  bumped the request ID again). selectTo also gains the same boundary
  guard stepList has, so wheeling past the list edge stops resetting
  the hunk selection and re-rendering.
- Delegate preview/diff wheel events to the bubbles viewport, which
  handles them natively (including horizontal wheel) — deleting the
  WheelLines constant that duplicated viewport.MouseWheelDelta.
- Share the windowed click-to-row mapping (tui.ClickedRow) between
  SplitList and stage instead of two drifting copies; route click
  focus through relayout like the keyboard path; dedupe the menu's
  enter/click activation; correct MouseEnabled's accepted-values doc.
- Cancel superseded stage diff loads: requestDiff now cancels the
  in-flight load's context before starting a new one, so a wheel flick
  across uncached files kills each superseded difftastic run instead of
  letting dozens complete into discarded results (the splitlist screens
  already had this via their stream cancel; stage did not).
- One pane-switch path per screen: stage gains the focusPane helper its
  keyboard and mouse handlers now share (same-pane no-op included), and
  splitlist's tab/enter route through the focusPane the mouse work
  introduced instead of inlining the flip.
- The log drilldown's three input-routing gates collapse into one
  type-switch (keys, mouse, editor-close all belong to the visible
  view); an allowlist by design — the parent's untagged async traffic
  must keep flowing to the hidden list.
- Menu derives its panel geometry from one contentHeight helper for
  both render and click hit-test, activates through one path, and its
  loading frame goes through ScreenView like every other view — the
  last stray tea.NewView, which made ScreenView's every-screen claim
  false and dropped alt-screen/mouse mode on the entry frame.
- Wheel handlers pass WheelDelta straight to the selection steppers
  (which already no-op on zero/boundary), dropping a branch per site;
  the unfireable navH guard in the splitlist hit-test is gone; stage's
  vim-jump pre-check was subsumed by selectTo's new boundary guard.
Dragging across a preview selects display lines — highlighted in
reverse video as you drag (re-asserted across the content's own SGR
resets, so styled diff lines highlight end to end) — and releasing
copies them through the same clipboard path as y, so the OSC 52
fallback covers SSH sessions. The copy is the displayed text,
ANSI-stripped and right-trimmed: no panel borders, no scrollbar
column, none of the artifacts a terminal-native selection would grab.

Selection is anchored to display-line indices at press time, and the
focus change a click normally performs waits for a clean release —
focusing can collapse the list strip and shift the content under the
pointer mid-drag. A plain click (press + release, no motion) focuses
the preview exactly as before; cell-motion tracking already reports
drag motion, so no new tracking mode is requested.
Clicking the preview used to enter the reading layout — the strip
snapping down to a one-row peek under the pointer, which reads as the
file list 'scoping down' to the selected file. Focus and the peek
collapse are now separate: a click focuses the pane it hit (border
highlight, key routing) while the layout stays put, and the collapse
remains a deliberate keyboard gesture (⇥/⏎ set peek; focusing the list
by any route leaves it, so clicking the peek row still expands).
@madhermit
madhermit merged commit aad18b8 into main Jul 16, 2026
1 check passed
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