TermUI.Renderer.Diff (TermUI v0.2.0)

View Source

Differential rendering algorithm for terminal UI.

Compares current and previous buffers to produce minimal render operations. The algorithm identifies changed cells, groups them into spans, and generates operations for cursor movement, style changes, and text output.

Usage

operations = Diff.diff(current_buffer, previous_buffer)
# => [{:move, 1, 5}, {:style, style}, {:text, "Hello"}, ...]

Operation Types

  • {:move, row, col} - Move cursor to position
  • {:style, style} - Set text style (colors, attributes)
  • {:text, string} - Output text at current cursor position
  • :reset - Reset all style attributes

Algorithm

  1. Iterate rows in order (row-major for efficient terminal output)
  2. For each row, find spans of changed cells
  3. Optimize spans by merging small gaps
  4. Generate render operations for each span
  5. Track style to emit deltas only

Summary

Functions

Compares two buffers and returns a list of render operations.

Compares a single row and returns render operations for changed spans.

Finds spans of changed cells within a row.

Merges adjacent spans when the gap is smaller than cursor move cost.

Converts a span to render operations.

Checks if a cell contains a wide character (display width > 1).

Types

operation()

@type operation() ::
  {:move, pos_integer(), pos_integer()}
  | {:style, TermUI.Renderer.Style.t()}
  | {:text, String.t()}
  | :reset

span()

@type span() :: %{
  row: pos_integer(),
  start_col: pos_integer(),
  end_col: pos_integer(),
  cells: [TermUI.Renderer.Cell.t()]
}

Functions

diff(current, previous)

Compares two buffers and returns a list of render operations.

The current buffer contains the new frame to render, and the previous buffer contains the last rendered frame. Only differences are output.

Examples

{:ok, current} = Buffer.new(24, 80)
{:ok, previous} = Buffer.new(24, 80)
Buffer.write_string(current, 1, 1, "Hello")

operations = Diff.diff(current, previous)
# => [{:move, 1, 1}, {:style, %Style{}}, {:text, "Hello"}]

diff_row(current, previous, row, cols)

Compares a single row and returns render operations for changed spans.

find_changed_spans(current_cells, previous_cells, row)

@spec find_changed_spans(
  [{pos_integer(), TermUI.Renderer.Cell.t()}],
  [{pos_integer(), TermUI.Renderer.Cell.t()}],
  pos_integer()
) :: [span()]

Finds spans of changed cells within a row.

Returns a list of spans, where each span contains contiguous changed cells.

merge_spans(spans, current_cells_map)

@spec merge_spans([span()], map()) :: [span()]

Merges adjacent spans when the gap is smaller than cursor move cost.

This reduces cursor movements by including unchanged cells in the output when it's cheaper than moving the cursor around them.

The current_cells_map is used to fill gaps with actual cell content from the current buffer, rather than empty cells.

span_to_operations(map)

@spec span_to_operations(span()) :: [operation()]

Converts a span to render operations.

Generates move, style, and text operations for the span. Splits on style changes to minimize SGR sequence overhead.

wide_char?(cell)

@spec wide_char?(TermUI.Renderer.Cell.t()) :: boolean()

Checks if a cell contains a wide character (display width > 1).