Help us shape the future of Go! Take the 2026 Go Developer Survey

tui

package
v0.37.1 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package tui is a single Bubble Tea-based TUI foundation for bentoolkit.

Command code never talks to Bubble Tea directly: it emits progress through the Reporter interface (see reporter.go), which is implemented by two backends — teaReporter (forwards to a live Program) and plainReporter (streams plain text to an io.Writer for non-TTY / opt-out runs). The event types below are the tea.Msg values teaReporter forwards via program.Send (the documented goroutine-safe ingress), so they double as the model's Update inputs.

Index

Constants

View Source
const MaxCapturedBytes = 64 << 10

MaxCapturedBytes is how much child output is kept for an error message: the LAST 64 KiB. Before this bound a child that wrote 200 MB had all 200 MB kept in memory and pasted into its error.

Variables

This section is empty.

Functions

func Enabled

func Enabled(o Options) bool

Enabled is the ONE place the live-TUI decision is made. The TUI is used iff stdout is a terminal AND none of the opt-outs is set: the --no-tui flag, the NO_COLOR convention, or BENTOO_NO_TUI. An UNSET env var is the empty string; per the NO_COLOR convention an empty value means "not set", so only a non-empty value counts as an opt-out. The TTY probe goes through the isTerminal seam so the decision is testable.

func New

func New(ctx context.Context, cancel func(), out io.Writer, in io.Reader) (*Program, Reporter)

New builds a TUI Program with a fresh model bound to ctx and returns it together with a Reporter that forwards to it. Ctrl-C invokes cancel — the caller's existing cancellation chain — because NewProgram injects cancel into the model. out/in are the program's terminal streams (os.Stdout/os.Stdin in production); callers may pass other streams in tests.

This is the exported entry point for apply drivers in other packages: the model/reporter constructors are unexported, so the driver cannot assemble the pair itself.

func RunAttached

func RunAttached(tc terminalController, cmd *exec.Cmd) error

RunAttached runs an interactive child command with the real terminal handed back to it, so a sudo/doas password prompt is read on the actual TTY rather than swallowed by the running program.

The terminal is released before the child starts and restored afterwards. Restore is best-effort: it runs via defer so the terminal is reclaimed even when the child fails. If the release itself fails the child is NOT run and no restore is attempted, because the program still owns the terminal.

Each of cmd.Stdin/Stdout/Stderr is wired to the process's real streams only when the caller left it nil, so the child owns the TTY for the prompt while a caller that pre-set a stream (e.g. to capture output) keeps its choice.

func Tail

func Tail(s string) string

Tail returns s unchanged when it is at most MaxCapturedBytes long. Otherwise it returns the last MaxCapturedBytes bytes of s, preceded by one line stating how many earlier bytes were dropped — the shape Captured() has past the bound. The cut is by bytes, so the kept text may start mid-line or mid-rune.

It is for text pasted into a message, never for a transcript something still has to judge: the build verdict reads every phase marker of the whole transcript.

func TaskDoneWithHint added in v0.36.0

func TaskDoneWithHint(r Reporter, id string, ok bool, summary, hint, capturedOutput string)

TaskDoneWithHint closes a task on r, showing hint beside its outcome when r is a HintReporter and the hint is not empty; otherwise it is r.TaskDone.

Types

type BatchDoneMsg

type BatchDoneMsg struct{ Summary string }

BatchDoneMsg closes the run with a final summary line.

type BatchStartMsg

type BatchStartMsg struct{ Total int }

BatchStartMsg opens a run and sets the optional denominator/header.

type ConfirmMsg

type ConfirmMsg struct {
	Prompt string
	Reply  chan bool
}

ConfirmMsg asks the model to render an in-UI yes/no prompt and deliver the answer on Reply. It is the in-band confirmation path: the y/N decision is a key prompt rendered by the model rather than a read of os.Stdin behind the running program. Reply should be a buffered (cap >= 1) channel so the model's non-blocking send always lands.

type HintReporter added in v0.36.0

type HintReporter interface {
	Reporter
	// TaskDoneHint is TaskDone with hint shown at the end of the task's row.
	TaskDoneHint(id string, ok bool, summary, hint, capturedOutput string)
}

HintReporter is a Reporter that can show a short next step beside a finished task's outcome. It is optional: the plain and no-op backends do not implement it, so a hint reaches only the live region.

type LogMsg

type LogMsg struct{ Level, Text string }

LogMsg routes a logger/output line into the TUI scrollback so stray writes do not corrupt a frame.

type Options

type Options struct {
	// NoTUI is the --no-tui flag value; when true the TUI is suppressed and
	// command code falls back to plain output.
	NoTUI bool
}

