output

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package output is the app-wide console: one ring buffer of everything the app has said, and a screen for reading it.

The statusbar's center slot holds one line and wipes it on the next keypress, which makes it useless for anything you might want to read twice — a failing command's stderr, a wrapped error chain, an API response body. This package is where that output goes instead. The app shell (pkg/app) owns a Buffer, feeds it from every app.Info / app.Error, from the InfoDetail / ErrorDetail channel, and from runner.Capture, and pushes a Screen over the stack when the user asks for it.

The whole feature is opt-in: it exists only when app.Options.OutputKey is set. See pkg/app.

Records, not strings

The buffer holds Records — one per rendered line, flat, with a body line being a Record with Head=false rather than a field on its parent. Lines are formatted at render time rather than on the way in, because pre-rendered ANSI would be baked with whatever palette was active when it was written: swap themes and the log becomes a stratigraphy of old ones.

Options and pkg/theme

Options is built with OptionsFrom(t) rather than a theme.Output() method, which is a deliberate break from the th.Component() convention every other component follows (CLAUDE.md rule 3). Screen implements screen.Screen, pkg/screen imports pkg/theme for SetTheme, so a Theme method returning output.Options would close an import cycle. Inverting it — output imports theme, theme knows nothing about output — is the only arrangement that keeps the screen in this package, and keeping it here is what makes it testable without standing up an app shell.

Index

Constants

View Source
const DefaultMaxRecords = 10000

DefaultMaxRecords is the ring cap applied when Options.MaxRecords is 0, matching logview.DefaultMaxLines.

View Source
const DefaultSourceWidth = 10

DefaultSourceWidth is the column the source name is padded to, so the ›/│ glyphs line up down the log regardless of which screen or command produced each line.

Variables

This section is empty.

Functions

This section is empty.

Types

type Badge

type Badge struct {
	// Text is the affordance label, e.g. "output", "3 output", "⟳ 2 output".
	Text string
	// Error is true when any unread record was an error, selecting
	// Options.BadgeErrorStyle.
	Error bool
	// Show is false while there is nothing to look at, in which case the
	// affordance is not rendered at all.
	Show bool
}

Badge describes the statusbar affordance for a buffer's current state.

type Buffer

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

Buffer is the app-wide ring of records plus the accounting the statusbar badge reads: how many events have arrived since the log was last read, whether any of them was an error, and what is still streaming.

The zero value is not usable; construct with NewBuffer.

func NewBuffer

func NewBuffer(max int) *Buffer

NewBuffer returns an empty buffer capped at max records. max <= 0 applies DefaultMaxRecords.

func (*Buffer) Append

func (b *Buffer) Append(r Record)

Append adds one record, stamping Time when the caller left it zero, and trims if the ring is over cap.

func (*Buffer) AppendAll

func (b *Buffer) AppendAll(rs []Record)

AppendAll adds records in order. Cheaper than a loop for a burst, and it trims once at the end rather than per line.

func (*Buffer) Clear

func (b *Buffer) Clear()

Clear empties the ring and marks everything read — there is nothing left for the badge to be counting.

func (*Buffer) EndRun

func (b *Buffer) EndRun(id int64)

EndRun deregisters a capture. Unknown ids are ignored.

func (*Buffer) Epoch

func (b *Buffer) Epoch() int

Epoch changes whenever records are dropped from the front (trim or Clear). A mirror that is appending the tail should compare epochs first and rebuild from scratch when it has moved.

func (*Buffer) InFlight

func (b *Buffer) InFlight() int

InFlight is the number of captures currently streaming. The badge renders a static marker while this is non-zero.

func (*Buffer) Kill

func (b *Buffer) Kill(id int64) error

Kill signals the run with the given id. Returns nil for an unknown id or a run registered without a kill func — both mean "nothing left to stop."

func (*Buffer) Len

func (b *Buffer) Len() int

Len is the number of buffered records.

func (*Buffer) MarkRead

func (b *Buffer) MarkRead()

MarkRead resets the unread count and the error tint. The shell calls this when the output screen pops, rather than when it opens, so records arriving while the user sits on the screen don't come back unread as they leave.

func (*Buffer) Records

func (b *Buffer) Records() []Record

Records returns the buffered records, oldest first. The slice aliases the internal ring — copy it if you intend to retain it across appends.

func (*Buffer) Runs

func (b *Buffer) Runs() []Run

Runs returns the in-flight captures in the order they started.

func (*Buffer) StartRun

func (b *Buffer) StartRun(id int64, label string, kill func() error)

StartRun registers a capture as in flight. kill may be nil for a run that cannot be signalled.

func (*Buffer) Unread

func (b *Buffer) Unread() int

