tuigo

package module
v0.1.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 12 Imported by: 0

README

tuigo

Go Reference Go Report Card Go Version

A declarative, component-based terminal UI (TUI) framework for Go — modeled after Ink (React for CLIs), but built from plain Go function calls, flexbox layout, and hooks. No JSX, no code generation, no virtual-DOM reconciliation you don't need. Just Box, Text, UseState, and events — a terminal UI library that feels like Go, not a port of a browser idea into a place that never needed it.

demo

Why tuigo

Most terminal UI is either imperative cursor math, or a port of a browser idea (virtual DOM diffing) into a place that never needed it — a terminal screen is a few thousand cells, not a million-node DOM tree. tuigo skips that step on purpose: every frame is a full rebuild of a small element tree, and only the cells that actually changed get written to the terminal. Simpler pipeline, faster in practice, at the scale a TUI actually runs at.

The design goal, stated plainly: best-in-class for 2026 — event-driven and property-based like Borland's VCL/TObject era, not a React clone in Go. See ARCHITECTURE.md for the reasoning behind every architectural choice below.

  • TUIML — components are just Go functions returning an Element tree built from Box/Text/Fragment and functional options. If you can write Go, you already know the syntax.
  • Hooks, not callbacks-on-callbacksUseState/UseReducer, one hook slot per component instance (keyed by tree position or an explicit key), the same mental model as React/Ink without needing a JS runtime.
  • Real events — keyboard (focus-scoped, with a Global() escape hatch for app-wide shortcuts like Ctrl+C) and mouse (click/scroll, hit-tested against the actual rendered layout — including floating overlays).
  • Fast by construction — flexbox layout → cell buffer → diff → ANSI. SGR state and cursor position are tracked across frames so unchanged style/position never gets re-emitted.
  • Batteries included — a widget layer (Panel, Dialog, Menu, ProgressBar, Spinner, Clock, Badge), a Picture-in-Picture overlay system, and standalone audio and asciiart libraries.

Install

go get github.com/wildneuro/tuigo

Requires Go 1.22+.

Quick start

package main

import tuigo "github.com/wildneuro/tuigo"

func Counter(ctx *tuigo.Ctx) tuigo.Element {
	n, setN := tuigo.UseState(ctx, 0)

	return tuigo.Box(
		tuigo.Padding(1), tuigo.FlexColumn(), tuigo.Border(tuigo.BorderSingle),
		tuigo.OnKey('+', func() { setN(n + 1) }),
		tuigo.Global()(tuigo.OnSpecialKey(tuigo.KeyCtrlC, ctx.Exit)),
		tuigo.Children(
			tuigo.Text("count: %d", n),
			tuigo.With(tuigo.Text("press + to increment, ctrl+c to quit"), tuigo.ColorFg(tuigo.ColorGray)),
		),
	)
}

func main() {
	if err := tuigo.Render(Counter); err != nil {
		panic(err)
	}
}

Render puts the terminal into raw/alt-screen mode, runs the loop, and always restores it on exit — including on panic. See _examples/static-counter, _examples/style-showcase, and the full _examples/chat app (bubbles, a / command menu, mouse, focus/Tab cycling, and 3+ simultaneous PiP windows) for more.

Core concepts

TUIML: composition, not markup

Every node is a plain types.Element built by a constructor plus functional options. Options apply left-to-right; later options win when they touch the same field.

tuigo.Box(
    tuigo.Padding(1), tuigo.FlexColumn(), tuigo.Gap(1), tuigo.Border(tuigo.BorderSingle),
    tuigo.Children(
        tuigo.Text("hello"),
        tuigo.With(tuigo.Text("dim caption"), tuigo.ColorFg(tuigo.ColorGray), tuigo.Italic()),
    ),
)

Box is a flex container (row or column, your call via FlexRow() / FlexColumn(), the latter being the common one). Text renders a formatted string. Fragment groups children without its own box, exactly like React's <>...</>. With applies extra options to an already-constructed element — mainly for styling Text, whose own variadic slot is reserved for fmt.Sprintf args.

Layout: pure Go flexbox

layout/ computes every node's position and size with no terminal involved — testable in isolation, deterministic, side-effect free. Padding, margin, gap, explicit Width/Height, and intrinsic sizing (a Text node's height is its word-wrapped line count) all compose the way you'd expect from CSS flexbox, minus the parts a terminal doesn't need.

Style: cascade where it makes sense, not everywhere

