PHPackages                             sugarcraft/candy-buffer - 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-buffer

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

sugarcraft/candy-buffer
=======================

Cell-grid value objects for terminal rendering — Buffer and Cell as immutable, fluent data model.

06↑2400%PHP

Since May 31Pushed 1w agoCompare

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

READMEChangelogDependenciesVersions (1)Used By (0)

candy-buffer
============

[](#candy-buffer)

Cell-grid value objects for terminal rendering — immutable `Buffer` and `Cell` types that form the foundation for all SugarCraft rendering components.

Overview
--------

[](#overview)

`candy-buffer` provides the core data model for terminal rendering:

- **`Buffer`** — a 2-D cell grid with immutable `with*()` mutation and bounds-checked access.
- **`Cell`** — a single terminal cell: rune, style, hyperlink, and display width.
- **`Position`** / **`Region`** — geometric primitives for buffer navigation and sub-region blitting.
- **`Style`** — minimal style record (fg colour, bg colour, attributes bitmask) for per-cell styling.
- **`Hyperlink`** — OSC 8 hyperlink anchor (url + id).

> **`Buffer::diff()`** produces minimal delta ANSI ops — see [`## Diffing & delta ANSI`](#diffing--delta-ansi) for details.

Install
-------

[](#install)

```
composer require sugarcraft/candy-buffer
```

Quickstart
----------

[](#quickstart)

```
use SugarCraft\Buffer\Buffer;
use SugarCraft\Buffer\Cell;
use SugarCraft\Buffer\Position;
use SugarCraft\Buffer\Region;
use SugarCraft\Buffer\Style;

// Create a 20x3 buffer filled with blank cells.
$buf = Buffer::new(20, 3);

// Set a cell at (col=5, row=1).
$buf = $buf->withCellAt(5, 1, Cell::new('H'));

// Blit a 2x2 sub-buffer at origin.
$sub = Buffer::new(2, 2);
$buf = $buf->withRegion(new Region(Position::new(0, 0), 2, 2), $sub);

// Read a cell back.
$cell = $buf->cellAt(5, 1);
echo $cell->rune(); // 'H'

// Access dimensions.
echo $buf->width();  // 20
echo $buf->height(); // 3
```

Wide characters
---------------

[](#wide-characters)

Wide characters (CJK, many emoji) have a display width of 2. The next cell in the grid is reserved as an empty "continuation" cell (rune `''`, width 0):

```
$cell = Cell::new('中'); // width=2, next cell must be a continuation
```

API
---

[](#api)

### Buffer

[](#buffer)

- `Buffer::new(int $width, int $height): self` — factory, creates a grid filled with blank cells.
- `Buffer::cellAt(int $col, int $row): Cell` — bounds-checked accessor; throws `\OutOfRangeException` on miss.
- `Buffer::withCellAt(int $col, int $row, Cell $cell): self` — immutable cell setter.
- `Buffer::withRegion(Region $region, Buffer $source): self` — blit `$source` into `$region`.
- `Buffer::width(): int`, `Buffer::height(): int`, `Buffer::region(): Region`.
- `Buffer::diff(Buffer $previous): list` — returns minimal delta ops (see [Diffing &amp; delta ANSI](#diffing--delta-ansi)).

### Cell

[](#cell)

- `Cell::new(string $rune = ' ', ?Style $style = null, ?Hyperlink $link = null, int $width = 1): self`
- `Cell::rune(): string`, `Cell::style(): ?Style`, `Cell::link(): ?Hyperlink`, `Cell::width(): int`

Diffing &amp; delta ANSI
------------------------

[](#diffing--delta-ansi)

`Buffer::diff(Buffer $previous)` compares two frames and returns a minimal list of `DiffOp` objects. Passing those ops through `DiffEncoder::encode()` produces a tight ANSI byte stream — no full repaint, just the cells that changed.

**DiffOp types**

OpANSIWhen used`MoveCursorOp`CUP `\x1b[row;colH`Reposition before a run of changes`SetStyleOp`SGR `\x1b[...m`Style transition (only when style differs)`SetCellOp`raw rune bytesOne or more consecutive changed cells`RepeatRunOp`REP `\x1b[N b`2+ identical adjacent cells with same style`EraseRunOp`ECH `\x1b[N X`Large regions cleared to blanks`SetHyperlinkOp`OSC 8Open/close OSC 8 hyperlink anchors**Worked example**

Two 5×2 buffers, before (all blanks) and after (styled 'A' at col 0, bold 'BBB' at col 1):

```
use SugarCraft\Buffer\Buffer;
use SugarCraft\Buffer\Cell;
use SugarCraft\Buffer\Style;
use SugarCraft\Buffer\Diff\DiffEncoder;

$prev = Buffer::new(5, 2);                           // all blanks
$curr = $prev
    ->withCellAt(0, 0, Cell::new('A', Style::fg(0xFF0000)))  // red A
    ->withCellAt(1, 0, Cell::new('B', Style::fg(0x0000FF)->withAttrs(Style::ATTR_BOLD)))  // blue bold B
    ->withCellAt(2, 0, Cell::new('B', Style::fg(0x0000FF)->withAttrs(Style::ATTR_BOLD)))
    ->withCellAt(3, 0, Cell::new('B', Style::fg(0x0000FF)->withAttrs(Style::ATTR_BOLD)));

$ops = $curr->diff($prev);
// [MoveCursorOp(0,0), SetStyleOp(red), SetCellOp([A]),
//  SetStyleOp(blue+bold), SetCellOp([B]), RepeatRunOp('B',2)]

$bytes = (new DiffEncoder())->encode($ops);
```

Emitted bytes (annotated):

```
\x1b[1;1H           # CUP → col 0, row 0  (from MoveCursorOp)
\x1b[38;2;255;0;0m  # SGR → red fg         (from SetStyleOp)
A                  # SetCellOp 'A'
\x1b[38;2;0;0;255;1m  # SGR → blue fg + bold  (style transition)
B                  # SetCellOp first 'B'
\x1b[2b            # REP → repeat 'B' 2×   (from RepeatRunOp)
\x1b[0m            # SGR reset             (DiffEncoder close)

```

Total: ~38 bytes vs ~95 bytes for a full 5×2 repaint. The diff is round-trip verified: `$curr->diff($prev)->apply($prev)` equals `$curr`.

Upstream
--------

[](#upstream)

Mirrors the Buffer/Cell data model from [charmbracelet/vte](https://github.com/charmbracelet/vte) and the terminal cell representation in [charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss).

License
-------

[](#license)

MIT

###  Health Score

22

—

LowBetter than 22% of packages

Maintenance64

Regular maintenance activity

Popularity6

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 (3 commits)")

### Embed Badge

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

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

###  Alternatives

[gabrielelana/byte-units

Library to parse, format and convert byte units

1752.3M20](/packages/gabrielelana-byte-units)

PHPackages © 2026

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