Skip to content

Conversation

@Aaronontheweb
Copy link
Owner

Summary

  • Add double-buffering terminal wrapper (DiffingTerminal) that renders to a pending buffer and only outputs changed cells on flush
  • Eliminates the visual flicker caused by unconditional screen clears on every render cycle
  • Uses the same proven pattern as ncurses, termbox, and other TUI libraries

Changes

New components:

  • TerminalCell - Value type representing a single cell with character, colors, and text decoration
  • FrameBuffer - 2D buffer with efficient diffing via GetChangedRuns() that groups consecutive changes for minimal cursor movement
  • DiffingTerminal - IAnsiTerminal wrapper that intercepts all rendering calls and emits only changed cells

Key behavior:

  • ClearScreen() now only clears the pending buffer, not the actual terminal
  • Flush() diffs pending vs current buffer and outputs only differences
  • ForceFullRefresh() forces complete redraw (used on resize)
  • TerminaApplication auto-wraps terminals with DiffingTerminal

Test plan

  • All 561 existing tests pass
  • New unit tests for FrameBuffer (14 tests)
  • New unit tests for DiffingTerminal (21 tests)
  • Manual testing with SpinnerNode animations
  • Manual testing with TextInputNode typing
  • Manual testing with terminal resize

Add double-buffering terminal wrapper that renders to a pending buffer and
only outputs changed cells on flush, eliminating the visual flicker caused
by unconditional screen clears on every render cycle.

New components:
- TerminalCell: Value type representing a single cell with character, colors,
  and text decoration
- FrameBuffer: 2D buffer of TerminalCell with efficient diffing via
  GetChangedRuns() that groups consecutive changes for minimal cursor movement
- DiffingTerminal: IAnsiTerminal wrapper that intercepts all rendering calls,
  maintains current/pending frame buffers, and emits only changed cells

Key behavior changes:
- ClearScreen() now only clears the pending buffer, not the actual terminal
- Flush() diffs pending vs current buffer and outputs only differences
- ForceFullRefresh() forces complete redraw (used on resize or corruption)
- TerminaApplication auto-wraps terminals with DiffingTerminal (except
  VirtualTerminal for testing)

This is the same proven pattern used by ncurses, termbox, and other TUI
libraries to achieve flicker-free rendering.
Remove SetCursorVisible calls from FlushFull and FlushDiff methods that
were incorrectly toggling cursor visibility on every render. The application
should control cursor visibility, not the terminal wrapper.

This fixes a regression where multiple cursors would appear in the UI
because the flush methods were unconditionally showing the cursor after
each render cycle.
Skip resize when terminal reports zero width or height, which occurs in
headless CI environments. This prevents ArgumentOutOfRangeException when
FrameBuffer.Resize is called with invalid dimensions.
@Aaronontheweb Aaronontheweb merged commit ad22bf6 into dev Dec 16, 2025
7 checks passed
@Aaronontheweb Aaronontheweb deleted the feature/diff-based-rendering branch December 16, 2025 20:44
@Aaronontheweb Aaronontheweb mentioned this pull request Dec 16, 2025
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants