wijjit.terminal.screen_buffer.ScreenBuffer

class wijjit.terminal.screen_buffer.ScreenBuffer(width, height)[source]

2D buffer of terminal cells with dirty region tracking.

This class manages a 2D array of Cell objects representing the terminal screen. It tracks which regions have changed (dirty regions) to enable efficient diff rendering.

Parameters:
  • width (int) – Buffer width in columns

  • height (int) – Buffer height in rows

Variables:
  • width (int) – Buffer width

  • height (int) – Buffer height

  • cells (list of list of Cell) – 2D array of cells [row][col]

  • dirty_regions (set of tuple) – Set of (x, y, width, height) rectangles that have changed

Notes

Wide-character model. A width-2 glyph (most CJK, many emoji) is stored as two cells: a head cell holding the glyph and a continuation cell whose char is the empty string (see wijjit.terminal.cell.is_continuation()) carrying the head’s style. Zero-width combining marks (e.g. an NFD-decomposed accent) are folded onto the base glyph as a single multi-code-point char. The standard text path – wijjit.rendering.paint_context.PaintContext.write_text() plus the diff and full-render emitters here – is column-correct: it skips continuation cells and advances the diff cursor by each glyph’s true column width, so wide glyphs no longer overflow borders or desync the cursor.

Element rendering also goes through this path now: all elements paint via the clipped, wide-aware PaintContext write APIs (write_text, write_cell, write_cells/write_cells_vertical); direct buffer.set_cell loops were migrated out (review items 2.1/2.11) and a ratchet test keeps them out. Remaining gap (roadmap): wijjit.rendering.ansi_adapter.ansi_string_to_cells() – used for pre-rendered ANSI content such as Rich-rendered tables with content_type="ansi" – still maps one code point per cell.

Examples

Create a buffer and write a cell:

>>> buffer = ScreenBuffer(80, 24)
>>> buffer.set_cell(0, 0, Cell('A', fg_color=(255, 0, 0)))
>>> buffer.get_cell(0, 0).char
'A'

Track dirty regions:

>>> buffer.mark_dirty(0, 0, 10, 1)
>>> dirty = buffer.get_dirty_regions()
>>> (0, 0, 10, 1) in dirty
True
__init__(width, height)[source]
Parameters:
Return type:

None

Methods

__init__(width, height)

clear()

Clear the entire buffer to empty cells.

clear_dirty()

Clear all dirty region tracking.

copy_from(other)

Make this buffer's cells reference other's, row by row.

end_tracking()

Stop recording coverage and return the cells painted this frame.

fill_rect(x, y, width, height, cell)

Fill a rectangular region with the same cell.

get_cell(x, y)

Get the cell at the specified position.

get_dirty_regions()

Get all dirty regions.

get_merged_dirty_regions()

Get dirty regions collapsed to full-width rows.

mark_all_dirty()

Mark the entire buffer as dirty.

mark_dirty(x, y, width, height)

Mark a rectangular region as needing update.

peek_coverage()

Snapshot the cells painted so far this frame, without ending tracking.

record_coverage_rect(x, y, width, height)

Record a rectangular region as painted, without touching any cell.

reset()

Reset to a freshly-constructed state: all blank, no dirty regions.

resize(new_width, new_height)

Resize the buffer, preserving content where possible.

set_cell(x, y, cell)

Set a cell at the specified position and mark it dirty.

set_cells_horizontal(x, y, cells)

Set multiple cells horizontally in a single operation.

set_cells_vertical(x, y, cells)

Set multiple cells vertically in a single operation.

start_tracking(damage)

Begin recording paint coverage for one frame.

to_string()

Convert buffer to plain text string for debugging.

to_text()

Convert buffer to plain text (for testing/debugging).

Attributes

blank_cell

The shared blank cell (see module-level _BLANK_CELL).

blank_cell: Cell = Cell(char=' ', fg_color=None, bg_color=None, bold=False, italic=False, underline=False, reverse=False, dim=False, _style_mask=0)

The shared blank cell (see module-level _BLANK_CELL). Exposed on the instance so callers (e.g. the incremental renderer blanking a vacated cell) can reference it without importing the module constant.

start_tracking(damage)[source]

Begin recording paint coverage for one frame.

Parameters:

damage (bool) – When True, bulk writes also change-detect per cell (used on the incremental paint path where the buffer starts as a copy of the previous frame). When False, coverage is recorded but bulk writes keep their unconditional fast path (used to capture coverage on the full-repaint path without altering its output).

Return type:

None

end_tracking()[source]

Stop recording coverage and return the cells painted this frame.

Returns:

The positions touched by any write since start_tracking, each packed as y * width + x. Unpack with x = p % width and y = p // width.

Return type:

set of int

peek_coverage()[source]

Snapshot the cells painted so far this frame, without ending tracking.

Returns a copy so the caller can compare it against coverage recorded by later writes (e.g. the incremental path snapshots the frame-border pass’s coverage before the element loop, to keep elements that overlap a border from being skipped). Empty when tracking is not active.

Returns:

Positions painted since start_tracking, packed y * width + x.

Return type:

set of int

record_coverage_rect(x, y, width, height)[source]

Record a rectangular region as painted, without touching any cell.

Used by the incremental paint path when an unchanged element is skipped (its cells are already correct in the copied-in baseline): the element’s previously-painted region must still be counted as coverage so the vacated-cell blanking does not erase it. The rect is clipped to the buffer and packed as y * width + x ints, matching set_cell(). A no-op when coverage tracking is not active.

Parameters:
  • x (int) – Top-left corner of the region.

  • y (int) – Top-left corner of the region.

  • width (int) – Region size in cells.

  • height (int) – Region size in cells.

