Click and scroll in a session with send --mouse #145

Merged
natsukium merged 2 commits from cli/send-mouse into main 2026-10-14 09:12:20 +09:00
Owner

Closes #131.

Commits

  1. protocol, grid, daemon: place a mouse event's missing pixels at its cell's middle. A mouse event with a 0 pixel axis (a sender with no pointer) gets the middle of the pixels a program maps back to its cell, at the PTY winsize's full ratio (Grid::winsize_pixels, from #143). The encode_mouse protocol filter is factored out as felis_grid::mouse_event_reported.
  2. cli, docs, skills: click and scroll in a session with send --mouse. send --mouse [MODS+]ACTION[-BUTTON]:COL,ROW (repeatable), 0-based cells matching search --source visible's col_spans. --key and --mouse go out after TEXT in command-line order (a hand-written clap Args reads both back by argv index). Before writing anything the CLI checks every event against the burst's ModeFlags.mouse_protocol and the grid dims, and refuses the whole command (invalid_request, exit 1) when the program would drop one or a cell is off the grid.

No wire change, no minor gate: Input::Mouse from a non-window attachment and ModeFlags.mouse_protocol both exist since minor 0; only the doc comment on px/py gains the 0 meaning.

Decisions settled here

  • Pixel placement: the middle of the integer pixels that map back to the cell, not a center from the floored cell size (column 80 of an 809-px, 80-column grid would land on 79), nor the rounded geometric center (falls on a pixel shared with the previous cell when cells are barely wider than one pixel).
  • Error kind: invalid_request, no new kind; no caller branches on it yet and kinds are append-only.
  • control-surfaces rule: "a different payload kind is a new verb" contradicted --key; reworded to: an input kind that stands alone joins the first constraint, anything that reads only in combination with another flag is a new verb.
  • Accepted, not designed out: a mode change between the check and the write; X10 clamping past 223; a grid whose supplied pixel extent is smaller than its cell count (no window produces one).

Verification

  • cargo nextest run --workspace: 4068 passed; workspace clippy (--all-targets --all-features) clean; just prose-check 0 hits; just schema no drift; each commit builds.
  • End to end against an isolated daemon: a Textual app (SGR-pixels at the nominal cell) had its buttons clicked with send --mouse click:8,4 / click:8,1; an off-grid cell and a plain shell were refused with exit 1 and nothing sent.
Closes #131. ## Commits 1. **protocol, grid, daemon: place a mouse event's missing pixels at its cell's middle.** A mouse event with a `0` pixel axis (a sender with no pointer) gets the middle of the pixels a program maps back to its cell, at the PTY winsize's full ratio (`Grid::winsize_pixels`, from #143). The `encode_mouse` protocol filter is factored out as `felis_grid::mouse_event_reported`. 2. **cli, docs, skills: click and scroll in a session with `send --mouse`.** `send --mouse [MODS+]ACTION[-BUTTON]:COL,ROW` (repeatable), 0-based cells matching `search --source visible`'s `col_spans`. `--key` and `--mouse` go out after TEXT in command-line order (a hand-written clap `Args` reads both back by argv index). Before writing anything the CLI checks every event against the burst's `ModeFlags.mouse_protocol` and the grid dims, and refuses the whole command (`invalid_request`, exit 1) when the program would drop one or a cell is off the grid. No wire change, no minor gate: `Input::Mouse` from a non-window attachment and `ModeFlags.mouse_protocol` both exist since minor 0; only the doc comment on `px`/`py` gains the `0` meaning. ## Decisions settled here - **Pixel placement**: the middle of the integer pixels that map back to the cell, not a center from the floored cell size (column 80 of an 809-px, 80-column grid would land on 79), nor the rounded geometric center (falls on a pixel shared with the previous cell when cells are barely wider than one pixel). - **Error kind**: `invalid_request`, no new kind; no caller branches on it yet and kinds are append-only. - **control-surfaces rule**: "a different payload kind is a new verb" contradicted `--key`; reworded to: an input kind that stands alone joins the first constraint, anything that reads only in combination with another flag is a new verb. - **Accepted, not designed out**: a mode change between the check and the write; X10 clamping past 223; a grid whose supplied pixel extent is smaller than its cell count (no window produces one). ## Verification - `cargo nextest run --workspace`: 4068 passed; workspace clippy (`--all-targets --all-features`) clean; `just prose-check` 0 hits; `just schema` no drift; each commit builds. - End to end against an isolated daemon: a Textual app (SGR-pixels at the nominal cell) had its buttons clicked with `send --mouse click:8,4` / `click:8,1`; an off-grid cell and a plain shell were refused with exit 1 and nothing sent.
A sender with no pointer (the coming `sessions send --mouse`) cannot know
the pixel extent a program maps an SGR-pixel report back with: that is
the PTY winsize, which can hold the nominal cell while the roster reports
the pixels as unknown. The daemon now fills each pixel axis an event
leaves at 0 (no 1-based coordinate) with the middle of the pixels that
map back to its cell at the winsize's full ratio. A center built from the
floored cell size lands column 80 of an 809-pixel, 80-column grid on
column 79, and a rounded geometric center can fall on a pixel shared with
the previous cell when cells are barely wider than one pixel.

The protocol filter `encode_mouse` applies is factored out as
`mouse_event_reported`, so the CLI can refuse an event the program would
drop before writing anything.

Refs #131

