main ab6686c

Frontend-neutral VT terminal engine for Go.

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.

Keep scrollback

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 →

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close