Text-styling fields (ColorFg, ColorBg, Bold, Italic, Underline) cascade from an ancestor Box down to Text children that don't set their own value — set a theme color once on a container, every child inherits it. Layout fields (Padding, Margin, Gap, Border, size, direction) never inherit; they only ever apply to the node they're set on. Colors support both the 256-color xterm palette (ColorRed, ColorBlue, …) and 24-bit truecolor (RGB(r, g, b)) for terminals that support it.

State: hooks, per component instance
count, setCount := tuigo.UseState(ctx, 0)
state, dispatch := tuigo.UseReducer(ctx, reducer, initialState)

Call hooks unconditionally at the top of your component, same rule as React. Each component instance (identified by its position in the tree, or an explicit key via WithKey for lists whose order/count can change) owns its own hook storage — not one flat sequence for the whole app, so independent components don't stomp on each other's state.

Events: focus-scoped by default, global when it matters
tuigo.OnKey('q', handler)              // matches a specific rune
tuigo.OnSpecialKey(tuigo.KeyEnter, h)  // matches a named key
tuigo.OnAnyKey(func(k tuigo.Key) {})   // every key — build text inputs with this
tuigo.OnClick(func(m tuigo.MouseEvent) {})
tuigo.OnScroll(func(delta int) {})     // -1 wheel up, +1 wheel down

tuigo.Focusable()          // mark an element as a Tab target
ctx.Focus("key")           // move focus programmatically
ctx.FocusNext(), ctx.FocusPrev()  // Tab / Shift+Tab

tuigo.Global()(tuigo.OnSpecialKey(tuigo.KeyCtrlC, ctx.Exit)) // fires regardless of focus

Keyboard events go to whichever element currently has focus — the VCL model, not "every handler in the tree fires for every keystroke." Global() wraps a handler so it fires app-wide instead (use it sparingly: quit shortcuts, not a way to avoid thinking about focus). Mouse events are hit-tested against the actual last-rendered layout, including floating overlays, bubbling from the innermost matching element outward.

Widgets

Panel, Divider, Badge, ProgressBar, Spinner, Clock, Dialog, Menu — all pure composition over Box/Text, no new primitives. Read their doc comments for the honest caveats (e.g. Dialog is a full-viewport takeover, not a floating window — see Overlays below for that).

Picture-in-Picture overlays

tuigo's layout is flow-only — no z-order, no absolute positioning. For floating content (PiP panels, popups, toasts) there's a small, explicit escape hatch instead of bolting z-order onto the whole layout model:

ctx.Overlay(myPanel, x, y) // call every render you want it visible

Each overlay gets its own independent layout+draw pass, sized by its own explicit Width/Height, then is composited onto the main frame before the cell diff — so it participates in the same minimal-redraw diffing as everything else. _examples/chat runs three-plus of these simultaneously: a live telemetry log, a CPU/mem meter, and (via the /music command) a music player + an ASCII-art image gallery, all on top of the same chat view at once.

Timers
cancel := ctx.After(2*time.Second, func() { /* runs on the render loop, safely */ })

Routed through a channel into the single render-loop goroutine — nothing outside that goroutine ever touches component state directly, so there's no locking and no data races to reason about.

Standalone libraries

Two more packages, usable with or without tuigo:

  • audioPlay/PlayAsync/Record by shelling out to platform-native tools (afplay/paplay/aplay/ffplay/PowerShell), plus a pure-Go WAV chirp synthesizer (Beep, GenerateChirpWAV) for UI feedback sounds with zero external dependencies.
  • asciiart — decode an image and render it as a grayscale ASCII ramp or a truecolor RGB grid (for solid-block "pixel art" in a 24-bit terminal). Pure stdlib, no CGo, no third-party deps.

Project layout

tuigo/
├── tuigo.go, node.go, style.go, event.go, state.go, overlay.go, widgets.go
├── types/       Element, Style, Key/MouseEvent — shared, avoids import cycles
├── layout/      pure flexbox geometry, zero terminal dependency
├── renderer/    Cell/Buffer/Diff + the terminal shell (raw mode, ANSI, input decode)
├── reconciler/  reserved — deliberately unimplemented, see ARCHITECTURE.md
├── audio/       standalone play/record library
├── asciiart/    standalone image → terminal-art library
└── _examples/   static-counter, style-showcase, chat

Development