Options carries the runtime inputs that gate the live TUI. NoTUI mirrors the --no-tui CLI flag value.

type Program

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

Program is a thin wrapper over a *tea.Program that owns the run goroutine and exposes a small Start/Wait/Stop lifecycle plus Send and the terminal release/restore passthroughs (§4.1).

SIGINT is reconciled, not double-owned (AD9): the program is created with WithoutSignalHandler so Bubble Tea installs no competing signal handler. Ctrl-C reaches the model as a key message, and the model invokes the injected cancel — the caller's existing cancellation chain — before quitting. The program is NOT bound to the caller's context: bubbletea kills a program whose context ends, which skips the final render and leaves the last live frame on screen. A watcher releases the region instead, as Stop does.

func NewProgram

func NewProgram(ctx context.Context, cancel func(), m *model, out io.Writer, in io.Reader) *Program

NewProgram builds the wrapper. It injects cancel into the model so Ctrl-C triggers the caller's cancellation, and keeps ctx so an external cancellation (the caller's signal chain) releases the live region. Output and input are explicit so callers can drive the program in tests or against a specific terminal; WithoutSignalHandler keeps SIGINT ownership with the caller (AD9).

func (*Program) Confirm

func (p *Program) Confirm(prompt string) bool

Confirm asks the running program for a yes/no decision and blocks until the user answers. It sends a ConfirmMsg carrying a buffered reply channel so the model can answer without blocking, then waits for that answer.

func (*Program) PrintHistory added in v0.36.0

func (p *Program) PrintHistory(w io.Writer) error

PrintHistory writes every finished task's row to w, oldest first, one per line, unfolded and not cut to the terminal width. It waits for the program to exit first, so it prints below the released live region; like Wait, it must follow Start. When Wait gave up on a stuck program, it prints without waiting for that program any longer.

func (*Program) ReleaseTerminal

func (p *Program) ReleaseTerminal() error

ReleaseTerminal relinquishes the terminal so a caller can write to it directly (passthrough to the underlying program).

func (*Program) RestoreTerminal

func (p *Program) RestoreTerminal() error

RestoreTerminal reclaims the terminal after a ReleaseTerminal (passthrough to the underlying program).

func (*Program) Send

func (p *Program) Send(msg tea.Msg)

Send forwards a message to the running program. This is the documented goroutine-safe ingress teaReporter uses to deliver event messages.

func (*Program) Start

func (p *Program) Start()

Start runs the program in a background goroutine, plus a watcher that releases the live region once the run context ends or Stop is called. It is guarded so repeated calls are no-ops; the run error is stored before done is closed so Wait reads it race-free. The watcher exits with the program.

func (*Program) Stop

func (p *Program) Stop()

Stop asks the program to release the live region and quit. It never blocks and is safe to call before Start or multiple times; pair it with Wait to block until the program has actually exited. The model drops its history rows from the frame it leaves on screen, so PrintHistory is what puts them in the scrollback.

func (*Program) Wait

func (p *Program) Wait() error

Wait blocks until the program goroutine finishes and returns its run error. Reading runErr only after done is closed pairs with the write in Start, so the handshake is race-free. A program that exits on its own returns at once. Once a release was asked for (Stop or the run context ending), the wait is bounded: after releaseGrace the program is killed (Run then reports tea.ErrProgramKilled), and after killGrace more Wait gives up and returns errProgramStuck — a renderer blocked on a terminal write cannot be ended.

type Reporter

type Reporter interface {
	// BatchStart opens a run; total is an optional denominator (0 = unknown).
	BatchStart(total int)
	// TaskStart registers a task slot under a display label.
	TaskStart(id, label string)
	// TaskStage moves a task to a named stage.
	TaskStage(id, stage string)
	// TaskProgress sets a task's progress fraction; frac < 0 = indeterminate.
	TaskProgress(id string, frac float64)
	// TaskLine emits one tail update for a task. eol=false is an in-place
	// replacement of the live line (carriage-return / partial flush); eol=true
	// commits the line into the bounded tail (it ended with a newline). See
	// TaskLineMsg for the model-side semantics.
	TaskLine(id string, stream Stream, text string, eol bool)
	// TaskDone terminates a task; capturedOutput carries the full buffer for
	// the error path (the Output: %s contract at applier.go is preserved).
	TaskDone(id string, ok bool, summary, capturedOutput string)
	// Log routes a logger/output line into the UI.
	Log(level, text string)
	// BatchDone closes the run with a final summary.
	BatchDone(summary string)
}