Return type:

None

reset()[source]

Reset to a freshly-constructed state: all blank, no dirty regions.

Unlike clear(), this does not mark the buffer dirty - it reproduces exactly what __init__ yields, so a pooled buffer can be reused for a full repaint without the differ treating every cell as changed.

Return type:

None

copy_from(other)[source]

Make this buffer’s cells reference other’s, row by row.

A shallow per-row copy: each row becomes a fresh list of references to other’s cell objects (cells are immutable and replaced wholesale, so sharing references is safe). Used by the incremental paint path to start a frame from the previous frame’s content, so that set_cell’s change-detection then dirties only the cells that actually change. Dirty regions and coverage are reset.

Parameters:

other (ScreenBuffer)

Return type:

None

set_cell(x, y, cell)[source]

Set a cell at the specified position and mark it dirty.

Parameters:
  • x (int) – Column position (0-indexed)

  • y (int) – Row position (0-indexed)

  • cell (Cell) – Cell to set

Return type:

None

Notes

Bounds checking is performed. Out-of-bounds coordinates are silently ignored. The cell region is automatically marked dirty if the cell content differs from the current cell.

set_cells_horizontal(x, y, cells)[source]

Set multiple cells horizontally in a single operation.

This is optimized for performance, avoiding individual cell comparisons and marking the entire region dirty once instead of cell-by-cell.

Parameters:
  • x (int) – Starting column position (0-indexed)

  • y (int) – Row position (0-indexed)

  • cells (list of Cell) – Cells to set horizontally

Return type:

None

Notes

Out-of-bounds cells are clipped. The entire region is marked dirty regardless of whether individual cells changed, which is more efficient than checking each cell when most cells are expected to change.

set_cells_vertical(x, y, cells)[source]

Set multiple cells vertically in a single operation.

This is optimized for performance, avoiding individual cell comparisons and marking the entire region dirty once instead of cell-by-cell.

Parameters:
  • x (int) – Column position (0-indexed)

  • y (int) – Starting row position (0-indexed)

  • cells (list of Cell) – Cells to set vertically

Return type:

None

Notes

Out-of-bounds cells are clipped. The entire region is marked dirty regardless of whether individual cells changed, which is more efficient than checking each cell when most cells are expected to change.

fill_rect(x, y, width, height, cell)[source]

Fill a rectangular region with the same cell.

This is highly optimized for filling regions with a single character, such as backgrounds or padding areas.

Parameters:
  • x (int) – Left edge of rectangle

  • y (int) – Top edge of rectangle

  • width (int) – Rectangle width

  • height (int) – Rectangle height

  • cell (Cell) – Cell to fill with

Return type:

None

Notes

The entire region is marked dirty once. This is much more efficient than individual set_cell() calls for filling large areas.

get_cell(x, y)[source]

Get the cell at the specified position.

Parameters:
  • x (int) – Column position (0-indexed)

  • y (int) – Row position (0-indexed)

Returns:

The cell at the position, or None if out of bounds

Return type:

Cell or None

mark_dirty(x, y, width, height)[source]

Mark a rectangular region as needing update.

Parameters:
  • x (int) – Left edge of rectangle

  • y (int) – Top edge of rectangle

  • width (int) – Rectangle width

  • height (int) – Rectangle height

Return type:

None

Notes

Dirty regions are used by the diff renderer to identify which parts of the screen need updating. Multiple overlapping regions are automatically merged during rendering.

mark_all_dirty()[source]

Mark the entire buffer as dirty.

Notes

Used when a full redraw is needed, such as after a terminal resize or when first rendering.

Return type:

None

get_dirty_regions()[source]

Get all dirty regions.

Returns:

Set of (x, y, width, height) tuples representing dirty rectangles

Return type:

set of tuple

get_merged_dirty_regions()[source]

Get dirty regions collapsed to full-width rows.

Returns:

One (x, y, width, height) tuple per dirty row: each spans the full buffer width with height 1, i.e. (0, row, width, 1).

Return type:

list of tuple

Notes

Rather than computing minimal bounding rectangles, every touched row is returned as a single full-width strip. This is cheaper to compute and, for the common case of scattered single-cell changes spread over a few rows, keeps the number of regions the diff renderer processes small.

clear_dirty()[source]

Clear all dirty region tracking.

Notes

Should be called after rendering is complete to reset dirty state for the next frame.

Return type:

None

clear()[source]

Clear the entire buffer to empty cells.

Notes

Fills all cells with the shared blank (space, no styling) - the same no-per-cell-allocation fill used at construction (see _BLANK_CELL) - and marks the entire buffer dirty.

Return type:

None

to_text()[source]

Convert buffer to plain text (for testing/debugging).

Returns:

Plain text representation with newlines

Return type:

str

Notes

This converts the cell buffer to plain text by extracting character data and joining with newlines. Useful for test assertions and debugging output.

Examples

>>> buffer = ScreenBuffer(10, 3)
>>> buffer.set_cell(0, 0, Cell('H'))
>>> buffer.set_cell(1, 0, Cell('i'))
>>> text = buffer.to_text()
>>> 'Hi' in text
True
resize(new_width, new_height)[source]

Resize the buffer, preserving content where possible.

Parameters:
  • new_width (int) – New width in columns

  • new_height (int) – New height in rows

Return type:

None

Notes

Content in the upper-left region that fits in both old and new sizes is preserved. New cells are filled with spaces. The entire buffer is marked dirty after resize.

to_string()[source]

Convert buffer to plain text string for debugging.

Returns:

Multi-line string representation of buffer content (no ANSI codes)

Return type:

str

Notes

This strips all styling and returns just the character content. Useful for debugging and testing.