go build ./...
go test ./...
go test -race ./...
go vet ./...
gofmt -l .
./example-chat.sh     # run the full chat demo in a real terminal
./make-demo.sh        # regenerate demo.gif via vhs
./release.sh          # bump VERSION, run checks, regenerate the demo, commit

example-chat.sh needs a real TTY on stdin (raw mode) — run it in an actual terminal, not piped through another tool.

Read more

  • PLAN.md — original design doc and v1 scope
  • STYLEGUIDE.md — the TUIML ruleset: what's enforced and why
  • ARCHITECTURE.md — the case for skipping React-style reconciliation, the focus/instance/timer design
  • TODO.md — what's left, with acceptance criteria per item

Status

Under active development. The core pipeline (layout → render → diff → flush), hooks, focus-scoped events, mouse, timers, truecolor, and the PiP overlay system all work today and are covered by tests — see TODO.md for the remaining gaps (row-direction text wrapping, Windows resize, more tuigo.go test coverage). Not yet at a tagged v1.

Documentation

Overview

Package tuigo is a declarative, component-based terminal UI framework.

Index

Constants

View Source
const (
	KeyNone      = types.KeyNone
	KeyEnter     = types.KeyEnter
	KeyEsc       = types.KeyEsc
	KeyTab       = types.KeyTab
	KeyBackspace = types.KeyBackspace
	KeyDelete    = types.KeyDelete
	KeyUp        = types.KeyUp
	KeyDown      = types.KeyDown
	KeyLeft      = types.KeyLeft
	KeyRight     = types.KeyRight
	KeyHome      = types.KeyHome
	KeyEnd       = types.KeyEnd
	KeyCtrlC     = types.KeyCtrlC
	KeyBackTab   = types.KeyBackTab
	KeyCtrlO     = types.KeyCtrlO

	KeyCtrlSpace      = types.KeyCtrlSpace
	KeyCtrlG          = types.KeyCtrlG
	KeyCtrlBackslash  = types.KeyCtrlBackslash
	KeyCtrlRBracket   = types.KeyCtrlRBracket
	KeyCtrlCaret      = types.KeyCtrlCaret
	KeyCtrlUnderscore = types.KeyCtrlUnderscore
)
View Source
const (
	MouseLeft      = types.MouseLeft
	MouseMiddle    = types.MouseMiddle
	MouseRight     = types.MouseRight
	MouseWheelUp   = types.MouseWheelUp
	MouseWheelDown = types.MouseWheelDown
)
View Source
const (
	BorderNone    = types.BorderNone
	BorderSingle  = types.BorderSingle
	BorderDouble  = types.BorderDouble
	BorderRounded = types.BorderRounded
)

Variables

View Source
var DarkTheme = ThemeColors{
	Background:  RGB(16, 17, 22),
	Surface:     RGB(28, 29, 36),
	SurfaceAlt:  RGB(42, 44, 54),
	TextPrimary: ColorBrightWhite,
	TextMuted:   ColorGray,
	Accent:      RGB(0, 180, 216),
	AccentAlt:   RGB(0, 216, 180),
	Success:     ColorGreen,
	Warning:     ColorYellow,
	Error:       ColorRed,
}
View Source
var FocusCycleKey = types.Key{Special: types.KeyCtrlO}

FocusCycleKey is the global chord that cycles focus between focusable elements, regardless of what is focused (so you can always leave a grab-all TerminalPane). Default: Ctrl-O. Override it before Render, e.g.

tuigo.FocusCycleKey = tuigo.Key{Special: tuigo.KeyBackTab}

Pick something the embedded child doesn't rely on.

View Source
var LightTheme = ThemeColors{
	Background:  RGB(245, 245, 245),
	Surface:     RGB(255, 255, 255),
	SurfaceAlt:  RGB(225, 225, 225),
	TextPrimary: RGB(20, 20, 20),
	TextMuted:   RGB(90, 90, 90),
	Accent:      RGB(0, 110, 170),
	AccentAlt:   RGB(0, 140, 120),
	Success:     RGB(20, 130, 60),
	Warning:     RGB(160, 110, 0),
	Error:       RGB(180, 30, 30),
}
View Source
var NamedThemes = map[string]ThemeColors{
	"dark":  DarkTheme,
	"light": LightTheme,
}

NamedThemes indexes the built-in themes for cycling/lookup (e.g. a "/theme" command).

View Source
var RGB = types.RGB
View Source
var Theme = DarkTheme

Theme is the active palette. It starts as DarkTheme; call SetTheme to switch it — every subsequent render that reads Theme.* picks up the change immediately, since it's read fresh each render, not cached.

