2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00

terminal

Direct ANSI terminal control for Go with zero-allocation rendering. Built for sustained 60fps full-screen redraws in cell-based applications (games, dashboards, TUIs). Depends only on the standard library and golang.org/x/sys (Unix builds).

The package bypasses terminfo/termcap entirely and emits ANSI sequences directly. Target environments: xterm-compatible terminals on Linux and BSDs, and browsers via xterm.js (WASM builds).

Features

  • True color (24-bit) and 256-color palette output with automatic capability detection
  • Double-buffered output with cell-level diffing — only changed cells emit sequences
  • Raw stdin parsing: keys, modifiers, UTF-8 runes, SGR mouse, resize
  • Perceptual (Redmean) RGB → 256-palette mapping via O(1) LUT
  • Color blending library: alpha, additive, screen, overlay, soft light
  • Named color palettes for true color and xterm-256
  • Panic-safe terminal restoration (Fini, EmergencyReset)
  • Unix and WASM backends behind a common interface

Architecture

Terminal (interface)
  └── termImpl
        ├── outputBuffer   diffing, ANSI generation, 128KB buffered writer
        ├── inputReader    escape sequence parser, event channel
        └── Backend (interface)
              ├── unixBackend   //go:build unix — termios, unix.Poll, SIGWINCH
              └── wasmBackend   //go:build wasm — syscall/js, xterm.js bridge

Shared code carries no build tags: cell diffing, ANSI generation, escape parsing, service lifecycle. Platform specifics are isolated in the Backend implementations.

Rendering pipeline

The application owns a flat []Cell buffer (row-major, cells[y*width+x]) and passes it to Flush. The output buffer diffs against the previously flushed frame:

  • Rows are scanned with early termination (trailing unchanged cells skipped).
  • Cursor moves are emitted only when the write position is non-contiguous.
  • SGR state (fg, bg, attributes) is coalesced across cells; redundant sequences are suppressed.
  • If the backend size changed between buffer preparation and Flush, the frame is dropped to prevent resize-race corruption. The next frame (built at the new size) renders normally.

Sync() clears the screen and invalidates the front buffer, forcing a full redraw — required after any external process writes to the terminal.

Auto-wrap is disabled during the session, making the bottom-right cell writable without scroll side effects.

Quick start

package main

import "github.com/lixenwraith/terminal"

func main() {
    term := terminal.New() // color mode auto-detected
    if err := term.Init(); err != nil {
        panic(err)
    }
    defer term.Fini()

    w, h := term.Size()
    cells := make([]terminal.Cell, w*h)

    for {
        // Build frame
        for i := range cells {
            cells[i] = terminal.Cell{Rune: ' ', Bg: terminal.Gunmetal}
        }
        msg := "hello"
        for i, ch := range msg {
            // len(msg) to utf8.RuneCountIdString(msg) for non-ASCII
            cells[(h/2)*w+(w-len(msg))/2+i] = terminal.Cell{
                Rune: ch, Fg: terminal.Amber, Bg: terminal.Gunmetal,
                Attrs: terminal.AttrBold,
            }
        }
        term.Flush(cells, w, h)

        // Handle input
        ev := term.PollEvent()
        switch ev.Type {
        case terminal.EventKey:
            if ev.Key == terminal.KeyEscape || ev.Rune == 'q' {
                return
            }
        case terminal.EventResize:
            w, h = ev.Width, ev.Height
            cells = make([]terminal.Cell, w*h)
        }
    }
}

Cells and attributes

type Cell struct {
    Rune  rune
    Fg    RGB
    Bg    RGB
    Attrs Attr
}

Attr is a bitmask: AttrBold, AttrDim, AttrItalic, AttrUnderline, AttrBlink, AttrReverse.

Two flag bits change color interpretation: with AttrFg256 / AttrBg256 set, Fg.R / Bg.R holds an xterm-256 palette index directly and G/B are ignored. This allows exact palette output on true color terminals and skips RGB → palette conversion.

Color system

Modes

ColorModeTrueColor emits 38;2;R;G;B sequences; ColorMode256 emits 38;5;N after mapping. DetectColorMode() inspects the environment (COLORTERM, TERM). Explicit override: terminal.New(terminal.ColorMode256).