Unread is the number of events — head records — since the last MarkRead, never counting events the ring has already dropped.

A capture contributes one regardless of how many lines it emits: a 200-line dump is one failure with a lot of evidence, not 200 pieces of news.

func (*Buffer) UnreadError

func (b *Buffer) UnreadError() bool

UnreadError reports whether any record since the last MarkRead was an error and is still buffered.

It considers continuation lines too, so a capture that streams clean output and then fails on its completion line still tints the badge.

type Closed

type Closed struct{}

Closed is the value the output screen pops with.

screen.Pop fires OnEnter on the screen it uncovers, and OnEnter is the documented place to kick off a fetch — so without a sentinel, glancing at the log would silently refetch whatever was underneath. A parent that cares can early-return on this; one that doesn't can ignore it.

It lives here rather than in pkg/app because pkg/app imports pkg/output, so the reverse would be a cycle. pkg/app aliases it as app.OutputClosed for screens that would rather not take a second import.

type Keys

type Keys struct {
	Clear, Kill, Export key.Binding
}

Keys is the output screen's keymap, following CLAUDE.md rule 24: every binding the screen dispatches against lives here, carrying both its dispatch keys and its help label so Update and Help() read from one source.

Kill is "x" rather than the obvious "k": k is scroll-up everywhere in the library and rule 23 does not bend for it.

func DefaultKeys

func DefaultKeys() Keys

DefaultKeys returns the stock keymap.

func (*Keys) FillDefaults

func (k *Keys) FillDefaults()

FillDefaults fills any zero-valued binding with its DefaultKeys counterpart, so a partial override works without restating the rest. Exported to match pane.Keys, since pkg/app merges a caller's keymap across theme swaps.

type Level

type Level int

Level is an entry's severity. It drives the level tag in the rendered line and the statusbar badge's error tint.

const (
	LevelInfo Level = iota
	LevelError
)

func (Level) Tag

func (l Level) Tag() string

Tag is the three-character level marker rendered into each line.

type Notice

type Notice struct {
	Text  string
	Level Level
}

Notice asks the app shell to surface text in the statusbar.

The screen can't return app.Info directly — pkg/app imports this package, so the dependency only runs one way. The shell treats a Notice exactly as it treats StatusInfoMsg / StatusErrorMsg, which means it also lands in this buffer: the export path outlives the keypress that would have wiped it.

type Options

type Options struct {
	// MaxRecords caps the ring. 0 applies DefaultMaxRecords. Trimming is
	// event-aware: whole events are dropped from the front, so a surviving
	// body line always still has the head naming its command.
	MaxRecords int

	// ExportDir is where "w" writes. Empty falls back to os.TempDir().
	ExportDir string

	// SourceWidth is the column the source name is padded to. 0 applies
	// DefaultSourceWidth.
	SourceWidth int

	// Line colors. These are applied as foreground-only SGR (closing with
	// \x1b[39m) rather than through lipgloss, because logview's
	// CurrentLineStyle pads the current row to the pane width to paint a
	// background — a full \x1b[0m reset inside the prefix would punch a
	// hole in it. Same reasoning as CLAUDE.md rule 17 for table cells.
	TimeColor   lipgloss.TerminalColor
	InfoColor   lipgloss.TerminalColor
	ErrorColor  lipgloss.TerminalColor
	SourceColor lipgloss.TerminalColor
	GutterColor lipgloss.TerminalColor

	// BadgeStyle and BadgeErrorStyle render the statusbar affordance. They
	// live here rather than in pkg/app so the badge's colors sit with the
	// rest of the console's palette; pkg/app reads them when composing the
	// bar's right slot.
	BadgeStyle      lipgloss.Style
	BadgeErrorStyle lipgloss.Style

	// Logview configures the screen's body.
	Logview logview.Options
	// Confirm configures the kill confirmation modal.
	Confirm confirm.Options
	// Picker configures the run picker shown when more than one capture is
	// in flight.
	Picker list.Options

	// Keys is the screen's own keymap — the actions layered on top of the
	// logview's. Zero-valued bindings fall back to DefaultKeys.
	Keys Keys
}

Options configures the buffer and its screen. Build it with OptionsFrom and override individual fields.

func OptionsFrom

func OptionsFrom(t theme.Theme) Options

OptionsFrom returns Options pre-filled from a theme — the console's equivalent of the th.Component() builders, inverted for the import-cycle reason described in the package doc.

func (Options) Badge

func (o Options) Badge(b *Buffer) Badge

Badge computes the affordance state.

Two visible states rather than one: the count disappearing must not take the door with it, or a log that still has content in it becomes unreachable. So "output" bare once everything has been seen, "3 output" when it hasn't, and nothing at all only while there is genuinely nothing buffered and nothing running.

