PHPackages                             sugarcraft/candy-vt - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. sugarcraft/candy-vt

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

sugarcraft/candy-vt
===================

In-memory virtual terminal emulator — parses ANSI byte stream into a cell grid.

0494PHP

Since Jun 29Pushed 1mo agoCompare

[ Source](https://github.com/sugarcraft/candy-vt)[ Packagist](https://packagist.org/packages/sugarcraft/candy-vt)[ RSS](/packages/sugarcraft-candy-vt/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

[![candy-vt](.assets/icon.png)](.assets/icon.png)

CandyVt
=======

[](#candyvt)

[![CI](https://github.com/detain/sugarcraft/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/detain/sugarcraft/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/9a6bacca96b10df1eab2f88a93ac5cdd9f4e8912b8876ac8e4b4333690deb1c9/68747470733a2f2f636f6465636f762e696f2f67682f64657461696e2f737567617263726166742f6272616e63682f6d61737465722f67726170682f62616467652e7376673f666c61673d63616e64792d7674)](https://app.codecov.io/gh/detain/sugarcraft?flags%5B0%5D=candy-vt)[![Packagist Version](https://camo.githubusercontent.com/2fb1fe09ac97325a85817543a97e4ecd0eb31b5134219596618e6f4895417d90/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f737567617263726166742f63616e64792d76743f6c6162656c3d7061636b6167697374)](https://packagist.org/packages/sugarcraft/candy-vt)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/fd3accad83cd317a9843432403191b4e555c55d57d63e10897f3f1dff5ed169e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e332d3838393262662e737667)](https://www.php.net/)

In-memory virtual terminal emulator — parses an ANSI byte stream into a cell grid with cursor, mode, SGR style, and hyperlink state.

Mirrors [charmbracelet/x/vt](https://github.com/charmbracelet/x/tree/main/vt). Used as the terminal emulator behind [candy-vcr](../candy-vcr/)'s `render-tape` pipeline — every frame produced for a GIF is a `Snapshot::of($terminal, $time)` after the renderer feeds output bytes into a `Terminal` instance.

Contents
--------

[](#contents)

- [Install](#install)
- [Quickstart](#quickstart)
- [Architecture](#architecture)
- [Two Terminal classes](#two-terminal-classes)
- [Renderer value objects (vcr path)](#renderer-value-objects-vcr-path)
- [Parser state machine](#parser-state-machine)
- [CSI coverage table](#csi-coverage-table)
- [OSC coverage](#osc-coverage)
- [SGR attributes](#sgr-attributes)
- [Theme catalog](#theme-catalog)
- [Subsystems](#subsystems)
- [Development](#development)
- [Related](#related)

Install
-------

[](#install)

```
composer require sugarcraft/candy-vt
```

Quickstart
----------

[](#quickstart)

```
use SugarCraft\Vt\Terminal\Terminal;

$vt = Terminal::create(cols: 80, rows: 24);

// Feed raw ANSI bytes from a terminal program.
$vt->feed("\x1b[1;1H\x1b[31mmhello\x1b[0m");

// Inspect the rendered screen.
$screen = $vt->screen();
foreach ($screen->lines() as $row => $line) {
    echo "$row: $line\n";
}

// Current cursor position.
$cursor = $vt->cursor();
echo "cursor at {$cursor->row},{$cursor->col}\n";
```

Architecture
------------

[](#architecture)

LayerClassRoleFacade`Terminal\Terminal`Owns a Parser + ScreenHandler; `feed()` drives bytes inParser`Parser\Parser`VT500 state machine — Paul-Williams algorithm, handles partial input, parses subparameters (`:)Handler`Parser\CsiHandler`CSI sequence handler contract — Phase 1c implementation fills in CUP/SGR/ED/EL/DECSET/DECRST/DECSTBM/TBCHandler`Parser\OscHandler`OSC sequence handler contract — Phase 1c implementation fills in title/hyperlinkHandler`Handler\ScreenHandler`Dispatches parser actions to Buffer / Cursor / Sgr / ModeScreen`Screen\Screen`Immutable snapshot — read current grid after feeding bytesBuffer`Buffer\Buffer`Cell grid — `rows × cols` of styled grapheme cellsCursor`Cursor\Cursor`Position + visibility + origin mode trackingSGR`Sgr\Sgr`Current graphics rendition: foreground / background / attributesMode`Mode\Mode`DEC private mode flags (`DECSET`/`DECRST`) including DECAWM auto-wrapHyperlink`Hyperlink\Hyperlink`OSC 8 URL + id trackerScrollback`Screen\Scrollback`Ring buffer — stores rows that scroll off the top (default 1000)Msg`Msg\FocusInMsg / FocusOutMsg`Focus-in / focus-out event records (CSI I/O, mode 1004)Theme`Theme`256-color palette + default fg/bg + factory methods for named themesCatalog`Themes`Bundled theme catalog: `all()` and `v1()` accessorsTwo Terminal classes
--------------------

[](#two-terminal-classes)

candy-vt ships **two** Terminal entry-points for distinct use cases:

ClassUse caseMethods`SugarCraft\Vt\Terminal\Terminal`Full VT500 emulator — parses CSI/OSC/DCS, maintains Sgr/Mode/Hyperlink/Scrollback.`create(cols, rows, ?scrollbackSize)`, `feed(bytes)`, `flush()`, `screen()`, `cursor()`, `mode()`, `windowTitle()`, `palette()`, `clipboardEvents()`, `resize(cols, rows)`, `enableAltScreen()`/`disableAltScreen()`/`isAltScreen()`, plus `with*()` builders for Buffer/Cursor/Mode/WindowTitle/TabStops/ScrollbackSize.`SugarCraft\Vt\Terminal` (root)Lightweight emulator used by candy-vcr's renderer — produces `Snapshot` value objects directly.`new(cols, rows, ?Theme)`, `theme()`, `feed(bytes): self`, `snapshot(?time): Snapshot`, `cursor(): Cursor`, `grid(): CellGrid`, `windowTitle(): string`.```
use SugarCraft\Vt\Terminal;       // root — renderer path
use SugarCraft\Vt\Theme;

$vt = Terminal::new(cols: 80, rows: 24, theme: Theme::tokyoNight());
$vt = $vt->feed("\x1b[1;1H\x1b[31mhello\x1b[0m");
$snapshot = $vt->snapshot(time: 1.234);
```

The root `Terminal` returns a NEW instance from `feed()` (immutable fluent style) and uses the `HandlerAdapter` + `CsiHandlerImpl` + `OscHandlerImpl` triple internally. The full `Terminal\Terminal`mutates in place (`feed(): void`) — it's optimised for byte-stream ingest rather than per-frame snapshots.

### Renderer value objects (vcr path)

[](#renderer-value-objects-vcr-path)

The `SugarCraft\Vt` root namespace provides simplified value objects for the candy-vcr VHS renderer path — independent of the full VT parser stack:

```
use SugarCraft\Vt\Cell;
use SugarCraft\Vt\CellGrid;
use SugarCraft\Vt\Cursor;

// Cell — char + fg (0-255) + bg (0-255) + attrs bitfield
$cell = new Cell(char: 'X', fg: 196, bg: 21, attrs: Cell::ATTR_BOLD);
$cell = $cell->withFg(34);            // green foreground
$cell = $cell->withBg(226);           // yellow background
$cell = $cell->withAttrs(Cell::ATTR_ITALIC | Cell::ATTR_UNDERLINE);

// CellGrid — 2D grid with dirty-region tracking
$grid = new CellGrid(cols: 80, rows: 24);
$grid = $grid->set(0, 0, new Cell(char: 'H'));
$grid = $grid->set(0, 1, new Cell(char: 'i'));
echo $grid->get(0, 0)->char;          // 'H'
echo implode(',', $grid->dirtyRegion()); // minRow, maxRow, minCol, maxCol
$grid = $grid->clear();               // resets dirtyRegion
$grid = $grid->resize(100, 40);     // grow/shrink preserving content

// Cursor — row + col + shape + visibility
$cursor = new Cursor(row: 0, col: 0, shape: 0, visible: true);
$cursor = $cursor->at(5, 10);        // move to row 5, col 10
$cursor = $cursor->withShape(2);      // shape 2 = pipe
$cursor = $cursor->hidden();           // hide cursor
$cursor = $cursor->shown();          // show cursor
```

### Parser handlers

[](#parser-handlers)

Each handler translates parser actions into handler state mutations:

- `CursorHandler` — CUP, HVP, CUU/CUD/CUF/CUB, CR, LF/VT/FF, BS, RI, HOME, DECSC/USC
- `SgrHandler` — SGR sequences (colour + attributes)
- `EraseHandler` — ED, EL, ECH, DECSCA
- `ScrollHandler` — SU, SD, DECSTBM
- `ModeHandler` — DECSET/DECRST/DECMODESET/DECMODERST (includes DECAWM mode 7)
- `TabHandler` — TBC, HTS
- `OscHandler` — OSC window title, hyperlink, colour palette
- `ScreenHandler` — orchestrates all of the above; owns the Buffer

### Snapshot

[](#snapshot)

`SugarCraft\Vt\Snapshot` is an immutable point-in-time grid + cursor:

```
use SugarCraft\Vt\Snapshot;

$snap = Snapshot::of($terminal, time: 1.234);  // captures grid + cursor
$snap->grid;     // CellGrid
$snap->cursor;   // Cursor
$snap->time;     // float — virtual playback time

$snap->equals($other);          // grid + cursor only (used by FrameDedup)
$snap->equalsWithTime($other);  // grid + cursor + time (strict reproducibility)
```

`Snapshot::equals()` is the dedup signal used by candy-vcr's `FrameDedup` to collapse visually identical adjacent frames before GIF encoding.

Parser state machine
--------------------

[](#parser-state-machine)

`Parser\Parser` is a Paul Williams VT500 state machine ([reference](https://vt100.net/emu/dec_ansi_parser)) that drives a `Parser\Handler` interface with parsed actions.

ComponentRole`Parser\State`Enum of parser states: `Ground`, `Escape`, `CsiEntry`, `CsiParam`, `CsiIntermediate`, `CsiIgnore`, `DcsEntry`, `OscString`, etc.`Parser\Action`Enum of dispatch actions: `Print`, `Execute`, `CsiDispatch`, `EscDispatch`, `OscDispatch`, `DcsDispatch`, `Hook`, `Put`, `Unhook`.`Parser\Transitions`Static transition table — `(state, byte) → (action, next_state)`.`Parser\Handler`The contract the parser drives — implementations: `HandlerAdapter` (wraps CSI + OSC sub-handlers), `DebugHandler` (dumps every action).`Parser\CsiHandler`CSI dispatch contract — `printable`, `cuu`/`cud`/`cuf`/`cub`, `cup`, `sgr`, `ed`/`el`, `decset`/`decrst`, `decstbm`, `tbc`, `cht`/`cbt`, `gridRows`.`Parser\OscHandler`OSC dispatch contract — `title`, `hyperlink`.`Parser\CsiHandlerImpl`Default CSI handler used by the root `Terminal` renderer path.`Parser\OscHandlerImpl`Default OSC handler — tracks last window title.`Parser\HandlerAdapter`Glue that wires `CsiHandler` + `OscHandler` into a single `Handler` for the parser.Partial input is supported — feed any byte boundary and the parser state persists across `feed()` calls. Subparameter colons (`CSI 4:2 m` → double underline) are parsed natively.

CSI coverage table
------------------

[](#csi-coverage-table)

The default `CsiHandlerImpl` (used by `SugarCraft\Vt\Terminal`) implements the renderer-relevant subset of CSI dispatches. The full `Handler\ScreenHandler` (used by `SugarCraft\Vt\Terminal\Terminal`) implements the superset.

Final byteNameMethodBehavior`@`ICH(Handler)Insert N blank characters at cursor.`A`CUU`cuu($n=1)`Cursor up N, clamped to scroll region top.`B`CUD`cud($n=1)`Cursor down N, clamped to scroll region bottom.`C`CUF`cuf($n=1)`Cursor right N, clamped to right margin.`D`CUB`cub($n=1)`Cursor left N, clamped to column 0.`E`CNL(Handler)Cursor down N + column 0.`F`CPL(Handler)Cursor up N + column 0.`G`CHA(Handler)Cursor to column N.`H`CUP`cup($row, $col)`Move cursor to (row, col) 1-indexed.`I`CHT`cht($n=1)`Cursor forward N tab stops.`J`ED`ed($mode=0)`Erase display: 0=cursor→end, 1=begin→cursor, 2=all, 3=all+scrollback.`K`EL`el($mode=0)`Erase line: 0=cursor→eol, 1=bol→cursor, 2=full line.`L`IL(Handler)Insert N blank lines at cursor.`M`DL(Handler)Delete N lines at cursor.`P`DCH(Handler)Delete N characters at cursor.`S`SU(Handler)Scroll up N lines.`T`SD(Handler)Scroll down N lines.`X`ECH(Handler)Erase N characters from cursor (preserves BCE bg).`Z`CBT`cbt($n=1)`Cursor backward N tab stops.`d`VPA(Handler)Cursor to row N.`f`HVP`hvp($row, $col)`Same as CUP — move cursor to (row, col).`g`TBC`tbc($mode=0)`Tab clear: 0=column, 3=all.`h`SM/DECSET`decset($mode, $prefix)`Set mode. `?` prefix → private. `?7 h` enables DECAWM, `?12 h` BCE, `?25 h` cursor visible, `?47/?1047/?1049 h` alt screen, `?1004 h` focus events, `?2026 h` sync-update, `?6 h` DECOM.`l`RM/DECRST`decrst($mode, $prefix)`Reset mode (mirror of `h`).`m`SGR`sgr($params)`Set graphic rendition. Subparam colons (`4:1`..`4:5`) parsed.`r`DECSTBM`decstbm($top, $bottom)`Set scroll region rows (1-indexed).`s`DECSC(Handler)Save cursor + Sgr + origin mode.`u`DECRC(Handler)Restore cursor + Sgr + origin mode.` SP q`DECSCUSR(Handler)Cursor shape 0–6 (block/underline/bar × blink).` I/O`Focus(Handler)Focus-in / focus-out report (under `?1004 h`).`(Handler)` rows live in the full `Handler/ScreenHandler` dispatcher used by `SugarCraft\Vt\Terminal\Terminal`. The renderer path (`SugarCraft\Vt\Terminal`) implements the rows with an explicit `Method` column — the rest of the CSI sequences are ignored when the renderer encounters them (they're already simplified-away by upstream applications writing rendered output).

The `printable` and `execute` paths also route through the same adapter — printable bytes call `CsiHandlerImpl::printable()` (writes the cell + advances cursor + auto-wraps), and C0 controls (`\b \t \n \r`) call the corresponding cursor handler.

OSC coverage
------------

[](#osc-coverage)

The default `OscHandlerImpl` (renderer path) implements:

SequenceMethodBehavior`OSC 0; BEL/ST``title($title)`Set window title (and icon name).`OSC 2; BEL/ST``title($title)`Set window title only.`OSC 8;; BEL/ST``hyperlink($uri, $id)`Begin/end hyperlink (OSC 8). Empty URI ends the link.`lastTitle()`accessorMost recent title set during the stream.The full `Handler\OscHandler` (used by `SugarCraft\Vt\Terminal\Terminal`) adds palette query/set (`OSC 4`, `OSC 10`, `OSC 11`), clipboard get/set (`OSC 52`), and forwards focus / mode-query events to `ScreenHandler::$clipboardEvents` / `$focusEvents`.

SGR attributes
--------------

[](#sgr-attributes)

`Sgr\Sgr` tracks the current pen state. Supported attributes:

SGR paramEffect`0`Reset all attributes.`1`Bold.`2`Faint.`3`Italic.`4` (or `4:1`)Single underline.`4:2` / `4:3` / `4:4` / `4:5`Double / Curly / Dotted / Dashed underline.`7`Inverse (swap fg/bg).`9`Strikethrough.`21` / `22`Reset bold + faint.`23` / `24` / `27` / `29`Reset italic / underline / inverse / strikethrough.`30..37` / `90..97`Set 8 + 8 bright ANSI foreground.`38;5;` / `38;2;;;`256-color / truecolor foreground.`40..47` / `100..107`Set 8 + 8 bright ANSI background.`48;5;` / `48;2;;;`256-color / truecolor background.`39` / `49`Reset fg / bg to default.Attribute bitfield mirrored on `Cell::ATTR_*` constants: `ATTR_BOLD`, `ATTR_ITALIC`, `ATTR_UNDERLINE`, `ATTR_INVERSE`, `ATTR_STRIKETHROUGH`.

Background Color Erase (BCE)
----------------------------

[](#background-color-erase-bce)

When BCE mode is enabled (`CSI ?12 h`), erase operations (`CSI K`, `CSI J`, `CSI X`) fill cleared cells with the current SGR background color instead of a plain blank cell. This prevents "ghosting" in terminals with colored backgrounds — the erased area retains its visual background rather than going black.

```
$vt = Terminal::create(cols: 80, rows: 24);
$vt->feed("\x1b[48;5;196m\x1b[?12h");  // red background + BCE on
$vt->feed("\x1b[2J");                   // erase screen — cells carry red bg
echo $vt->screen()->cell(0, 0)->background()?->toInt(); // 196
```

BCE state is tracked via the `Sgr` object's background color — the pen must be set before the erase sequence for the background to carry through.

Diff API
--------

[](#diff-api)

After feeding bytes, snapshot the screen and compare:

```
$before = $vt->screen();
$vt->feed($moreBytes);
$after  = $vt->screen();

foreach ($before->diff($after) as $change) {
    [$row, $col, $prev, $next] = $change;
    echo "{$row},{$col}: '{$prev->grapheme}' → '{$next->grapheme}'\n";
}
```

Auto-wrap (DECAWM)
------------------

[](#auto-wrap-decawm)

DECAWM (`CSI ? 7 h` / `CSI ? 7 l`) controls whether the cursor automatically wraps to the next line when a character is printed at the rightmost column. The `Mode` object exposes this as:

```
// Query the current state.
$mode = $vt->mode();
var_dump($mode->autoWrap);  // bool

// Build a new mode with auto-wrap forced on/off.
$wrapped = $mode->withAutoWrap(true);
```

When auto-wrap is **off** (the default), characters written at the rightmost column are silently discarded. When **on**, the cursor moves to column 0 of the next row before writing — and if that row is within a scroll region the region scrolls up by one line, matching VT100 behavior.

Scroll regions (DECSTBM, `CSI r`) and auto-wrap interact correctly: wrapping at the bottom of a scroll region triggers a scroll within that region, not on the whole buffer.

SGR underline styles
--------------------

[](#sgr-underline-styles)

SGR `CSI 4:N` controls the underline style. The `Sgr` object exposes this as an `UnderlineStyle` enum and a fluent setter:

```
use SugarCraft\Vt\Sgr\Sgr;
use SugarCraft\Vt\Sgr\UnderlineStyle;

// Build an Sgr state with a specific underline style.
$sgr = Sgr::empty()->withUnderlineStyle(UnderlineStyle::Double);
echo $sgr->underline;           // true
echo $sgr->underlineStyle->value; // 2 (Double)
```

`UnderlineStyle` values map to CSI sequences as follows:

Enum valueCSI sequenceDescription`None``CSI 4:0 m`No underline`Single``CSI 4:1 m`Single underline`Double``CSI 4:2 m`Double underline`Curly``CSI 4:3 m`Curly underline`Dotted``CSI 4:4 m`Dotted underline`Dashed``CSI 4:5 m`Dashed underline`CSI 24 m` clears underline (any style). Plain `CSI 4 m` (no subparam) is equivalent to `4:1` (single).

Scrollback buffer
-----------------

[](#scrollback-buffer)

When the screen scrolls (LF at the bottom, DECSTBM scroll region, or auto-wrap at the bottom of a region), rows pushed off the top are stored in a ring-buffer `Scrollback`. Access it via:

```
$screen = $vt->screen();
$scrollback = $screen->scrollback();

foreach ($scrollback->all() as $rowIndex => $row) {
    // $row is array
    echo "scrollback row $rowIndex\n";
}
```

The default capacity is 1000 rows. Configure it at construction or retrospectively:

```
// Set at construction.
$vt = Terminal::create(cols: 80, rows: 24, scrollbackSize: 5000);

// Retrospective change — existing scrollback is replaced.
$vt = $vt->withScrollbackSize(5000);
```

Available `Scrollback` accessors:

```
$scrollback->count()    // int — rows currently stored
$scrollback->maxSize()  // int — configured capacity
$scrollback->at($n)     // array|null — row offset N from oldest
$scrollback->all()       // array — all rows oldest→newest
```

The scrollback is consumed by `ScreenHandler`'s scroll-up and scroll-down operations (SU/SD/IND/RI/NEL). Erase-of-scrollback (`CSI 3 J`) clears the ring buffer.

Origin mode (DECOM)
-------------------

[](#origin-mode-decom)

DECOM (`CSI ?6 h` / `CSI ?6 l`) controls whether cursor addressing is relative to the absolute screen origin or to the scroll region defined by DECSTBM. The `Mode` object exposes this as:

```
$mode = $vt->mode();
var_dump($mode->originMode);  // bool

$origin = $mode->withOriginMode(true);
```

When origin mode is **on**, the cursor is constrained to the scroll region and cursor-addressing commands (`CSI row;col H`, `CSI row;col f`) treat `(1,1)` as the top-left of the DECSTBM region. When **off** (the default), `(1,1)` is the absolute top-left of the screen. DECSTBM must be reset when leaving origin mode — both happen together via `CSI ?6 l`.

Cursor shape (DECSCUSR)
-----------------------

[](#cursor-shape-decscusr)

DECSCUSR (`CSI Ps SP q`) sets the terminal cursor shape. The `Mode` object stores this as an integer and exposes it via the `CursorShape` enum:

```
use SugarCraft\Vt\Mode\Mode;
use SugarCraft\Vt\CursorShape;

$mode = $vt->mode();
var_dump($mode->cursorShape);  // int (0–6)

// Build a new mode with a specific cursor shape.
$bar = $mode->withCursorShape(CursorShape::SteadyBar->toInt());
```

`CursorShape` values map to VT sequences:

Enum valueValueCSI sequenceDescription`BlinkingBlock`0/1`CSI 0 SP q`Blinking block`SteadyBlock`2`CSI 2 SP q`Steady block`BlinkingUnderline`3`CSI 3 SP q`Blinking underline`SteadyUnderline`4`CSI 4 SP q`Steady underline`BlinkingBar`5`CSI 5 SP q`Blinking vertical bar`SteadyBar`6`CSI 6 SP q`Steady vertical bar`CursorShape::fromInt()` normalises both `0` and `1` to `BlinkingBlock`to match the VT spec.

Focus events (mode 1004)
------------------------

[](#focus-events-mode-1004)

When focus event reporting is enabled (`CSI ?1004 h` / `CSI ?1004 l`), the terminal records focus-in (`CSI I`) and focus-out (`CSI O`) events. The `Mode` object tracks the reporting flag:

```
$mode = $vt->mode();
var_dump($mode->reportFocusEvents);  // bool

$tracking = $mode->withReportFocusEvents(true);
```

Events are accumulated on the internal `ScreenHandler::$focusEvents`array as `FocusInMsg` / `FocusOutMsg` value objects. A consumer can inspect the array directly or wire the handler to dispatch events to an event loop.

Combining characters and wide characters
----------------------------------------

[](#combining-characters-and-wide-characters)

`ScreenHandler::printChar()` categorises every incoming rune by width:

- **Wide (width ≥ 2):** CJK and most emoji occupy 2 cells. A `Cell::continuation()`marker is written to the second cell and inherits SGR + hyperlink from the base cell. The cursor advances by the full width.
- **Normal (width 1):** Written to a single cell.
- **Combining mark (width 0):** Zero-width Unicode combining diacritical marks (U+0300–U+036F) are not silently discarded. Instead they are attached to the preceding cell via `Cell::withCombining()` so the base character and its combining marks render as a single composed grapheme in one column:

```
$vt->feed("\x1b[31me\xcc\x81");  // 'e' + combining acute accent → 'é'
$cell = $vt->screen()->cell(0, 0);
echo $cell->grapheme;   // 'e'
echo $cell->combining;  // "\xcc\x81" — attach to grapheme for full cluster
```

`Cell::$combining` is a plain string; `$cell->withCombining($mark)` appends to it. The `$combining` field is compared in `Cell::equals()` so snapshots that include composed characters compare correctly.

Combining marks that arrive at column 0 or immediately after a wide-char continuation cell are silently dropped — nothing in the preceding column to attach to.

Synchronized output (DEC 2026)
------------------------------

[](#synchronized-output-dec-2026)

DEC 2026 (`CSI ?2026 h` / `CSI ?2026 l`) enables **synchronized output mode**. While active, all buffer mutations (character writes, combining attachments, erase fills) are held in a queue on `ScreenHandler` instead of being applied immediately. When the mode is disabled (`CSI ?2026 l`), all queued mutations are replayed atomically. This prevents mid-sequence screen updates from being visible to the user, matching xterm's batched update behavior.

```
$mode = $vt->mode();
var_dump($mode->syncUpdate);  // bool

$vt->feed("\x1b[?2026h");    // enter sync-update mode
$vt->feed("\x1b[31mA\x1b[0m"); // queued, not yet visible
$vt->feed("\x1b[?2026l");    // flush → both cells appear together
```

Mutations are queued in `ScreenHandler::$pendingMutations` and flushed by `flushPendingMutations()` when sync mode is exited. BCE erase (CSI ?12 h) and combining-char attachments are both eligible for queued writes.

Theme catalog
-------------

[](#theme-catalog)

`Theme` provides a 256-color palette with factory methods for named themes. `Themes` is the catalog listing all available themes:

```
use SugarCraft\Vt\Theme;
use SugarCraft\Vt\Themes;

// Tokyo Night — the only theme used in the monorepo's 277 .tape files
$theme = Theme::tokyoNight();

// Index into the 256-color palette
$red = $theme->color(1);       // 0xf7768e

// Resolve RGB components from a 256-color index
$rgb = Theme::rgb(196);        // [255, 85, 85]

// Map ANSI 0-15 slot to its 256-color index
$idx = Theme::fgIndex(1);        // 1 (Ansi1 → index 1)

// V1-ready themes
foreach (Themes::v1() as $name => $theme) {
    echo "$name\n";
}
// TokyoNight, TokyoNightLight, TokyoNightStorm

// All themes (including deferred stubs)
foreach (Themes::all() as $name => $theme) {
    echo "$name\n";
}
// TokyoNight, TokyoNightLight, TokyoNightStorm, Dracula, SolarizedDark
```

Available themes:

ThemeStatusDescription`TokyoNight`✅ v1Dark theme — used in all 277 monorepo tapes`TokyoNightLight`✅ v1Light variant of TokyoNight`TokyoNightStorm`✅ v1Storm variant of TokyoNight`Dracula`✅ v1Full Dracula palette`SolarizedDark`✅ v1Full Solarized Dark paletteAttribute constants on `Theme` match `Cell::ATTR_*` for SGR bitfield construction: `Theme::ATTR_BOLD`, `Theme::ATTR_ITALIC`, `Theme::ATTR_UNDERLINE`, `Theme::ATTR_INVERSE`, `Theme::ATTR_STRIKETHROUGH`.

### Theme accessors

[](#theme-accessors)

MethodReturns`Theme::color($index)``int` — packed 24-bit RGB for palette slot 0..255. Falls back through `Theme::rgb()` for grayscale (232..255) when a slot is absent.`Theme::rgb($index)``array{int,int,int}` — `[R,G,B]` resolution for any 256-color index, including the 6x6x6 color cube (16..231) and the 24-step grayscale ramp (232..255).`Theme::fgIndex($slot)``int` — Map an ANSI 0..15 slot to its 256-color index (currently a no-op identity, kept for spec parity).`Theme::bgIndex($slot)``int` — Same for background slot.`Theme::defaultPalette()``array` — Bootstrap palette (xterm defaults) used by `Themes::ansi()`.`Theme::ANSI_OFFSET` / `Theme::CUBE_OFFSET` / `Theme::GRAYSCALE_OFFSET`int constants 0 / 16 / 232 — palette region boundaries.Subsystems
----------

[](#subsystems)

Each subdirectory of `src/` owns a small piece of VT state:

NamespacePurposePublic surface`Buffer\Buffer`Cell grid storage (`rows × cols` array of `Cell`).`cell($row, $col)`, `put($row, $col, $cell)`, `each(): Generator`, `copy(): array`, `resize($cols, $rows): self`.`Screen\Screen`Immutable snapshot of Buffer + Scrollback; cell-level diff API.`fromBuffer($buf, ?$scrollback)`, `lines()`, `cell($row, $col)`, `scrollback()`, `diff($other)`.`Screen\Scrollback`Ring buffer (default 1000 rows) for rows scrolled off the top.`count()`, `maxSize()`, `at($n)`, `all()`.`Cell\Cell`Cell-grid cell — grapheme + Sgr + Hyperlink + combining marks. Used by the full Terminal facade.`empty()`, `continuation($prev)`, `withCombining($s)`, `sgr()`, `foreground()`, `background()`, `equals()`.`Cursor\Cursor`Position + visibility + shape + saved-state.`withRow($r)`, `withCol($c)`, `withVisible($v)`, `withShape($s)`, `save()`, `restore()`, `equals()`.`Sgr\Sgr`Pen state (fg, bg, attrs bitfield, underline style).`empty()`, `withBold/Italic/Underline/Strikethrough/Blink/Reverse/Dim/Hidden($v)`, `withUnderlineStyle($style)`, `withForeground(?$color)`, `withBackground(?$color)`, `equals()`.`Sgr\UnderlineStyle`Enum: `None`/`Single`/`Double`/`Curly`/`Dotted`/`Dashed`.`fromInt()`, `value`.`Mode\Mode`DEC private modes.`withAltScreen/AltScreenVariant/CursorVisible/MouseSgr/MouseHighlights/MouseAny/MouseCellMotion/MouseExtended/BracketedPaste/SyncUpdate/AutoWrap/OriginMode/CursorShape/ReportFocusEvents`, `isAltScreen()`, `equals()`.`Hyperlink\Hyperlink`OSC 8 URI + id state.`fromRaw($id, $uri)`, `equals()`.`Msg\FocusInMsg` / `FocusOutMsg`Focus event records — accumulated on `ScreenHandler::$focusEvents` when DEC mode `1004` is set.Plain DTOs.`Color\Color`Color value object — palette index or truecolor RGB.`default()`, `indexed16($i)`, `indexed256($i)`, `truecolor($r,$g,$b)`, `fromInt($kind,$v)`, `red()`, `green()`, `blue()`, `equals()`.`CursorShape` (root enum)`BlinkingBlock` (0/1) / `SteadyBlock` (2) / `BlinkingUnderline` (3) / `SteadyUnderline` (4) / `BlinkingBar` (5) / `SteadyBar` (6).`fromInt()`, `toInt()`.Development
-----------

[](#development)

```
cd candy-vt && composer install
vendor/bin/phpunit                                              # test suite
vendor/bin/phpstan analyze                                      # static analysis (level: max)
vendor/bin/php-cs-fixer fix --config=../.php-cs-fixer.dist.php  # lint + auto-fix style
```

Snapshot tests
--------------

[](#snapshot-tests)

Render output is covered by golden-file snapshot tests. Fixture files live in `tests/fixtures/` with a `.golden` extension and are compared against actual ANSI byte output via `SugarCraft\Testing\Snapshot\Assertions::assertGoldenAnsi()`. To re-record fixtures after intentional output changes:

```
UPDATE_GOLDENS=1 vendor/bin/phpunit
```

Code style is enforced by `php-cs-fixer` via the root `.php-cs-fixer.dist.php` (PSR-12 + `declare_strict_types` + `strict_param` + short array syntax). Append `--dry-run --diff` to preview without writing.

Related
-------

[](#related)

- **[candy-vcr](../candy-vcr/)** — VHS-compatible cassette recorder / GIF renderer. Uses this lib's root `Terminal` + `Snapshot` to drive every rendered frame.
- [SugarCraft monorepo](https://github.com/detain/sugarcraft)
- Upstream: [charmbracelet/x/vt](https://github.com/charmbracelet/x/tree/main/vt)

###  Health Score

24

—

LowBetter than 31% of packages

Maintenance59

Moderate activity, may be stable

Popularity17

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/b1036e0717211b8030b83cbe729e8ba6ba442fdbd5285fb97a39d7dcfe339342?d=identicon)[detain](/maintainers/detain)

---

Top Contributors

[![detain](https://avatars.githubusercontent.com/u/1364504?v=4)](https://github.com/detain "detain (81 commits)")

### Embed Badge

![Health badge](/badges/sugarcraft-candy-vt/health.svg)

```
[![Health](https://phpackages.com/badges/sugarcraft-candy-vt/health.svg)](https://phpackages.com/packages/sugarcraft-candy-vt)
```

###  Alternatives

[monsoonconsulting/magento2-pwa

Adds a service worker to Magento2 to enable PWA features.

2323.3k](/packages/monsoonconsulting-magento2-pwa)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
