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

ui

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package ui is hmm's Charm layer — every terminal write in the binary goes through it (the rule crush gives its TUI, scaled to an enriched CLI):

theme.go    one palette → lipgloss styles + the glamour style tree
markdown.go glamour renderers, memoized per style+width
stream.go   incremental markdown: emit blocks once provably complete
spinner.go  the loader: ticker-driven, pausable, atomic Line()
console.go  Console — the facade all output goes through
turn.go     Turn — weft events in, rendered terminal out

All of it is TTY-gated: piped output stays byte-clean (principle 2), and the full-screen TUI remains a later phase.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Compact

func Compact(s string, n int) string

Compact compacts s to one line of at most n runes, for the activity feed and spinner suffixes. Control bytes are dropped first: tool and plugin output is model-authored and can carry ANSI escapes that would garble the terminal (or embed control bytes in piped logs).

func Markdown

func Markdown(text string, styled bool) string

Markdown renders an answer with glamour. styled=false (a pipe, or NO_COLOR) returns the text untouched — redirection must stay raw.

func MarkdownStyle

func MarkdownStyle(dark bool) ansi.StyleConfig

MarkdownStyle builds the glamour side of the palette: the standard dark (or light) tree with hmm's colors on the elements that carry identity — headings, links, code, quotes — so answers match the feed.

Types

type Console

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

Console is the facade all hmm terminal output goes through (the layer crush gives its whole TUI; hmm is an enriched CLI, so the facade is smaller but the rule is the same: no package outside ui writes to the streams). It owns the TTY gates, the themes (one per stream, so color follows the right pipe), the spinner, and the json/quiet modes; every method degrades to byte-clean plain output on a non-TTY writer automatically (principle 2).

func NewConsole

func NewConsole(stdout, stderr io.Writer, stdoutTTY, stderrTTY bool) *Console

NewConsole binds the console to hmm's streams. stderr is serialized internally — callers hand their writers over once, here.

func (*Console) AnswerText

func (c *Console) AnswerText(text string)

AnswerText streams raw text to stdout (the pipe path).

func (*Console) Banner

func (c *Console) Banner(provider, model, what string)

Banner prints the conversation-mode banner (TTY only — the caller gates on stdin being interactive).

func (*Console) Dimf

func (c *Console) Dimf(format string, a ...any)

Dimf renders a subdued stdout line (config footnotes).

func (*Console) Err

func (c *Console) Err() io.Writer

