Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

Terminal<B> owns a double-buffered Grid and the Backend lifecycle (resize, present, events). Drawing itself goes entirely through Surface, handed out by Terminal::draw/ Terminal::surface: a game calls term.draw(|s| { s.put(...); ... }) once per frame, and present diffs the current frame against the previous one, sending only changed cells to the Backend. B is the only thing that changes between a headless test and a real window or terminal:

              ┌───────────────────────────┐
              │      App::update(...)      │  game logic, once, generic over B
              └──────────────┬─────────────┘
                             │ term.draw(|s| ...): writes through Surface
                             ▼
              ┌───────────────────────────┐
              │       Terminal<B>          │  double-buffered Grid, cell diff
              └──────────────┬─────────────┘
                             │ draw / draw_layers / poll_event
                             ▼
              ┌───────────────────────────┐
              │  B: Output + Input + Cursor │  the only piece that swaps out
              └──────────────┬─────────────┘
                             │
       ┌─────────────────────┼─────────────────────┐
       ▼                     ▼                      ▼
 Headless (core)      Crossterm                SoftwareRenderer
 in-memory grid,      (retroglyph-crossterm)   (retroglyph-software)
 synthetic events     real TTY, ANSI output    winit window, pixels

Headless stores presented content in memory and lets tests inject synthetic Events with Headless::push_event; nothing there talks to a real terminal or window. Swapping Headless for Crossterm or SoftwareRenderer changes only the B type parameter: App implementations, Terminal calls, and game logic are unchanged. run_on drives Terminal<Headless> and Terminal<Crossterm> identically; the software backend’s windowed loop drives Terminal<SoftwareRenderer> through the same App contract, inverted because winit owns the event loop instead of handing control back to a driver function.

See examples/headless.rs (cargo run -p retroglyph-core --example headless) for the smallest possible use of Headless, depending on nothing but retroglyph-core.

Input and Output are independent facets

Input and Output are independent facets of Backend, which does not fit a window as one type: some event loop owns input, while a per-renderer surface owns output. WindowBackend (retroglyph-window) reunites the two, implementing Output by delegating to its wrapped Presenter, Input via its own event queue, and the no-op default Cursor, so Terminal gets the full Backend it needs while renderer crates (retroglyph-software, retroglyph-gl, retroglyph-wgpu) implement only Presenter.

Because WindowBackend owns input, a Presenter should not implement Input or Cursor itself for windowed use: those impls would be dead (the event loop pushes to this queue, not the presenter’s) and would silently miss the Mouse(Moved) coalescing that WindowBackend’s push_event applies. A presenter that also wants a direct headless Terminal<Self> input path (as retroglyph-software does for pixel tests) may still implement Input for that path, accepting that a bare queue does not coalesce; a presenter with no such path (as retroglyph-gl) implements only Presenter.

With the winit feature enabled, winit::run_windowed and winit::run_app own the event loop, call push_event as winit events are translated, and call Presenter::present once per frame; callers never touch WindowBackend directly. With winit disabled, retroglyph-window exports no event loop at all: a caller driving its own loop (SDL2, tao, a custom driver) constructs WindowBackend::new(presenter) itself, calls push_event for each translated input event, and calls Terminal::present (which drives Presenter::flush) plus presenter_mut().present() once per frame.

Presenting is automatic

winit::run_windowed and winit::run_app (and every _with_proxy/_on variant of either) call Terminal::present for you, once, right after the app’s per-frame closure or App::update returns: you no longer need to (and, for a stale-content bug fixed by this behavior, should not rely on remembering to) call it yourself. Calling it yourself is still supported and has no ill effect (the driver detects it already ran and skips its own call), for example if you also want to observe Terminal::present’s Result directly.

For the App-based drivers (run_app and friends), the automatic present is skipped entirely on Flow::Idle, leaving the previous frame on screen; every other Flow variant (including Continue) presents as usual.