Reporter is the single sink command code emits progress to. Both backends implement it: teaReporter forwards each call as a tea.Msg through program.Send, plainReporter writes deterministic lines to an io.Writer.

A nil or Noop reporter is exactly equivalent to having no progress reporting: silent, fully-buffered execution. Command code holding a possibly-nil Reporter should normalize it through orNoop before use.

func NewPlainReporter

func NewPlainReporter(w io.Writer, throttle time.Duration) Reporter

NewPlainReporter is the exported constructor for the non-TTY / opt-out backend. It writes deterministic, ANSI-free lines to w; throttle rate-limits in-place tail updates per task id (0 disables it). See newPlainReporter for the line shapes.

func Noop

func Noop() Reporter

Noop returns a Reporter that discards all events.

type Stream

type Stream int

Stream identifies which subprocess stream a captured tail line came from.

const (
	// StreamStdout is the child process's standard output.
	StreamStdout Stream = iota
	// StreamStderr is the child process's standard error.
	StreamStderr
)

func (Stream) String

func (s Stream) String() string

String renders the stream name for plain-mode prefixes and log lines.

type StreamCapture

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

StreamCapture is the streaming capture seam that replaces exec's CombinedOutput for long subprocesses. It implements io.Writer, so it can be assigned to cmd.Stdout / cmd.Stderr, and tees every write into two sinks:

  • capture: an in-memory copy of the most recent output, returned by Captured() for the error path (the "Output: %s on failure" contract). It keeps the last MaxCapturedBytes verbatim and counts the bytes it dropped before them;
  • a line emitter that splits the stream into lines and forwards each update to the Reporter as a TaskLine event. It treats "\r" as an in-place line replacement and "\r\n" as a single terminator.

All mutable state is guarded by a mutex so a single StreamCapture is safe to drive from an os/exec copy goroutine and passes -race.

func NewStreamCapture

func NewStreamCapture(r Reporter, id string, stream Stream) *StreamCapture

NewStreamCapture returns a StreamCapture that emits TaskLine events for task id on the given stream. A nil Reporter is normalized to a no-op (orNoop), so the returned writer is always safe to use.

func (*StreamCapture) Captured

func (c *StreamCapture) Captured() string

Captured returns the child output kept for the error path. Up to MaxCapturedBytes written, it is every byte, byte-identical. Past that, it is the last MaxCapturedBytes bytes, preceded by one line stating how many earlier bytes were dropped — the same text Tail would make of the whole output. Reading it neither consumes nor re-truncates the capture.

func (*StreamCapture) Close

func (c *StreamCapture) Close() error

Close flushes any not-yet-emitted live-line state as an in-place (eol=false) update: a deferred trailing "\r", or a partial line with no terminator. It is safe to call more than once; a second Close emits nothing. Close always returns nil.

func (*StreamCapture) Write

func (c *StreamCapture) Write(p []byte) (int, error)

Write implements io.Writer. It appends every byte of p to the capture buffer verbatim (which keeps the last MaxCapturedBytes, see Captured) and feeds the bytes through the line splitter, emitting TaskLine events as lines update or complete. Because it is a tee it always consumes all of p, returning (len(p), nil) on success.

type TaskDoneMsg

type TaskDoneMsg struct {
	ID             string
	OK             bool
	Summary        string
	CapturedOutput string
	Hint           string
}

TaskDoneMsg terminates a task. CapturedOutput carries the full buffered child output for the error path (the model never renders it in full). Hint is an optional next step shown at the end of the task's history row; empty means none.

type TaskLineMsg

type TaskLineMsg struct {
	ID     string
	Stream Stream
	Text   string
	EOL    bool
}

TaskLineMsg carries one tail update for a task. Text is the full content of the line currently being assembled. EOL distinguishes the two terminal behaviors the StreamCapture emitter detects:

  • EOL=false: an in-place update (the child emitted "\r" or the partial line was flushed). The model REPLACES the task's live line.
  • EOL=true: the line ended with "\n". The model COMMITS the live line into the bounded tail ring and starts a fresh live line.

type TaskProgressMsg

type TaskProgressMsg struct {
	ID   string
	Frac float64
}

TaskProgressMsg sets a task's progress fraction. Frac < 0 means indeterminate (render a spinner rather than a bar).

type TaskStageMsg

type TaskStageMsg struct{ ID, Stage string }

TaskStageMsg moves a task to a named stage ("manifest", "llm-fix", …).

type TaskStartMsg

type TaskStartMsg struct{ ID, Label string }

TaskStartMsg registers a new task slot with a display label.

Jump to

Keyboard shortcuts

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