Functions

func Clamp01 added in v0.1.8

func Clamp01(t float64) float64

Clamp01 clamps t to [0, 1] — animation progress is almost always a t := elapsed/duration computation that can overshoot past 1 once the animation has finished but before the caller notices and stops ticking.

func EaseInCubic added in v0.1.8

func EaseInCubic(t float64) float64

EaseInCubic is the mirror of EaseOutCubic: slow start, fast finish.

func EaseOutCubic added in v0.1.8

func EaseOutCubic(t float64) float64

EaseOutCubic is a common UI motion curve: fast start, slow finish. Pass a Clamp01'd t in [0, 1].

func Global

func Global() func(Option) Option

func Lerp added in v0.1.8

func Lerp(a, b, t float64) float64

Lerp linearly interpolates between a and b by t. t is not clamped — pass Clamp01(t) yourself if you need that, since some callers deliberately extrapolate (e.g. an overshoot bounce).

func Render

func Render(component Component) (err error)

func RenderInline added in v0.1.13

func RenderInline(component Component) (err error)

RenderInline is Render on the MAIN screen — it never takes over the alternate screen buffer (see renderer.NewInlineTerminal). Use it when the host already owns an alt screen that must survive the render, e.g. a PTY wrapper showing a modal over a full-screen child: the wrapper switches away from the child's alt buffer, calls RenderInline, then switches back and the terminal restores the child's screen untouched. Same component/event model as Render; still raw mode + mouse + its own stdin for the render's duration.

func SetTheme

func SetTheme(t ThemeColors)

SetTheme replaces the active theme wholesale.

func UseReducer

func UseReducer[S, A any](ctx *Ctx, reducer func(S, A) S, initial S) (S, func(A))

func UseState

func UseState[T any](ctx *Ctx, initial T) (T, func(T))

Types

type BorderStyle

type BorderStyle = types.BorderStyle

Re-exported so callers only ever import the tuigo package for TUIML.

type Color

type Color = types.Color

Re-exported so callers only ever import the tuigo package for TUIML.

const (
	ColorBlack   Color = 16
	ColorRed     Color = 1
	ColorGreen   Color = 2
	ColorYellow  Color = 3
	ColorBlue    Color = 4
	ColorMagenta Color = 5
	ColorCyan    Color = 6
	ColorWhite   Color = 7
	ColorGray    Color = 8

	ColorBrightRed     Color = 9
	ColorBrightGreen   Color = 10
	ColorBrightYellow  Color = 11
	ColorBrightBlue    Color = 12
	ColorBrightMagenta Color = 13
	ColorBrightCyan    Color = 14
	ColorBrightWhite   Color = 15
)

Named xterm-256 colors. Color's zero value means "unset/inherit" (see types.Color), so plain black lives at the extended-palette index 16 rather than the ambiguous 0.

type Component

type Component func(ctx *Ctx) Element

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

func (*Ctx) After

func (c *Ctx) After(d time.Duration, fn func()) func()

func (*Ctx) Exit

func (c *Ctx) Exit()

func (*Ctx) Focus

func (c *Ctx) Focus(key string)

func (*Ctx) FocusNext

func (c *Ctx) FocusNext()

func (*Ctx) FocusPrev

func (c *Ctx) FocusPrev()

func (*Ctx) IsFocused added in v0.1.15

func (c *Ctx) IsFocused(key string) bool

IsFocused reports whether the element identified by key currently holds focus. It matches the app's focus path exactly or by trailing segment, so a pane keyed "term" is focused whether its resolved path is "term" or "panes/term". Used by a Canvas painter to decide whether to draw the child's cursor.

func (*Ctx) OnCleanup added in v0.1.15

func (c *Ctx) OnCleanup(fn func())

OnCleanup registers fn to run when this component instance is unmounted (removed from the tree on a later render). It piggybacks the same per-instance teardown list Ctx.After cancels use, so a long-lived resource — e.g. a TerminalPane's child process and PTY — is released when the pane leaves the tree. Call it once per mount (guard with UseState), not every render.

func (*Ctx) Overlay

func (c *Ctx) Overlay(el Element, x, y int)

Overlay registers ov to be drawn on top of the main frame for this render. Call it from within Component — like UseState, call it unconditionally on every render you want the overlay visible; omit the call on renders where it should disappear.

func (*Ctx) Size

func (c *Ctx) Size() (width, height int)

