cli: mouse input for driving a fullscreen TUI from felis sessions #131
Labels
No labels
Area/ci
Area/cli
Area/client
Area/client-core
Area/daemon
Area/deps
Area/docs
Area/grid
Area/manual
Area/nix
Area/protocol
Area/pty
Area/render
Area/shaping
Area/skills
Area/transport
Area/vt
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Performance
Kind/Security
Kind/Testing
perf-regression-accepted
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Blocked
Status
Need More Info
Status
Upstream
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
felis-terminal/felis#131
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
An agent driving a fullscreen TUI through
felis sessionscan read a button's label off the screen (search --source visiblereturns itscol_spans), but cannot click it. Pressing the label it sees is often one step where finding the equivalent keybinding is a help-screen hunt or impossible, and mouse-first TUIs are increasingly common. The same gap blocks an agent verifying the mouse handling of a TUI it is building.Today the caller hand-encodes SGR 1006 reports and sends them with
--raw:This guesses the encoding: it is wrong under X10 or SGR-pixel (
?1016) reporting, and if the application enabled no mouse tracking at all the bytes reach it as garbage input, with nothing telling the caller. SGR-pixel is not a corner case: Textual enables?1016on every terminal that supports?2048, felis included. This is the stale-mode problemsend --keysolves for keys by encoding daemon-side (docs/explanation/architecture/control-surfaces.md"send --keyreuses the chord grammar and sends the keystroke itself").No task was found that is reachable only by mouse (Claude Code's one mouse-only gap, starting a selection, does not matter to an agent that reads the screen); the consumer is the click-what-you-read path above and TUI mouse verification.
What exists
Input::Mouse(MouseEvent { button, action, mods, x, y, px, py })(1-based cells and pixels) is accepted from any attachment, window or not, since minor 0. The daemon encodes it against the session's current mouse protocol and encoding (felis_grid::encode_mouse). It reservesMAX_MOUSE_REPORT_BYTESand waits for budget rather than being dropped (serve.rsinput_reservation_bytes).sendalready attaches, drains the Ops rehydrate burst, writes its frames in order, and confirms admission withSession::InputFence. The burst carriesGridMsg::ModeFlags.mouse_protocol(not the encoding) and no grid rows; the grid dims arrive in the attach reply (Attached.info.dims), whichattach_by_prefixcurrently discards.TIOCGWINSZ,cell_pixel_w); a session no window has sized holds0until #143 gives it a nominal cell size.Design
Surface:
send --mouse EVENT, repeatable. No new verb.send's explanation keeps--keyonsendbecause one call names one target, resolves it once, and states the input sequence in order; that holds for mouse events unchanged.--mousedepends on no other flag, so it respects the frozen-grammar rule ("no new flag may depend on another"); the first constraint becomes "at least one of TEXT,--key,--mouseor--wait". The control-surfaces sentence "a different payload kind is a new verb" contradicts--keyalready being a payload kind onsend, and is reworded to what it guards: a flag that reads only in combination with another.Grammar.
EVENT := [MODS+]ACTION[-BUTTON]:COL,ROWACTION:click(press then release),press,drag,release,wheel-up,wheel-down,wheel-left,wheel-right.clickexists because a press whose release the caller forgets leaves the TUI mid-drag.BUTTON:left(default),middle,right; not accepted on wheel actions.MODS:shift,alt,ctrlin the[keymap]chord spelling;superis a usage error (no mouse report carries it).COL,ROW: 0-based, matchingsearch'scol_spansandcapture's liverow, so a match is clicked without arithmetic. The CLI adds 1.?1003hover) is left out until a consumer needs it.Ordering. TEXT is pasted first (unchanged);
--keyand--mouseframes then go out interleaved in argv order. Today's "keys after text" stays true.Pixels: derived daemon-side. A CLI event carries
px = py = 0, which no 1-based pixel coordinate uses. When the encoding isSgrPixelsand an event carries no pixels, the encoder fills them with the center of the event's cell, computed from the full pixel extents the session reports to the program (TIOCGWINSZ, and?2048once #143 settles it) and the grid dims:px = floor((col0 + 0.5) * pixel_w / cols) + 1, likewise forpy. Not from the truncatedcell_pixel_w: a program maps pixels back with the full ratio (Textual:(px - 1) / (pixel_w / cols)), and on an 809-pixel, 80-column grid a center built fromcell_pixel_w = 10lands column 80's click on column 79. Tests cover extents not divisible by the cell counts. Depends on #143, for two of its outcomes:?2048reports the PTY's real pixel extents (until then a Textual app crashes on any mouse event, CLI or window), and a session no window has sized gets a nominal cell size, so the headlesssessions spawnsession this verb mostly drives always has pixel geometry to derive from.Preflight: refuse when the program is not listening. After the drain, the CLI checks every event against
ModeFlags.mouse_protocolwith the filterencode_mouseapplies (factored into a shared pure predicate): protocolOff, or adragunder?1000. Any filtered event, or a coordinate outsideAttached.info.dims(kept instead of discarded), fails the whole command with nothing written (exit1; error kind to settle:invalid_requestvs a new kind).What a success guarantees, and what is accepted. Success means the program had mouse reporting on for these events when the CLI looked, and the daemon admitted all of them. Not guaranteed, and accepted rather than designed out:
?1006) clamps a coordinate above 223 onto the edge cell; only a legacy program on a grid over 223 cells reaches it.Closing these would need a daemon-side per-event outcome on the fence, a wire change no consumer has needed.
Admission. No new mechanism. Each event reserves like today and the trailing
InputFenceconfirms the daemon admitted all of them. A press can still land without its release: when the fence times out because the child stopped reading stdin, when the session ends between the two, or when the press itself makes the program turn mouse reporting off (the accepted mode-change case above).Version skew. No wire change and no minor gate:
Input::Mousefrom a non-window attachment andModeFlags.mouse_protocolboth exist since minor 0.Not in scope. No bridge op:
sessions.senddoes not take--keyeither, and no bridge client has asked.Cascade
docs/reference/cli.md"Verb details"send;control-surfaces.md(sendsections, the reworded sentence, and the control-surface name map'ssend --keyentry gainssend --mouse).docs/reference/ipc.md"Input (kind = 1)": theMouse(MouseEvent)description (pixels are client-supplied only for a window; a non-window sender's0pixels are derived daemon-side), andInputFence: the fence confirms admission, not delivery to the PTY.docs/reference/protocols/vt-compliance.md"Mouse and focus" anddocs/reference/protocols/key-encoding.md:?1016reports from a non-window sender carry cell-center pixels, not the pointer's true position.skills/felis/SKILL.md: replace the "aim a mouse report (1-based)" paragraph with a--mouseexample fed fromsearch --source visible.CHANGELOG.mdentry.