Terminal widget implementation plan
Goal: build a Fennel terminal widget that renders libvterm output, handles input, and integrates with the existing layout/render systems.
Constraints
- Use the new C++ Terminal binding (rows/cols, dirty regions, cursor, title, bell, send_key/text/mouse, resize, update).
- Respect existing layout primitives and renderers; avoid re-dirtying layouts mid-pass.
- Favor monospace font and per-cell background + glyph rendering.
Current state (context for future work)
- Binding: C++
Terminal(PTY + libvterm) exposed via sol2; available via(require :terminal)and constructed withterminal.Terminal(...). - Scrollback/alt-screen: binding now stores scrollback with line/byte caps, exposes
get_scrollback_size/get_scrollback_line, reportsis_alt_screen, supportsset_scrollback_limit, and allowsinject_outputfor PTY-less tests. - Widget:
assets/lua/terminal.fnlbuilds a terminal entity with layout measurer/layouter,update/drop, optionsrows/cols/cell-size/on-bell/on-title/focusable?/focus-name, click-to-focus, focus-node creation, and pointer-target registration. Hookson_screen_updated/on_cursor_movedto invalidate rendering, routes input while focused throughInputStatetosend_text/send_key/send_mouse, and auto-subscribesupdatetoapp.engine.events.updated(disconnects on drop). Scrollback options now available:scrollback-lines(sets binding limit),enable-alt-screen?(default true),follow-tail?(default true), initialscroll_offset, andon-scrollcallback. Scroll offsets are clamped to 0 in alt-screen and snap back to tail on input when following. - Rendering: New
assets/lua/terminal-renderer.fnlpaints dirty cells into triangle/text buffers (background + glyph) with depth offsets and cursor overlay + blink timing. Uses dirty regions for partial redraws, full redraw on resize, and clears dirty regions post-paint. - Tests:
assets/lua/tests/test-terminal-widget.fnl(measure + resize + focus/input routing) andassets/lua/tests/test-terminal-renderer.fnl(dirty-region painting, cursor blink GL calls) included inassets/lua/tests/fast.fnlviamake test/./build/space -m tests.fast:main. - Behavior: Widget resizes terminal on layout size changes (using font-derived cell metrics unless overridden). Rendering and cursor visuals are in place; input plumbing is wired through focus + clickables and expects
ctx.clickablesto be present. When PTY creation fails, the binding exposesis_pty_available; the widget injects a placeholder banner, disables scrollback navigation, and continues rendering an empty grid so tests and UI stay stable.
Plan
- Core widget skeleton ✅
- Added
assets/lua/terminal.fnlbuilder that instantiatesTerminal, exposesupdate/drop, supportsrows/cols/cell-sizeoptions, and hooks bell/title callbacks.
- Layout + sizing ✅
- Measurer reports
rows*cell_w,cols*cell_h(defaults derived from theme font; can override viacell-size). - Layouter converts pixel size→rows/cols (floor), calls
term:resize, and writes layout size. - Tests cover measure/resize (
assets/lua/tests/test-terminal-widget.fnl).
- Rendering pipeline ✅
assets/lua/terminal-renderer.fnliterates dirty regions, writes per-cell background quads + glyphs via existing buffers, honorsreverse/bold(simulated), and clears dirty regions. Uses depth offsets and full redraw on resize/layout changes; wired viaon_screen_updated/on_cursor_moved.
- Cursor visuals ✅
- Renders a cursor overlay (block) with depth offset above glyphs, respects visibility, and blinks on a configurable period in Fennel.
Glyph styling ✅
- Bold/Italic: renderer swaps to theme-provided MSDF variants (regular/italic/bold/bold-italic) per cell, clears unused font buffers, and falls back to the regular font if a variant is missing.
- Underline: per-cell underline quad derived from font metrics/line height; rendered at
depth+1, honors reverse/fg colors, and uses the layout transform. - Tests: font-variant selection and underline geometry covered in
assets/lua/tests/test-terminal-renderer.fnlalongside dirty-region behavior using the OpenGL mock.
- Input plumbing ✅
- When widget focused, connect to
InputState(same pattern asassets/lua/input.fnl) sonormal-statehandlers short-circuit to the active terminal and returntrueto stop propagation. - Map SDL/Fennel events to the binding:
- Printable text →
term:send_text. - Special keys (arrows, home/end, page up/down, delete/backspace, tab, enter, function keys) →
term:send_key. - Mouse events: convert screen coords to cell row/col via layout position/size; call
term:send_mousewith button + pressed flag.
- Printable text →
- On blur (focus lost), disconnect from
InputStateand let normal-state fall back; ignore input when unfocused. - Implemented in
assets/lua/terminal.fnlwith focus listener + clickable registration; input dispatch routes text/keys/mouse to the binding. Tests cover focus connect/disconnect and event forwarding.
- Update loop ✅
- Widget
updatecallsterm:updateand is auto-connected toapp.engine.events.updatedso PTY output is pulled every frame; handler is dropped on widgetdrop. - On
on-title-changed, optionally propagate to window title; onon-bell, call supplied callback or play a sound. Currently only hooks the provided callbacks.
- Tests
- Added
assets/lua/tests/test-terminal-widget.fnl(measure + resize + focus/input dispatch + frame-update subscription). - Added
assets/lua/tests/test-terminal-renderer.fnlusing the OpenGL mock to cover dirty-region repaint and cursor blink visibility. - Input mapping covered by terminal widget test (focus, text/key/mouse send).
- Configuration and docs ✅
- Documented
SPACE_TERMINAL_PROGRAMdefault/override in README and below; the command is whitespace-split and defaults to/bin/sh. - Sandboxed/PTY-less environments now render a placeholder message and leave scrollback controls disabled instead of crashing.
- Misc
- How to best handle scrollback/alt-screen? (libvterm supports callbacks; we may need higher-level buffering.)
- Expose a palette map to avoid per-cell truecolor for performance.
Scrollback/alt-screen design
- Source of truth: keep history and alt-screen state in C++/libvterm; expose read-only APIs to Fennel to fetch lines by offset and query
in_alt_screen?. - Limits: configurable
scrollback-lines(default 5k–10k) plus a byte cap (8–16 MB) enforced in the binding; let libvterm handle resize reflow. - Alt-screen semantics: match traditional behavior—alt-screen has no scrollback; main scrollback is preserved but hidden while in alt-screen.
- Rendering: renderer paints the live screen; when
scroll_offset> 0 (and not in alt-screen) it renders from history with that offset and freezes follow-tail until offset returns to 0. - Input/navigation: wheel/PageUp/PageDown adjust
scroll_offset; any typing or explicit follow-tail toggle snaps back to 0. Disable scrollback navigation in alt-screen. Optionalon-scrollcallback. - APIs: widget options
scrollback-lines,enable-alt-screen?(default true),follow-tail?(default true),on-scroll. Binding additions: history accessors,is_alt_screen,get_scrollback_size. - Performance: introduce a palette map for scrollback rendering to reduce per-cell truecolor, and avoid re-dirtying layouts when only
scroll_offsetchanges. - Fallback: if PTY/libvterm unavailable, render empty grid with a status message and disable scrollback/alt-screen controls.
Configuration
- Runtime shell: defaults to
/bin/sh; override withSPACE_TERMINAL_PROGRAM(split on whitespace) such asSPACE_TERMINAL_PROGRAM="bash -l". - Sandboxes: when PTY creation fails, the widget injects a placeholder banner into the terminal buffer and keeps scrollback navigation disabled while still rendering the grid.
Implementation plan
- C++ binding ✅: scrollback storage/limits, alt-screen flag, accessors for current screen and history lines,
get_scrollback_size,set_scrollback_limit, andinject_outputfor PTY-less tests. - Widget state/options ✅: add
scrollback-lines,enable-alt-screen?,follow-tail?,scroll_offset,on-scroll; snap offset to 0 on input when follow-tail is true; block scrollback navigation in alt-screen. - Renderer ✅: support rendering with
scroll_offset(history rows) while reusing existing dirty-region logic; add palette map for color lookup to reduce per-cell truecolor. - Input wiring ✅: map wheel/PageUp/PageDown/Shift+Wheel to offset adjustments; add follow-tail toggle; disable scrollback navigation in alt-screen; call
on-scrollwhen offset changes. - Tests ✅: C++ unit tests for limits/history; Fennel tests for scrollback navigation, alt-screen blocking, renderer offset handling, and palette map behavior.
