Skip to content

Rebuild the interactive tui on Textual

The first forest tui drove its interactive loop with hand-rolled termios/tty code: cbreak mode, manual arrow-key escape parsing, an alternate-screen enter/restore dance, and a select-poll workaround for a real footgun — polars installs a SIGINT handler with SA_RESTART at import, so a blocking os.read never woke for Ctrl-C. That loop worked, but every new capability (mouse selection, a scrollable drill-down, resize handling) meant more bespoke terminal plumbing, and none of it was testable without a pty.

The interactive experience now runs on Textual (textual>=8, which keeps the project's Python 3.9 floor). The split that made the first version safe is preserved: scenery.py keeps the renderer-agnostic layers — gathering (local metadata only, never the network) and the canvas renderer — and the Textual app in tui_app.py is a thin shell around them. The scene widget draws the exact frames the canvas produces (so forest tui --once and piped output stay byte-identical and pipe-safe, with no Textual on that path), and the renderer additionally reports clickable hit regions so mouse clicks map to trees and canopy bands without duplicating drawing logic. Per-unit drill-down detail is gathered lazily — only when a stage is opened — keeping the scene overview cheap.

Consequences

  • textual and rich become runtime dependencies (rich is textual's own renderer, imported directly for ANSI-to-Text conversion); the dev extra gains pytest-asyncio to drive App.run_test()/Pilot tests headlessly.
  • The termios loop and the polars-SIGINT poll workaround are deleted: Textual's driver owns raw mode (Ctrl-C arrives as a key event, so the SA_RESTART handler is never in the read path) and restores the terminal even on crash.
  • Mouse selection, the stage drill-down screen, the bindings footer, and the command palette come from Textual; keyboard behavior is unchanged (arrows or h/l/j/k, r, q/Esc).
  • The interactive path is exercised by real key/click tests in CI instead of being untestable pty-only code.