Assisted-by: Claude Code:Opus 5.5
cli, docs, skills: click and scroll in a session with send --mouse
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
windows / detect relevant changes (pull_request) Successful in 10s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 57s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m10s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 43s
windows / cargo nextest (Windows) (pull_request) Successful in 2m52s
windows / frontend smoke (Windows) (pull_request) Successful in 56s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m40s
pr / wire schema is compatible with the base (pull_request) Successful in 2s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 28s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
9310a2a1d6
An agent driving a fullscreen TUI could read a button's cells off
`search --source visible` but not click it, short of hand-encoding an
SGR report with `--raw`, which guesses the encoding: wrong under X10 or
SGR-pixels, which Textual turns on wherever `?2048` is supported.

`send --mouse EVENT` (repeatable) sends `InputMsg::Mouse`, encoded
daemon-side against the live mouse modes, at 0-based cells so a
`col_spans` segment is clicked without arithmetic. It joins `send` rather
than a new verb for the reason `--key` did, and depends on no other flag.
`--key` and `--mouse` go out in command-line order, which the clap derive
loses across two `Vec`s, so the pair is read back by argv index.

An event the program would discard (no reporting on, or a drag under
`?1000`) or a cell off the grid fails the whole command before anything
is written: otherwise a click would report success while nothing
happened. A mode change after the check still drops an event behind a
successful fence; closing that needs a per-event fence outcome no
consumer has needed. A distinct error kind is not added: no caller
branches on it yet, and kinds are append-only.

Closes #131

Assisted-by: Claude Code:Opus 5.5
Author
Owner

Real-TUI run

Debug build of this branch, isolated daemon, headless sessions (no window, so the nominal cell). Targets found with search --source visible and clicked with the col_spans numbers as given.

Program Mouse mode What was sent Result
Textual 8.x app ?1003 + ?1016 (SGR-pixels) click on two buttons right button pressed each time
nvim (mouse=a) ?1002 + SGR click, wheel-down ×2, press/drag/release, ctrl+click-right cursor on the exact cell (12,13, 7,1); scrolled 6 lines; 3-line visual selection; ctrl+right ran tag pop (E73)
vim (mouse=a) SGR click, press/drag/release cursor on the exact cell (7,10), drag end at 4,7
htop 3.5 SGR click on PID and MEM% headers (after a multi-byte △), wheel-down, click on F10Quit sort column moved each time; list scrolled; htop quit
lazygit 0.65 SGR click on the Commits panel, then on commit 1 panel focused; patch view showed commit 1
mc 4.8 SGR click on Options; double click on /.git menu opened; entered .git
ranger ?1000 click, wheel-down; drag wheel and click move the cursor; drag refused with exit 1 (?1000 without ?1002), nothing sent
btop 1.4 SGR click on tree, click on menu tree view toggled; menu opened

Refusals seen: an off-grid cell (exit 1, outside the 80x24 grid) and a plain shell (exit 1, no mouse reporting on).

One quirk, not felis: ranger ignores the very first click after start-up. A hand-encoded --raw SGR click is ignored the same way, and every later click lands.

## Real-TUI run Debug build of this branch, isolated daemon, headless sessions (no window, so the nominal cell). Targets found with `search --source visible` and clicked with the `col_spans` numbers as given. | Program | Mouse mode | What was sent | Result | | --- | --- | --- | --- | | Textual 8.x app | `?1003` + `?1016` (SGR-pixels) | `click` on two buttons | right button pressed each time | | nvim (`mouse=a`) | `?1002` + SGR | `click`, `wheel-down` ×2, `press`/`drag`/`release`, `ctrl+click-right` | cursor on the exact cell (`12,13`, `7,1`); scrolled 6 lines; 3-line visual selection; `ctrl+right` ran tag pop (`E73`) | | vim (`mouse=a`) | SGR | `click`, `press`/`drag`/`release` | cursor on the exact cell (`7,10`), drag end at `4,7` | | htop 3.5 | SGR | `click` on `PID` and `MEM%` headers (after a multi-byte `△`), `wheel-down`, `click` on `F10Quit` | sort column moved each time; list scrolled; htop quit | | lazygit 0.65 | SGR | `click` on the Commits panel, then on `commit 1` | panel focused; patch view showed commit 1 | | mc 4.8 | SGR | `click` on `Options`; double `click` on `/.git` | menu opened; entered `.git` | | ranger | `?1000` | `click`, `wheel-down`; `drag` | wheel and click move the cursor; `drag` refused with exit 1 (`?1000 without ?1002`), nothing sent | | btop 1.4 | SGR | `click` on `tree`, `click` on `menu` | tree view toggled; menu opened | Refusals seen: an off-grid cell (exit 1, `outside the 80x24 grid`) and a plain shell (exit 1, `no mouse reporting on`). One quirk, not felis: ranger ignores the very first click after start-up. A hand-encoded `--raw` SGR click is ignored the same way, and every later click lands.
Author
Owner

Correction to the table above: the "Mouse mode" column was partly inferred, not observed. Observed: ranger runs ?1000 alone (the CLI refused its drag); nvim, vim and mc accept drags, so they run ?1002 or ?1003. Textual turning on SGR-pixels is from its source (#131), not read off the session. The encodings for htop, lazygit and btop were not checked; the results column is unaffected.

Correction to the table above: the "Mouse mode" column was partly inferred, not observed. Observed: ranger runs `?1000` alone (the CLI refused its `drag`); nvim, vim and mc accept drags, so they run `?1002` or `?1003`. Textual turning on SGR-pixels is from its source (#131), not read off the session. The encodings for htop, lazygit and btop were not checked; the results column is unaffected.
natsukium deleted branch cli/send-mouse 2026-10-14 09:12:20 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!145
No description provided.