Report the PTY's pixel extents in ?2048 and CSI 14/16 t #144

Merged
natsukium merged 3 commits from grid/report-pixel-extents into main 2026-10-14 08:19:42 +09:00
Owner

Every Textual app with mouse support crashed on its first click under felis. Textual turns on SGR-pixel mouse reporting (?1016) whenever ?2048 is supported, and converts each report back to cells by dividing by the pixel extents of the CSI 48 t notification, which felis sent as 0.

The line this draws: a program may learn the PTY's own geometry, pixels included, but not desktop facts. The PTY has one winsize, the size owner's, and TIOCGWINSZ already hands it to any program on the PTY, so restating it discloses nothing.

Commits

  1. daemon: create each session's PTY at its admitted geometry. Sessions were spawned at 24 × 80 with no pixels and resized afterwards, so a child reading TIOCGWINSZ early saw the default.
  2. grid, daemon, docs: report the PTY's pixel extents in the ?2048 notification (closes #143).
    • The grid holds the winsize pixels, which the daemon sets.
    • A change of the pixel extents alone also sends a report.
    • A session no window has sized gets a nominal 8 × 16 cell. tmux does the same at 16 × 32; 8 × 16 keeps a 2048-cell axis inside MAX_GRID_PIXELS.
    • The wire keeps 0 = unknown.
    • A later resize that leaves an axis unknown keeps the current cell.
    • Size-ownership promotion compares pixels too.
  3. grid, docs: answer CSI 14 t and 16 t from the PTY's winsize. CSI 13 t (window position) and 15 t (screen size) stay 0;0. REQ-900 and REQ-900a are adjusted, and the security model's CSI t classification is rewritten.

Verification

  • cargo nextest on felis-daemon and felis-grid: all pass. A cargo check of the whole workspace and clippy on both crates are clean.
  • End to end on an isolated daemon, with a headless Textual 8.2.8 app: an SGR-pixel click at the nominal cell center (45,25) maps to cell (5,1), the button fires, and the app does not crash.
  • External review: three rounds. The design review and the code review each came back BLOCKED once; the findings were the child's first TIOCGWINSZ racing the resize, and a zero or over-bound fill. The final round, covering design and code together, passed, and its two non-blocking findings are folded in.

Unblocks #131 (send --mouse).

Every Textual app with mouse support crashed on its first click under felis. Textual turns on SGR-pixel mouse reporting (`?1016`) whenever `?2048` is supported, and converts each report back to cells by dividing by the pixel extents of the `CSI 48 t` notification, which felis sent as `0`. The line this draws: a program may learn the PTY's own geometry, pixels included, but not desktop facts. The PTY has one winsize, the size owner's, and `TIOCGWINSZ` already hands it to any program on the PTY, so restating it discloses nothing. ## Commits 1. **daemon: create each session's PTY at its admitted geometry.** Sessions were spawned at 24 × 80 with no pixels and resized afterwards, so a child reading `TIOCGWINSZ` early saw the default. 2. **grid, daemon, docs: report the PTY's pixel extents in the `?2048` notification** (closes #143). - The grid holds the winsize pixels, which the daemon sets. - A change of the pixel extents alone also sends a report. - A session no window has sized gets a nominal 8 × 16 cell. tmux does the same at 16 × 32; 8 × 16 keeps a 2048-cell axis inside `MAX_GRID_PIXELS`. - The wire keeps `0` = unknown. - A later resize that leaves an axis unknown keeps the current cell. - Size-ownership promotion compares pixels too. 3. **grid, docs: answer `CSI 14 t` and `16 t` from the PTY's winsize.** `CSI 13 t` (window position) and `15 t` (screen size) stay `0;0`. REQ-900 and REQ-900a are adjusted, and the security model's `CSI t` classification is rewritten. ## Verification - `cargo nextest` on felis-daemon and felis-grid: all pass. A `cargo check` of the whole workspace and `clippy` on both crates are clean. - End to end on an isolated daemon, with a headless Textual 8.2.8 app: an SGR-pixel click at the nominal cell center (45,25) maps to cell (5,1), the button fires, and the app does not crash. - External review: three rounds. The design review and the code review each came back BLOCKED once; the findings were the child's first `TIOCGWINSZ` racing the resize, and a zero or over-bound fill. The final round, covering design and code together, passed, and its two non-blocking findings are folded in. Unblocks #131 (`send --mouse`).
A session was spawned at 24 x 80 with no pixels and resized to its
admitted geometry only afterwards, so a child that read TIOCGWINSZ
before the resize landed saw the default size. The PTY is now created
at the geometry, and spawn_session no longer resizes after the spawn.

Assisted-by: Claude Code
Textual turns on SGR-pixel mouse reporting whenever ?2048 is supported
and converts each report back to cells by dividing by the pixel extents
of the CSI 48 t notification. felis sent 0 there, so every Textual app
with mouse support died with ZeroDivisionError on its first click.

The notification now carries the PTY's own winsize pixels, which
TIOCGWINSZ already hands any program on the PTY, so it discloses
nothing new. The grid holds those extents (the daemon sets them), so
the dumped grid carries them across an in-place upgrade. A change of
the pixel extents alone reports too, or the program's ratio goes stale
after a font change.

A session no window has sized had 0 pixels in both places, so the
program-facing winsize fills an unknown axis at 8 x 16 per cell (tmux
does the same at 16 x 32; 8 x 16 keeps a 2048-cell axis inside
MAX_GRID_PIXELS), from the PTY's creation on. The wire keeps
0 = unknown. A later resize that leaves an axis unknown keeps the
current cell rather than the nominal one, which would re-resolve every
natural image extent, unless that cell floored to 0; the fill is capped
at MAX_GRID_PIXELS. Size-ownership promotion compares the filled
pixels, and announces the new owner's supplied ones when they fill to
the same extents.

Closes #143

Assisted-by: Claude Code
grid, docs: answer CSI 14 t and 16 t from the PTY's winsize
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 9s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 44s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m11s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 46s
pr / nix flake check (pull_request) Successful in 37s
windows / cargo nextest (Windows) (pull_request) Successful in 2m45s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m31s
pr / wire schema is compatible with the base (pull_request) Successful in 2s
windows / frontend smoke (Windows) (pull_request) Successful in 58s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 27s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
829497e0e8
CSI 14 t (the text area in pixels) and CSI 16 t (one cell in pixels)
answered 0;0 on the grounds that a pixel extent is one window's fact on
a grid every mirror shares. The PTY has one winsize, the size owner's,
and TIOCGWINSZ hands it to any program on the PTY, which the ?2048
report restates as well; answering the two queries from it discloses
nothing new, and image tools that probe the cell size with CSI 16 t stop
falling back. CSI 13 t and 15 t, the window's position and the screen's
size, stay stubbed: those are desktop facts.

Assisted-by: Claude Code
natsukium deleted branch grid/report-pixel-extents 2026-10-14 08:19:42 +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!144
No description provided.