RGB → 256 mapping

RGBTo256 maps any RGB to the nearest xterm-256 index using perceptually weighted Redmean distance. The full mapping is pre-computed at init into a 6-bit-quantized LUT (256KB, L2-resident), making per-cell conversion a single array load. Applications targeting 256-color terminals can render in RGB throughout; degradation is automatic.

Palette helpers: Cube256(r,g,b) / CubeRGB256(idx) for 6×6×6 cube math, Gray256(step) for the grayscale ramp, plus named constants (P256Amber, P256SteelBlue, ...) in rgb_256.go and named true color values (Amber, Gunmetal, Obsidian, ...) in rgb_truecolor.go.

Blending

blend.go provides compositing primitives operating on RGB. All take destination first and are branch-free in the hot path or LUT-backed; suitable for per-cell use at frame rate.

Function Operation Character
Blend(dst, src, alpha) linear interpolation standard transparency
Add(dst, src, alpha) saturating add bright accumulation, clips
Screen(dst, src, alpha) 1-(1-d)(1-s) lightens, never clips
Overlay(dst, src, alpha) multiply/screen split at 0.5 contrast, keeps dst structure
SoftLight(dst, src, intensity) Perez soft light gentle tint/glow
Max(dst, src, alpha) per-channel max non-additive highlight
Scale(c, factor) channel multiply dim/brighten
Grayscale(c) Rec. 601 luma desaturation
c.Lerp(other, t) method on RGB gradients, animation

alpha/intensity/t are [0,1]; out-of-range values clamp. alpha of 0 or 1 short-circuits without float math. All float→channel conversions round half-up, so gradients from Blend, Scale, Lerp, and SoftLight are bit-consistent.

bg := terminal.Gunmetal
glow := terminal.RGB{R: 255, G: 160, B: 40}

cell.Bg = terminal.Screen(bg, terminal.Scale(glow, pulse), 1.0) // pulsing glow
cell.Bg = terminal.Blend(cell.Bg, terminal.Black, 0.6)          // dim overlay backdrop
cell.Fg = terminal.SoftLight(cell.Fg, tint, 0.4)                // subtle recolor
bar := cold.Lerp(hot, load)                                     // value-mapped gradient

Integer paths (Add, Screen, Overlay) use a (x + (x>>8) + 1) >> 8 division approximation; SoftLight uses init-time LUTs replacing math.Sqrt.

Input

PollEvent() blocks on a unified channel. Event.Type values:

  • EventKeyKey for named keys (KeyEnter, KeyUp, KeyCtrlC, ...), Key == KeyRune with Rune set for printable input, Modifiers bitmask (ModShift, ModAlt, ModCtrl)
  • EventMouse — 0-indexed MouseX/Y, MouseBtn (buttons, wheel), MouseAction (press/release/move/drag), modifiers. Enable via SetMouseMode(MouseModeClick | MouseModeDrag); SGR protocol only.
  • EventResize — new Width/Height
  • EventError, EventClosed

A standalone ESC press is disambiguated from escape sequences by a short input-idle timeout (one ~10ms poll cycle). Partial UTF-8 and escape sequences at read boundaries are reassembled in a persistent buffer. PostEvent injects synthetic events (used for clean shutdown of blocked PollEvent).

Service wrapper

TerminalService packages lifecycle (init, input goroutine, panic-safe teardown) behind Init/Start/Stop for service-registry architectures:

svc := terminal.NewService()
svc.Init()
svc.Start()
defer svc.Stop()

term := svc.Terminal()
for ev := range svc.Events() { /* ... */ }

Input-goroutine panics trigger EmergencyReset (restores cooked mode, main screen, cursor) before printing the stack trace, keeping the shell usable.

WASM

WASM builds bridge to xterm.js via JS globals:

goTerminalWrite(Uint8Array)    // Go → JS terminal output
goTerminalInput(Uint8Array)    // JS → Go keyboard input
goTerminalResize(cols, rows)   // JS → Go resize
xterm.cols, xterm.rows         // initial size query

Sub-packages

  • tui — immediate-mode widget toolkit (regions, layout, widgets, scroll/editor state) built on the cell buffer model.
S
Description
No description provided
Readme BSD-3-Clause
263 KiB
Languages
Go 100%