vev-vt
A Go library for reading terminal output and keeping track of what should appear
on screen. It powers vev.
Feed it the bytes from a shell or command. It handles text, colors, cursor
movement, scrolling and resizing. You can then read the screen, save its history,
or use the ansi package to draw it in a terminal.
The html package prepares typed browser updates, and html/browser provides a
safe interactive DOM adapter without owning transport or terminal-input policy.
This library does not start processes or manage a PTY. Your application does
that and passes the output to vev-vt.
Install
Requires Go 1.27 or newer.
go get github.com/bnema/vev-vt
Alpha software: APIs and saved-history formats can change between versions.
The current history format does not read older VTH3 data.
Read terminal output
package main
import (
"fmt"
vt "github.com/bnema/vev-vt"
)
func main() {
screen := vt.NewScreen(80, 24)
screen.Write([]byte("Hello, \x1b[31mworld\x1b[0m!"))
// Colors and escape sequences are processed, not included in the text.
for x := 0; x < 13; x++ {
fmt.Printf("%c", screen.Cell(x, 0).Rune)
}
fmt.Println() // Hello, world!
}
Use screen.Resize(columns, rows) when the terminal size changes.
Use screen.Snapshot() when you need a copy that stays unchanged as new output
arrives.
Scrollback is the text that has moved above the visible screen.
// Your application chooses these limits; vev-vt has no default budget.
config := vt.HistoryConfig{
MaxBytes: 20_000_000, // At most 20 MB of history data.
MaxRows: 5_000, // Optional: also keep at most 5,000 lines.
}
screen := vt.NewScreenWithHistory(80, 24, config)
vev-vt enforces the limits your application supplies. The PTY transports bytes;
it does not store scrollback. The oldest lines are removed when a supplied limit
is reached. Limits apply to each
screen separately and exclude the visible screen. The byte limit measures
uncompressed history data, not total process memory.
History guide →: limit settings, saving/restoring history and
optional compression during idle time.
A few rules
- Serialize
Write, Resize, Snapshot, reads and history mutations in one
goroutine, or protect them with your own lock.
- Rows returned by the API are copies. Changing one does not change the screen.
- Use
vt.DefaultStyle() for terminal-default colors, not vt.Style{}.
- Callbacks run during
screen.Write; keep them short.
API ownership and styles →
Packages
| Package |
Use it for |
vev-vt |
Parse terminal output, read the screen and manage history. |
vev-vt/core |
Work with cells, styles and writable grids. |
vev-vt/ansi |
Render a screen or grid as ANSI terminal output. |
vev-vt/html |
Prepare transactional browser updates, structural CSS and terminal themes. |
vev-vt/html/browser |
Embed the DOM runtime and decode neutral browser events. |
vev-vt/graphics |
Read supported terminal images and their positions. |
Supported image features →
HTML frontend
The HTML renderer compares every row with its committed shadow; damage values are
non-authoritative hints. Prepare permits one outstanding draw and requires an
explicit Commit or Abort. Reset invalidates retained prepared draws.
Updates are immutable JSON-compatible values at schema version 2; browser
events share that version, so a host must serve the matching terminal.js.
Complete-row
replacement preserves wide-cell atomicity, and scroll damage uses a safe
snapshot fallback. Rows are encoded as text runs: adjacent printable ASCII
cells with one style form a single entry, while wide and non-ASCII characters
stay single entries so column alignment never depends on fallback fonts. html.DefaultLimits() documents the default 1,000,000-cell,
10,000-row, 64 MiB generated-update, and 65,536-style bounds.
The browser adapter builds DOM nodes with textContent and fixed classes. It
provides a labeled input proxy, synchronized plain-text accessible output,
typed CSS themes, IME-aware text input, keys, paste, pointer, wheel, resize, and
focus events. A synchronous consumer callback decides default prevention.
applyAll validates a queue of updates and merges them before touching the
DOM, so a consumer that falls behind rebuilds each changed row once. Text runs
assume a monospace primary font whose ASCII advance is 1ch, and
maxTextBytes bounds one run, so keep it at least as large as the widest row.
Consumers remain responsible for transport and mapping events to terminal bytes
or application actions. Clipboard text is preserved unchanged, including
control bytes, so consumers forwarding paste events to a PTY must apply their
required framing or filtering policy. browser.DefaultEventLimits() documents
the default 8 MiB event, 64 KiB text, 1 MiB paste, and 10,000×10,000 geometry
bounds.
renderer, err := html.New(html.Options{})
if err != nil {
return err
}
prepared, err := renderer.Prepare(frame, damage, reset, html.Cursor{
Row: cursorRow, Column: cursorColumn, Visible: true,
})
if err != nil {
return err
}
if err := send(prepared.JSON()); err != nil {
_ = prepared.Abort()
return err
}
return prepared.Commit()
Embed or serve html.Stylesheet() and browser.JavaScript() from a
consumer-owned application. The runtime supports a self-only style-src and
script-src CSP in the pinned browser harness; dynamic colors are set through
validated numeric DOM style properties. The current core model drops combining
marks and does not coalesce ZWJ sequences, so rendered output inherits those
limits even though browser IME input remains composition-aware.
Development
go test ./...
go test ./... -race
go vet ./...
npm ci
npm run test:browser:install
npm run test:browser
# Reproducible three-engine fallback on unsupported Linux hosts:
npm run test:browser:docker
TestDifferentialOracle replays seeded terminal workloads through the public
VT, history, graphics, ANSI and HTML APIs and compares one digest per layer with
testdata/oracle/golden.txt. Keep it unchanged for refactors and performance
work. For a deliberate behavior change, regenerate it on a full run with
go test -run TestDifferentialOracle -oracle.update . and explain the changed
columns. To locate a divergence, run -oracle.dump=DIR on both trees and diff
the scenario transcripts.
Playwright 1.62.1 and its Chromium, Firefox, and WebKit revisions are pinned by
package-lock.json. The matching Playwright container provides the reproducible
fallback when host libraries cannot run one of those browser builds. Production
Go packages retain the module's standard-library-only dependency boundary apart
from vev-vt/core.
Storage benchmarks and design decisions →
Measuring and profiling performance changes →
source
1# vev-vt
2
3A Go library for reading terminal output and keeping track of what should appear
4on screen. It powers [vev](https://github.com/bnema/vev).
5
6Feed it the bytes from a shell or command. It handles text, colors, cursor
7movement, scrolling and resizing. You can then read the screen, save its history,
8or use the `ansi` package to draw it in a terminal.
9The `html` package prepares typed browser updates, and `html/browser` provides a
10safe interactive DOM adapter without owning transport or terminal-input policy.
11
12This library does **not** start processes or manage a PTY. Your application does
13that and passes the output to vev-vt.
14
15## Install
16
17Requires Go 1.27 or newer.
18
19```sh
20go get github.com/bnema/vev-vt
21```
22
23**Alpha software:** APIs and saved-history formats can change between versions.
24The current history format does not read older VTH3 data.
25
26## Read terminal output
27
28```go
29package main
30
31import (
32 "fmt"
33
34 vt "github.com/bnema/vev-vt"
35)
36
37func main() {
38 screen := vt.NewScreen(80, 24)
39 screen.Write([]byte("Hello, \x1b[31mworld\x1b[0m!"))
40
41 // Colors and escape sequences are processed, not included in the text.
42 for x := 0; x < 13; x++ {
43 fmt.Printf("%c", screen.Cell(x, 0).Rune)
44 }
45 fmt.Println() // Hello, world!
46}
47```
48
49Use `screen.Resize(columns, rows)` when the terminal size changes.
50Use `screen.Snapshot()` when you need a copy that stays unchanged as new output
51arrives.
52
53## Keep scrollback
54
55Scrollback is the text that has moved above the visible screen.
56
57```go
58// Your application chooses these limits; vev-vt has no default budget.
59config := vt.HistoryConfig{
60 MaxBytes: 20_000_000, // At most 20 MB of history data.
61 MaxRows: 5_000, // Optional: also keep at most 5,000 lines.
62}
63screen := vt.NewScreenWithHistory(80, 24, config)
64```
65
66vev-vt enforces the limits your application supplies. The PTY transports bytes;
67it does not store scrollback. The oldest lines are removed when a supplied limit
68is reached. Limits apply to each
69screen separately and exclude the visible screen. The byte limit measures
70uncompressed history data, **not total process memory**.
71
72[History guide →](docs/history.md): limit settings, saving/restoring history and
73optional compression during idle time.
74
75## A few rules
76
77- Serialize `Write`, `Resize`, `Snapshot`, reads and history mutations in one
78 goroutine, or protect them with your own lock.
79- Rows returned by the API are copies. Changing one does not change the screen.
80- Use `vt.DefaultStyle()` for terminal-default colors, not `vt.Style{}`.
81- Callbacks run during `screen.Write`; keep them short.
82
83[API ownership and styles →](docs/ownership.md)
84
85## Packages
86
87| Package | Use it for |
88| --- | --- |
89| `vev-vt` | Parse terminal output, read the screen and manage history. |
90| `vev-vt/core` | Work with cells, styles and writable grids. |
91| `vev-vt/ansi` | Render a screen or grid as ANSI terminal output. |
92| `vev-vt/html` | Prepare transactional browser updates, structural CSS and terminal themes. |
93| `vev-vt/html/browser` | Embed the DOM runtime and decode neutral browser events. |
94| `vev-vt/graphics` | Read supported terminal images and their positions. |
95
96[Supported image features →](docs/graphics.md)
97
98## HTML frontend
99
100The HTML renderer compares every row with its committed shadow; damage values are
101non-authoritative hints. `Prepare` permits one outstanding draw and requires an
102explicit `Commit` or `Abort`. `Reset` invalidates retained prepared draws.
103Updates are immutable JSON-compatible values at schema version 2; browser
104events share that version, so a host must serve the matching `terminal.js`.
105Complete-row
106replacement preserves wide-cell atomicity, and scroll damage uses a safe
107snapshot fallback. Rows are encoded as text runs: adjacent printable ASCII
108cells with one style form a single entry, while wide and non-ASCII characters
109stay single entries so column alignment never depends on fallback fonts. `html.DefaultLimits()` documents the default 1,000,000-cell,
11010,000-row, 64 MiB generated-update, and 65,536-style bounds.
111
112The browser adapter builds DOM nodes with `textContent` and fixed classes. It
113provides a labeled input proxy, synchronized plain-text accessible output,
114typed CSS themes, IME-aware text input, keys, paste, pointer, wheel, resize, and
115focus events. A synchronous consumer callback decides default prevention.
116`applyAll` validates a queue of updates and merges them before touching the
117DOM, so a consumer that falls behind rebuilds each changed row once. Text runs
118assume a monospace primary font whose ASCII advance is `1ch`, and
119`maxTextBytes` bounds one run, so keep it at least as large as the widest row.
120Consumers remain responsible for transport and mapping events to terminal bytes
121or application actions. Clipboard text is preserved unchanged, including
122control bytes, so consumers forwarding paste events to a PTY must apply their
123required framing or filtering policy. `browser.DefaultEventLimits()` documents
124the default 8 MiB event, 64 KiB text, 1 MiB paste, and 10,000×10,000 geometry
125bounds.
126
127```go
128renderer, err := html.New(html.Options{})
129if err != nil {
130 return err
131}
132prepared, err := renderer.Prepare(frame, damage, reset, html.Cursor{
133 Row: cursorRow, Column: cursorColumn, Visible: true,
134})
135if err != nil {
136 return err
137}
138if err := send(prepared.JSON()); err != nil {
139 _ = prepared.Abort()
140 return err
141}
142return prepared.Commit()
143```
144
145Embed or serve `html.Stylesheet()` and `browser.JavaScript()` from a
146consumer-owned application. The runtime supports a self-only `style-src` and
147`script-src` CSP in the pinned browser harness; dynamic colors are set through
148validated numeric DOM style properties. The current core model drops combining
149marks and does not coalesce ZWJ sequences, so rendered output inherits those
150limits even though browser IME input remains composition-aware.
151
152## Development
153
154```sh
155go test ./...
156go test ./... -race
157go vet ./...
158npm ci
159npm run test:browser:install
160npm run test:browser
161# Reproducible three-engine fallback on unsupported Linux hosts:
162npm run test:browser:docker
163```
164
165`TestDifferentialOracle` replays seeded terminal workloads through the public
166VT, history, graphics, ANSI and HTML APIs and compares one digest per layer with
167`testdata/oracle/golden.txt`. Keep it unchanged for refactors and performance
168work. For a deliberate behavior change, regenerate it on a full run with
169`go test -run TestDifferentialOracle -oracle.update .` and explain the changed
170columns. To locate a divergence, run `-oracle.dump=DIR` on both trees and diff
171the scenario transcripts.
172
173Playwright 1.62.1 and its Chromium, Firefox, and WebKit revisions are pinned by
174`package-lock.json`. The matching Playwright container provides the reproducible
175fallback when host libraries cannot run one of those browser builds. Production
176Go packages retain the module's standard-library-only dependency boundary apart
177from `vev-vt/core`.
178
179[Storage benchmarks and design decisions →](docs/storage-optimization.md)
180
181[Measuring and profiling performance changes →](docs/performance.md)