Skip to content

Custom TUI Engine

Panix uses a custom-built terminal rendering engine, zeroterm (the pkg/tui and pkg/buffer packages), instead of existing TUI frameworks. The engine is designed for speed and near zero-allocation rendering.

The four-phase journey that produced it: Performance Optimization.

Bubble Tea re-renders entire views every frame through string intermediates, producing thousands of short-lived allocations per frame and heavy GC pressure. Panix renders a tree of nested viewports that shrink when commands finish, with dozens of machines updating at once, so full-frame re-rendering is wasted work at fleet scale. zeroterm bounds rendering work and terminal output by the changed lines, not the frame size.

  • Contiguous line buffer (LinesBuf): one []byte with []int line offsets; Line(i) is zero-copy and bulk ops (AppendFrom, WritePaddedView) copy visible regions in a single append. Pooled via sync.Pool, reset with buf[:0] retaining capacity.
  • Versioned buffer (LinesBufVer): thread-safe LinesBuf wrapper with a version counter for cache invalidation; Snapshot() provides consistent copies.
  • In-place styling (AppendStyledLine/AppendStyledPad): writes prefix + content + reset directly into the caller’s buffer using pre-rendered ANSI prefixes and a padding pool.
  • Double-buffered frame diffing: two LinesBufDiff buffers alternate as current/previous; Diff(prev) returns changed line indices via bytes.Equal, unchanged lines produce zero output.
  • Incremental caching: tables re-render only dirty rows, viewports append when only new lines arrive, and components cache output keyed on content/version/width/selection.
  • Pre-rendered everything: border bytes, connector chars, scrollbar cells, zone markers (\x1b[<id>z), and ANSI prefixes, computed once as package-level vars.
  • Stack-allocated scratch: tree prefixes use var pfxBuf [1024]byte, the table a single partitioned widthsBuf []int; no heap scratch allocations.
  • Zero-copy string and byte conversion (stringbyte.StringByte): a named string type with Bytes() via unsafe.Slice; comparable, JSON-serializable, no []byte(s) copies in the hot path.
  • Zero-CGO: pure Go, no C dependencies.

Full-pipeline benchmarks (render + diff, composite layout with viewports, tree, and table at terminal size of 200×50) against Bubble Tea and cview (a fork of tview):

FeatureOursBubble Tea +262×Cview +605×
Pipeline_EveryFrameUpdate8.7 µs (0 B, 0 allocs)1.9 ms (206.3 KB, 5476 allocs) +215×2.4 ms (1.1 MB, 27891 allocs) +279×
Pipeline_NoChange2.5 µs (0 B, 0 allocs)749.2 µs (189.3 KB, 4781 allocs) +297×2.1 ms (854.4 KB, 22533 allocs) +823×
Pipeline_QuarterFrameUpdate2.6 µs (0 B, 0 allocs)715.6 µs (183.5 KB, 4133 allocs) +276×1.9 ms (819.9 KB, 20464 allocs) +715×

goos: linux goarch: amd64 cpu: AMD Ryzen 9 5900HX with Radeon Graphics, benchtime: 1s, ran: 2026-06-24

Benchmark source code