func (*Ctx) Wake added in v0.1.15

func (c *Ctx) Wake()

Wake asks the render loop to run one more frame as soon as it can. It is the redraw trigger a BACKGROUND goroutine uses after mutating state the UI reads (e.g. a TerminalPane's reader goroutine after writing child output into its Screen): the loop selects on the same timers channel Ctx.After feeds, so a wake is just an empty job pushed onto it. Safe to call from any goroutine; non-blocking (a wake is dropped only when one is already queued, which is fine — the queued frame will observe the latest state anyway).

type Element

type Element = types.Element

Element is the tree node produced by Box, Text, and Fragment.

func Badge

func Badge(text string, bg Color) Element

Badge is a small colored pill label, e.g. for status tags or counts.

func Box

func Box(opts ...Option) Element

Box is a flex container: it lays out its children and never renders text of its own.

func Clock

func Clock(layout string) Element

Clock renders the current wall-clock time formatted per the time package layout convention (e.g. "15:04:05"). Render's main loop ticks once a second specifically so a Clock element updates on its own without any user input driving a re-render.

func Dialog

func Dialog(viewportW, viewportH, dialogW, dialogH int, title string, bodyOpts ...Option) Element

Dialog centers a titled Panel of exactly dialogW x dialogH within a viewportW x viewportH screen, using flex spacers on every side. It's a full-viewport takeover, not a true floating overlay — tuigo has no z-order/absolute-positioning compositing yet (see TODO.md), so a Dialog is meant to fully replace the tree it's shown in while open, not draw on top of other content.

func Divider

func Divider(width int) Element

Divider draws a horizontal rule width cells wide, e.g. to separate sections inside a Panel without a full nested border.

func Fragment

func Fragment(children ...Element) Element

Fragment groups children without introducing a layout box of its own.

func Menu(items []MenuItem, selected int, onSelect func(index int)) Element

Menu renders a selectable list — a dropdown/popup style widget for things like a "/" slash-command palette in a chat input. It occupies its own row(s) in normal flow layout (tuigo has no floating overlays yet), so the idiomatic use is to conditionally insert it as a sibling just above/below the input it's triggered from, pushing other content rather than floating over it. Menu renders items with items[selected] highlighted. onSelect, if non-nil, fires with a row's index when it's clicked — pass nil for a keyboard-only menu.

func Panel

func Panel(title string, opts ...Option) Element

Panel is a titled, bordered container in the spirit of a Turbo Vision / Borland-era window frame: a full-width colored title bar rather than a plain text line, so the title reads as a window's chrome instead of just another content row. The bar uses Theme.Accent — a Box, not a bare Text, since a standalone Text's ColorBg only paints behind its own characters (see renderer/draw.go's drawText), not the full row; a Box's fillBackground paints its entire rect. Pass Children(...) and any other Box options (Width, Height, Padding, ...) as opts; Border defaults to BorderRounded and FlexColumn if not overridden by a later option.

func ProgressBar

func ProgressBar(fraction float64, width int) Element

ProgressBar renders a fraction in [0,1] as a two-tone bar of the given width plus a percentage label. Zero-width segments are omitted rather than passed as Width(0), since Style's zero value means "unset" (see types.Style) and would otherwise be treated as a flex child instead of literally zero-width.

func Spinner

func Spinner(frame int) Element

Spinner returns one frame of a braille spinner glyph. It's pure — the caller drives animation by incrementing frame (e.g. once per received key, or once per tick once TODO.md item 3's timers land) and re-rendering.

func TerminalPane added in v0.1.15

func TerminalPane(ctx *Ctx, argv []string, opts ...Option) Element

TerminalPane spawns argv[0] (with argv[1:] as arguments) in a PTY and returns a Canvas element that composites the child's live screen into the tuigo layout. It must be called from inside a component (it uses ctx hooks). Give it a stable WithKey so focus (and thus input routing) targets it; it is Focusable by default. Standard layout options (Width, Height, flex) apply as they do to a Box; OnPaneExit surfaces the child's exit.

tuigo.TerminalPane(ctx, []string{"bash"}, tuigo.WithKey("term"))

func Text

func Text(format string, args ...any) Element

Text renders a formatted string. Unset style fields cascade from the nearest ancestor Box.

func With

func With(e Element, opts ...Option) Element

With applies style/key options to an already-constructed Element (most commonly a Text node, whose constructor reserves its variadic args for fmt.Sprintf). It never mutates the Element passed in.

type FlexDirection

type FlexDirection = types.FlexDirection

Re-exported so callers only ever import the tuigo package for TUIML.

type Key

type Key = types.Key
type MenuItem struct {
	Label string
	Hint  string // shown dim/right-aligned-ish after Label, e.g. a shortcut or description
}

MenuItem is one row in a Menu.

type MouseButton

type MouseButton = types.MouseButton

MouseButton identifies which button (or wheel direction) a MouseEvent reports.

type MouseEvent

type MouseEvent = types.MouseEvent

MouseEvent is a single mouse report: a button/wheel action at a 0-indexed cell coordinate, delivered by OnClick/OnMouse/OnScroll.

type Option

type Option func(*Element)

Option mutates an Element during construction. Options apply in the order they're passed and never mutate a shared Element.

func Bold

func Bold() Option

func Border

func Border(b BorderStyle) Option

Border sets the border style drawn around a Box.

func Children

func Children(children ...Element) Option

Children attaches child elements to a Box.

func ColorBg

func ColorBg(c Color) Option

ColorBg sets the background color.

func ColorFg

func ColorFg(c Color) Option

ColorFg sets the cascading foreground text color.

func FlexColumn

func FlexColumn() Option

FlexColumn lays out children top-to-bottom.

func FlexRow

func FlexRow() Option

FlexRow lays out children left-to-right. It's the zero-value direction, so this option exists mainly for explicitness.

func Focusable

func Focusable() Option

func Gap

func Gap(n int) Option

Gap sets the spacing between a Box's children.

func GrabInput added in v0.1.16

func GrabInput() Option

GrabInput marks an element as focusable AND a grab-all key sink: while it holds focus, Tab and Shift-Tab are delivered to it (via its key handlers) instead of cycling focus, so an embedded full-screen child gets those keys. Focus is still switchable with the global focus chord (FocusCycleKey, default Ctrl-O). TerminalPane sets this by default.

func Height

func Height(n int) Option

func Italic

func Italic() Option

func Margin

func Margin(n int) Option

Margin sets all four margin edges to n.

func MarginBottom

func MarginBottom(n int) Option

func MarginLeft

func MarginLeft(n int) Option

func MarginRight

func MarginRight(n int) Option

func MarginTop

func MarginTop(n int) Option

func MinHeight

func MinHeight(n int) Option

func MinWidth

func MinWidth(n int) Option

func OnAnyKey

func OnAnyKey(handler func(Key)) Option

func OnClick

func OnClick(handler func(MouseEvent)) Option

OnClick calls handler when the left mouse button is pressed anywhere within this Element's rendered rect. The dispatcher hit-tests using the last computed layout, so it works for Box and Text alike.

func OnHotkey added in v0.1.19

func OnHotkey(k SpecialKey, handler func()) Option

OnHotkey registers a GLOBAL key binding: handler fires when k is pressed anywhere in the app, regardless of which element holds focus (sugar over Global()(OnSpecialKey(...))). Attach it to any element — conventionally the root.

func OnKey

func OnKey(r rune, handler func()) Option

func OnMouse

func OnMouse(handler func(MouseEvent)) Option

OnMouse calls handler for every mouse event that hits this Element's rendered rect, regardless of button.

func OnPaneExit added in v0.1.15

func OnPaneExit(fn func(error)) Option

OnPaneExit registers a callback fired once when a TerminalPane's child process exits (or fails to start). The error is the child's exit error, or nil for a clean exit. A parent typically reacts by exiting the app or replacing the pane.

func OnScroll

func OnScroll(handler func(delta int)) Option

OnScroll calls handler with -1 for wheel-up or +1 for wheel-down when the scroll wheel is used over this Element's rendered rect.

func OnSpecialKey

func OnSpecialKey(k SpecialKey, handler func()) Option

func Padding

func Padding(n int) Option

Padding sets all four padding edges to n.

func PaddingBottom

func PaddingBottom(n int) Option

func PaddingLeft

func PaddingLeft(n int) Option

func PaddingRight

func PaddingRight(n int) Option

func PaddingTop

func PaddingTop(n int) Option

func Truncate

func Truncate() Option

Truncate disables word-wrapping for a Text element and instead renders a single line, replacing the last visible character with '…' if the text is wider than the available width.

func Underline

func Underline() Option

func Width

func Width(n int) Option

func WithKey

func WithKey(key string) Option

WithKey assigns a stable identity used by the reconciler to match nodes across renders.

type Overlay

type Overlay struct {
	Element Element
	X, Y    int
}

Overlay is one floating panel — a Picture-in-Picture widget, a toast, a popup — composited on top of the main frame at a fixed screen position. Element must carry explicit Width/Height (e.g. via the Width/Height options) since it's laid out and drawn independently of the main tree, with no parent to size it.

tuigo has no z-order/absolute-positioning support in the layout tree itself (see ARCHITECTURE.md) — Overlay is the escape hatch: it renders its own small Element tree into its own Buffer, then stamps that buffer onto the main frame after the main Draw call and before the cell diff, so only the overlay's actually-changed cells get re-flushed like anything else. It does not participate in flow layout, hit-testing, or focus; call OnClick/OnScroll on its own Element if it needs input.

type Screen added in v0.1.14

type Screen struct {
	// contains filtered or unexported fields
}

Screen is a cell-grid terminal emulator (backed by vt10x) plus a page stack. It ingests a terminal output byte stream via Write (io.Writer) and can save the visible screen onto a page stack for later restore. It is not safe for concurrent use; the host must serialise Write against the page/snapshot calls.

func NewScreen added in v0.1.14

func NewScreen(rows, cols int) *Screen

NewScreen creates a rows×cols emulator with a blank screen and an empty page stack.

func (*Screen) CellAt added in v0.1.15

func (s *Screen) CellAt(x, y int) (r rune, fg, bg Color, bold, italic, underline bool)

CellAt returns the painted contents of one cell: its rune, resolved foreground/background as tuigo Colors, and the SGR attributes tuigo can repaint. Palette index 0 and vt10x defaults both map to Color 0 ("unset"), so they cascade to the surrounding style rather than forcing black.

func (*Screen) Cursor added in v0.1.15

func (s *Screen) Cursor() (x, y int, visible bool)

Cursor reports the child cursor's position (col x, row y) and whether it is visible. A TerminalPane draws a block cursor there when the pane is focused.

func (*Screen) MouseMode added in v0.1.18

func (s *Screen) MouseMode() (enabled bool, sgr bool)

MouseMode reports whether the emulated child has enabled ANY mouse reporting mode (X10/button/motion/many) and whether it additionally enabled the SGR (1006) coordinate extension. A TerminalPane consults this before forwarding host mouse events to the child's PTY: if the child never opted in, forwarding is a silent no-op (matches real terminal behavior).

func (*Screen) PageDepth added in v0.1.14

func (s *Screen) PageDepth() int

PageDepth reports how many pages are currently stacked (unclosed PushPage calls). Useful for asserting balanced push/pop.

func (*Screen) PopPage added in v0.1.14

func (s *Screen) PopPage(w io.Writer) error

PopPage repaints the top saved page to w and pops it off the stack. It returns an error if the stack is empty (nothing to restore) or the write fails. The saved page — not the current live screen — is what gets repainted, so a dialog drawn over the child leaves no trace.

func (*Screen) PushPage added in v0.1.14

func (s *Screen) PushPage()

PushPage saves the current visible screen onto the page stack. Pair it with PopPage to repaint that exact page after a dialog closes; nested dialogs push again and pop in reverse order.

func (*Screen) Resize added in v0.1.14

func (s *Screen) Resize(rows, cols int)

Resize changes the emulated screen to rows×cols.

func (*Screen) Restore added in v0.1.14

func (s *Screen) Restore(w io.Writer) error

Restore is a convenience that snapshots the current screen and repaints it to w.

func (*Screen) Size added in v0.1.15

func (s *Screen) Size() (rows, cols int)

Size reports the emulated screen dimensions in rows and cols.

func (*Screen) Snapshot added in v0.1.14

func (s *Screen) Snapshot() *ScreenSnapshot

Snapshot copies the current visible screen — every cell, the cursor position, and cursor visibility — into a standalone page value that Restore can repaint later.

func (*Screen) Write added in v0.1.14

func (s *Screen) Write(p []byte) (int, error)

Write ingests a terminal output byte stream, updating the emulated screen. A multibyte UTF-8 rune split across two Writes (e.g. at a PTY read-buffer boundary) would otherwise be dropped by vt10x, so an incomplete trailing rune is held back and prepended to the next Write. All input bytes are accepted, so the reported count is always len(p).

type ScreenSnapshot added in v0.1.14

type ScreenSnapshot struct {
	// contains filtered or unexported fields
}

ScreenSnapshot is an immutable copy of a Screen's visible viewport (cell grid + cursor), produced by Snapshot / PushPage and repainted by Restore / PopPage.

func (*ScreenSnapshot) Restore added in v0.1.14

func (snap *ScreenSnapshot) Restore(w io.Writer) error

Restore repaints the whole page to w: hide cursor, reset SGR, clear screen, then for every cell emit its rune with the right SGR (re-emitting a style only when it changes), and finally leave the cursor exactly where the snapshot had it. Feeding Restore's output into a fresh NewScreen of the same size reproduces an identical grid (round-trip).

type SpecialKey

type SpecialKey = types.SpecialKey

type ThemeColors

type ThemeColors struct {
	// Surfaces
	Background Color
	Surface    Color
	SurfaceAlt Color

	// Text
	TextPrimary Color
	TextMuted   Color

	// Accents
	Accent    Color
	AccentAlt Color

	// Semantic
	Success Color
	Warning Color
	Error   Color
}

ThemeColors is a small, intentional color palette for tuigo applications. Prefer theme colors over raw palette indices; use RGB only for one-off visual effects that don't belong in the theme.

Directories

Path Synopsis
_examples
chat command
Command chat is a mock two-sided chat UI demonstrating TUIML's state management (UseState), event handling (OnAnyKey/OnSpecialKey/OnScroll), and widget layer (Panel/Badge/Clock/Spinner/ProgressBar/Menu/Dialog) on top of the style/flex system.
Command chat is a mock two-sided chat UI demonstrating TUIML's state management (UseState), event handling (OnAnyKey/OnSpecialKey/OnScroll), and widget layer (Panel/Badge/Clock/Spinner/ProgressBar/Menu/Dialog) on top of the style/flex system.
compositor command
Command compositor is the proof-of-concept for embedding a DEMANDING full-screen child (think Claude Code) in a tuigo TerminalPane and overlaying a dialog on top of it without corrupting the pane.
Command compositor is the proof-of-concept for embedding a DEMANDING full-screen child (think Claude Code) in a tuigo TerminalPane and overlaying a dialog on top of it without corrupting the pane.
static-counter command
Command static-counter builds a small TUIML element tree and prints its structure.
Command static-counter builds a small TUIML element tree and prints its structure.
style-showcase command
Command style-showcase exercises every style option (colors, text attributes, border, flex row vs column) as a reference for TUIML's style system.
Command style-showcase exercises every style option (colors, text attributes, border, flex row vs column) as a reference for TUIML's style system.
termpane command
Command termpane is a minimal terminal COMPOSITOR built with tuigo: it embeds a live child shell (bash by default, or the program named on the command line) inside a tuigo TerminalPane and lays it out beside a status column — tmux/zellij-as-a-library in ~40 lines.
Command termpane is a minimal terminal COMPOSITOR built with tuigo: it embeds a live child shell (bash by default, or the program named on the command line) inside a tuigo TerminalPane and lays it out beside a status column — tmux/zellij-as-a-library in ~40 lines.
Package asciiart decodes images (anything image.Decode supports — png/jpeg/gif via blank imports below) into terminal-friendly grids, either a grayscale character ramp or per-cell averaged RGB for truecolor block rendering.
Package asciiart decodes images (anything image.Decode supports — png/jpeg/gif via blank imports below) into terminal-friendly grids, either a grayscale character ramp or per-cell averaged RGB for truecolor block rendering.
Package audio plays and records audio by shelling out to platform-native tools rather than linking a CGo audio backend — no C toolchain, no cross-compile headaches, consistent with tuigo's "boring, testable layers" philosophy (see the root STYLEGUIDE.md).
Package audio plays and records audio by shelling out to platform-native tools rather than linking a CGo audio backend — no C toolchain, no cross-compile headaches, consistent with tuigo's "boring, testable layers" philosophy (see the root STYLEGUIDE.md).
Package layout computes pure Go flexbox geometry for an Element tree.
Package layout computes pure Go flexbox geometry for an Element tree.
Package renderer turns a laid-out Element tree into a cell buffer, diffs it against the previous frame, and flushes only the changed cells to a terminal.
Package renderer turns a laid-out Element tree into a cell buffer, diffs it against the previous frame, and flushes only the changed cells to a terminal.
Package types defines the declarative element tree shared by tuigo's layout, renderer, and reconciler packages.
Package types defines the declarative element tree shared by tuigo's layout, renderer, and reconciler packages.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL