vt: Textual apps crash on the first mouse event because ?2048 reports 0 pixel extents #143

Closed
opened 2026-10-13 23:10:46 +09:00 by natsukium · 0 comments
Owner

Problem

Every Textual app with mouse support crashes on its first mouse event under felis.

Textual turns on SGR-pixel mouse reporting (?1016) whenever the terminal answers ?2048 as supported (textual/drivers/linux_driver.py process_message, _enable_mouse_pixels). It then converts each report back to cells with the pixel extents from the CSI 48 ; rows ; cols ; height ; width t notification (textual/_xterm_parser.py parse_mouse_code: x /= pixel_width / width). felis sends 0 for both pixel axes (felis-grid resize_notify_report_bytes), so the division raises ZeroDivisionError and the app exits.

Reproduced on main (debug build, isolated daemon, Textual 8.2.8): a one-button Textual app spawned with felis sessions spawn, then one SGR click sent with felis sessions send --raw:

│ ❱ 103 │   │   │   │   x /= x_ratio                                           │
ZeroDivisionError: division by zero

Not run with a window, but a window's clicks take the same path: the client fills ?1016 reports with real pixels, while the ?2048 report is 0;0 whatever the window supplied, so the division is the same whoever sends the click.

The recorded decision and why it does not hold

docs/explanation/protocols/landscape.md "CSI" keeps the pixel axes at the 0 stub CSI 14 t answers "so the push never widens what a program can learn about the host". But the session's PTY already carries the window-supplied pixel extents in TIOCGWINSZ (ws_xpixel / ws_ypixel, set from the attach/resize dims in session_task.rs; vt-compliance.md's own table names TIOCGWINSZ as the path to a window's pixels). Reporting those same values in CSI 48 t tells a program nothing it cannot already read, so the rationale covers CSI 14 t (a pull of the host window's extents) but not ?2048.

Direction to settle

  • ?2048 reports the PTY's own ws_ypixel / ws_xpixel, so the notification and TIOCGWINSZ agree. CSI 14 / 15 / 16 t stay stubbed; the landscape.md paragraph is reworded to draw the line at "nothing beyond TIOCGWINSZ" rather than at "no pixels".
  • A pixel-only change then needs a report too (vt-compliance.md: "A resize that changes only the pixel extents reports nothing"), or Textual's ratio goes stale after a font-size change.
  • A session with no window has 0 pixel extents in both places, so a Textual app there still divides by zero on the first click. The daemon gives a windowless session a nominal cell size, applied to TIOCGWINSZ and the report alike: #131 (send --mouse) derives ?1016 pixels from this geometry and mostly drives headless sessions spawn sessions, so it depends on this. A window's first resize replaces the nominal size.
  • Independently, Textual's divide-by-zero is worth an upstream report: a terminal that reports 0 pixel extents should not crash the app.
## Problem Every Textual app with mouse support crashes on its first mouse event under felis. Textual turns on SGR-pixel mouse reporting (`?1016`) whenever the terminal answers `?2048` as supported (`textual/drivers/linux_driver.py` `process_message`, `_enable_mouse_pixels`). It then converts each report back to cells with the pixel extents from the `CSI 48 ; rows ; cols ; height ; width t` notification (`textual/_xterm_parser.py` `parse_mouse_code`: `x /= pixel_width / width`). felis sends `0` for both pixel axes (`felis-grid` `resize_notify_report_bytes`), so the division raises `ZeroDivisionError` and the app exits. Reproduced on `main` (debug build, isolated daemon, Textual 8.2.8): a one-button Textual app spawned with `felis sessions spawn`, then one SGR click sent with `felis sessions send --raw`: ``` │ ❱ 103 │ │ │ │ x /= x_ratio │ ZeroDivisionError: division by zero ``` Not run with a window, but a window's clicks take the same path: the client fills `?1016` reports with real pixels, while the `?2048` report is `0;0` whatever the window supplied, so the division is the same whoever sends the click. ## The recorded decision and why it does not hold `docs/explanation/protocols/landscape.md` "CSI" keeps the pixel axes at the `0` stub `CSI 14 t` answers "so the push never widens what a program can learn about the host". But the session's PTY already carries the window-supplied pixel extents in `TIOCGWINSZ` (`ws_xpixel` / `ws_ypixel`, set from the attach/resize dims in `session_task.rs`; vt-compliance.md's own table names `TIOCGWINSZ` as the path to a window's pixels). Reporting those same values in `CSI 48 t` tells a program nothing it cannot already read, so the rationale covers `CSI 14 t` (a pull of the host window's extents) but not `?2048`. ## Direction to settle - `?2048` reports the PTY's own `ws_ypixel` / `ws_xpixel`, so the notification and `TIOCGWINSZ` agree. `CSI 14 / 15 / 16 t` stay stubbed; the landscape.md paragraph is reworded to draw the line at "nothing beyond `TIOCGWINSZ`" rather than at "no pixels". - A pixel-only change then needs a report too (vt-compliance.md: "A resize that changes only the pixel extents reports nothing"), or Textual's ratio goes stale after a font-size change. - A session with no window has `0` pixel extents in both places, so a Textual app there still divides by zero on the first click. The daemon gives a windowless session a nominal cell size, applied to `TIOCGWINSZ` and the report alike: #131 (`send --mouse`) derives `?1016` pixels from this geometry and mostly drives headless `sessions spawn` sessions, so it depends on this. A window's first resize replaces the nominal size. - Independently, Textual's divide-by-zero is worth an upstream report: a terminal that reports `0` pixel extents should not crash the app.
Sign in to join this conversation.
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#143
No description provided.