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
- type Badge
- type Buffer
- func (b *Buffer) Append(r Record)
- func (b *Buffer) AppendAll(rs []Record)
- func (b *Buffer) Clear()
- func (b *Buffer) EndRun(id int64)
- func (b *Buffer) Epoch() int
- func (b *Buffer) InFlight() int
- func (b *Buffer) Kill(id int64) error
- func (b *Buffer) Len() int
- func (b *Buffer) MarkRead()
- func (b *Buffer) Records() []Record
- func (b *Buffer) Runs() []Run
- func (b *Buffer) StartRun(id int64, label string, kill func() error)
- func (b *Buffer) Unread() int
- func (b *Buffer) UnreadError() bool
- type Closed
- type Keys
- type Level
- type Notice
- type Options
- type Record
- type Run
- type Screen
- func (s *Screen) Buffer() *Buffer
- func (s *Screen) Help() []key.Binding
- func (s *Screen) Init() tea.Cmd
- func (s *Screen) IsCapturingKeys() bool
- func (s *Screen) Layout() layout.Node
- func (s *Screen) OnEnter(any) tea.Cmd
- func (s *Screen) SetTheme(t theme.Theme)
- func (s *Screen) Title() string
- func (s *Screen) Update(msg tea.Msg) (screen.Screen, tea.Cmd)
Constants ¶
const DefaultMaxRecords = 10000
DefaultMaxRecords is the ring cap applied when Options.MaxRecords is 0, matching logview.DefaultMaxLines.
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 ¶
NewBuffer returns an empty buffer capped at max records. max <= 0 applies DefaultMaxRecords.
func (*Buffer) Append ¶
Append adds one record, stamping Time when the caller left it zero, and trims if the ring is over cap.
func (*Buffer) AppendAll ¶
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) Epoch ¶
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 ¶
InFlight is the number of captures currently streaming. The badge renders a static marker while this is non-zero.
func (*Buffer) Kill ¶
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) 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 ¶
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) StartRun ¶
StartRun registers a capture as in flight. kill may be nil for a run that cannot be signalled.
func (*Buffer) Unread ¶
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 ¶
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 ¶
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 (*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.
type Notice ¶
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 ¶
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 ¶
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 ¶
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) RenderBadge ¶
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 ¶
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 (*Screen) Buffer ¶
Buffer exposes the ring this screen is reading, for callers that built the screen and want to keep feeding it.
func (*Screen) Help ¶
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) IsCapturingKeys ¶
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 ¶
Layout is the logview filling the body, with the kill confirmation or run picker composited on top when either is up.
func (*Screen) OnEnter ¶
OnEnter re-syncs in case records arrived while the screen was being pushed.
func (*Screen) SetTheme ¶
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.