func (Options) Render

func (o Options) Render(r Record) string

Render formats one record as a display line, with the full prefix repeated on every line — including continuation lines.

The repetition is the point. logview's filter mode (\) shows only matching lines, so a body line rendered bare would surface with no timestamp, no level, and no indication of which command produced it. It costs roughly 20 columns of every line, which is the trade taken deliberately.

func (Options) RenderAll

func (o Options) RenderAll(rs []Record) []string

RenderAll formats a batch in order.

func (Options) RenderBadge

func (o Options) RenderBadge(b *Buffer) string

RenderBadge returns the styled affordance, or "" when it should not be shown. pkg/app places it in the statusbar's right slot, ahead of the version string.

func (Options) RenderPlain

func (o Options) RenderPlain(r Record) string

RenderPlain is Render without any styling, for export. A file full of SGR escapes is not what anyone wants to attach to a bug report.

type Record

type Record struct {
	// Time is wall-clock, stamped on arrival at the buffer.
	Time time.Time
	// Level drives the level tag and the badge tint.
	Level Level
	// Source is the screen title for Info/Error entries, or the command
	// label for lines from a capture.
	Source string
	// Text is the line itself, unprefixed.
	Text string
	// Head marks a summary line (rendered with ›). Continuation lines
	// (rendered with │) have Head=false. Unread events count Heads.
	Head bool
	// Stderr marks a captured line that arrived on stderr, rendered with a
	// heavier gutter glyph.
	//
	// It is deliberately not expressed as LevelError. Plenty of well-behaved
	// tools write progress to stderr, and folding that into severity would
	// leave the statusbar badge permanently red — the tint comes from the
	// run's exit status, which is the thing that actually means failure.
	Stderr bool
	// RunID is non-zero for records belonging to a runner.Capture run.
	RunID int64
}

Record is one line's worth of structure. The buffer is flat: a detail body or a captured stdout line is a Record with Head=false, not a field hanging off the summary above it. That keeps every line self-describing, which is what lets logview's filter mode (\) hide non-matching lines without orphaning a body line from the command that produced it.

type Run

type Run struct {
	ID      int64
	Label   string
	Started time.Time
	// contains filtered or unexported fields
}

Run is a capture that is currently streaming into the buffer.

The kill func is a closure rather than an *exec.Cmd so this package stays out of os/exec: the shell holds the process and hands the buffer only the ability to end it, which also makes the kill path testable without spawning anything.

type Screen

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

Screen is the console view: a logview over the buffer, plus clear, kill, and export.

It is pushed and popped rather than drawn as an overlay, so the stack handles key routing, theme propagation and esc-to-close with no new mode in the shell. The cost is a breadcrumb crumb for a place the user didn't really navigate to.

func NewScreen

func NewScreen(buf *Buffer, opts Options) *Screen

NewScreen builds the console over buf. Pass options from OptionsFrom(t).

func (*Screen) Buffer

func (s *Screen) Buffer() *Buffer

Buffer exposes the ring this screen is reading, for callers that built the screen and want to keep feeding it.

func (*Screen) Help

func (s *Screen) Help() []key.Binding

Help composes the logview's bindings with the screen's own actions, and swaps in the modal's while one is up so the hint strip tracks context.

func (*Screen) Init

func (s *Screen) Init() tea.Cmd

Init satisfies screen.Screen.

func (*Screen) IsCapturingKeys

func (s *Screen) IsCapturingKeys() bool

IsCapturingKeys reports whether something on this screen owns the keyboard — a modal, the run picker, or the logview's search filter. The shell reads it to keep its globals (including esc-pop) out of the way.

func (*Screen) Layout

func (s *Screen) Layout() layout.Node

Layout is the logview filling the body, with the kill confirmation or run picker composited on top when either is up.

func (*Screen) OnEnter

func (s *Screen) OnEnter(any) tea.Cmd

OnEnter re-syncs in case records arrived while the screen was being pushed.

func (*Screen) SetTheme

func (s *Screen) SetTheme(t theme.Theme)

SetTheme rebuilds the themed pieces, carrying the logview's query, filter mode and follow state across the swap.

Colors come back from the theme by definition, so a hand-set BadgeStyle or line color does not survive — the same bargain every other component makes under CLAUDE.md rule 4. The non-visual knobs (cap, export dir, source width, keys) are preserved.

func (*Screen) Title

func (s *Screen) Title() string

Title labels the screen in the breadcrumb.

func (*Screen) Update

func (s *Screen) Update(msg tea.Msg) (screen.Screen, tea.Cmd)

Update mirrors the buffer, then routes input. While a modal is up it owns every message and the logview sees none, per CLAUDE.md rule 20.

Jump to

Keyboard shortcuts

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