Err returns the serialized stderr writer — for the few external sinks (the flag package's output) that must interleave cleanly.

func (*Console) Errorf

func (c *Console) Errorf(format string, a ...any)

Errorf prints a "hmm: …" failure line to stderr. On a pipe the bytes are exactly the plain form logs have always had.

func (*Console) Feed

func (c *Console) Feed(line string)

Feed prints one permanent activity-feed line above the spinner, atomically. Suppressed by --quiet and --json.

func (*Console) Hint

func (c *Console) Hint(s string)

Hint prints a dim stderr hint (the continue hint after a one-shot).

func (*Console) JSON

func (c *Console) JSON() bool

JSON reports whether --json mode is on.

func (*Console) JSONLine

func (c *Console) JSONLine(v any)

JSONLine writes one NDJSON event to stdout; encode failures land on stderr like every other warning.

func (*Console) Mark

func (c *Console) Mark(s string) string

Mark styles an inline stdout fragment (the current-model "►" row) — violet on a TTY, untouched bytes elsewhere.

func (*Console) Markf

func (c *Console) Markf(format string, a ...any)

Markf renders a highlighted stdout line (the config header) — violet on a TTY, plain bytes elsewhere.

func (*Console) Newline

func (c *Console) Newline()

Newline adds the trailing newline a streamed answer may be missing.

func (*Console) Outf

func (c *Console) Outf(format string, a ...any)

Outf writes plain stdout text (subcommand tables); on a TTY the companion themed helpers below add color without changing layout.

func (*Console) Outln

func (c *Console) Outln(s string)

Outln writes one plain stdout line.

func (*Console) PauseSpin

func (c *Console) PauseSpin()

PauseSpin holds the spinner's line free (the y/N gate); nil-safe.

func (*Console) ResumeSpin

func (c *Console) ResumeSpin()

ResumeSpin repaints after PauseSpin; nil-safe.

func (*Console) SetMode

func (c *Console) SetMode(jsonMode, quiet bool)

SetMode installs the parsed --json/--quiet; both change what the feed and spinner do, so they live here rather than branching at every call site.

func (*Console) StartSpin

func (c *Console) StartSpin()

StartSpin begins the loader: stderr TTY, feed not quieted, not json.

func (*Console) Status

func (c *Console) Status(s string)

Status swaps the spinner's suffix ("thinking · …", "step 2 · …"). Suppressed by --quiet and --json; store-only, so per-token updates cost a mutex and a pointer store.

func (*Console) Stdout

func (c *Console) Stdout() io.Writer

Stdout returns the stdout writer — the answer's stream, used by the Turn renderer.

func (*Console) StopSpin

func (c *Console) StopSpin()

StopSpin erases the loader and ends its goroutine.

func (*Console) Turn

func (c *Console) Turn() *Turn

Turn starts a render context for one run.

func (*Console) TurnPrompt

func (c *Console) TurnPrompt()

TurnPrompt prints the conversation's "› " turn glyph (no newline).

func (*Console) Usage

func (c *Console) Usage(s string)

Usage prints the after-every-answer bookkeeping line to stderr.

type Spinner

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

Spinner drives bubbles' spinner on one line without bubbletea: a goroutine ticks frames at the spinner's FPS and redraws "frame suffix". The drawing invariants, learned the hard way:

  • a paused spinner never redraws — Pause means the line is free for permanent output, and the old loop redrew over it anyway within one frame (the garbled-feed bug);
  • Line() prints a permanent line atomically (erase → write → redraw under one lock) — callers must not compose Pause, print, Resume themselves, that composition is inherently racy;
  • every draw erases the whole line first ("\r\x1b[K") — padding math over ANSI-styled bytes leaves stale tails;
  • the loop selects on done and a ticker, so Stop returns promptly instead of blocking out the last frame (~143ms per turn).

A nil *Spinner (or a non-TTY writer) is a no-op — callers never branch.

func StartSpinner

func StartSpinner(w io.Writer, t *Theme, tty bool) *Spinner

StartSpinner begins the loader; on a non-TTY it returns nil (a safe no-op), so pipes see nothing. The Meter's stock 7 FPS reads as a stutter, so it runs at 12.

func (*Spinner) Line

func (s *Spinner) Line(line string)

Line prints one permanent line above the spinner, atomically: the erase, the write, and the redraw happen under a single lock, so no frame can interleave. A paused spinner stays paused; a stopped one has no live loop left to refresh or erase the frame, so the line prints without the repaint.

func (*Spinner) Pause

func (s *Spinner) Pause()

Pause erases the loader and holds the line free for output of unknown length (the y/N gate reads stdin while paused); Resume repaints immediately.

func (*Spinner) Resume

func (s *Spinner) Resume()

Resume restarts drawing at once.

func (*Spinner) Set

func (s *Spinner) Set(status string)

Set swaps the loader's status suffix ("thinking…", "⚙ read_file"). Store-only: the next frame picks it up, which also throttles per-token status updates to the frame rate for free.

func (*Spinner) Stop

func (s *Spinner) Stop()

Stop erases the loader and ends the goroutine. The loop wakes on the closed channel, so this returns in microseconds, not a frame.

type Theme

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

Theme holds the styles, bound to the feed writer so color detection follows the right stream: a renderer over a pipe yields no color and plain glyphs, which keeps logs (and tests) clean.

func NewTheme

func NewTheme(w io.Writer) *Theme

NewTheme builds the lipgloss side of the palette over w.

func (*Theme) ErrorHeader

func (t *Theme) ErrorHeader(s string) string

ErrorHeader styles "hmm: …" failure lines.

func (*Theme) Prompt

func (t *Theme) Prompt(s string) string

Prompt styles the conversation's banner and "›" turn glyph.

func (*Theme) ThinkingPrefix

func (t *Theme) ThinkingPrefix() string

ThinkingPrefix marks the reasoning stream.

func (*Theme) ToolDone

func (t *Theme) ToolDone(name, preview string, isErr bool) string

ToolDone renders a ✓/✗ feed entry with a one-line preview.

func (*Theme) ToolLine

func (t *Theme) ToolLine(name, args string) string

ToolLine renders a ⚙ feed entry: colored glyph + bold name + args.

func (*Theme) UsageLine

func (t *Theme) UsageLine(s string) string

UsageLine styles the after-answer bookkeeping line.

type Turn

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

Turn is the renderer for one agent run: it maps weft's event stream to the terminal — the stderr activity feed, the spinner status, the streamed or glamour-rendered answer, the --json event lines — so the CLI layer only pumps events; every presentation decision lives here (turn() in internal/cli used to be this, half the file).

On a TTY stdout the answer streams as completed markdown blocks (mdStream); on a pipe it streams raw per-delta, byte-clean.

func (*Turn) Answer

func (t *Turn) Answer() string

Answer returns the full raw answer accumulated so far.

func (*Turn) Event

func (t *Turn) Event(ev weft.Event)

Event renders one weft event; unknown events are ignored (the wire set is additive).

func (*Turn) Finish

func (t *Turn) Finish(runErr error)

Finish renders the end of the turn: the answer's trailing tail, the trailing newline a raw stream may lack, and the run error. The --json error line is the caller's (it carries RunError details); stderr's "hmm: …" is printed here.

func (*Turn) Start

func (t *Turn) Start()

Start begins the turn: the loader and its first status.

func (*Turn) StopSpin

func (t *Turn) StopSpin()

StopSpin ends the loader before the (possibly slow) final Wait.

Jump to

Keyboard shortcuts

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