Documentation
¶
Overview ¶
Package cliout is the bubble-tea-free core of the interactive terminal output experience for long-running sdd commands. It carries the log pipe (a slog.Handler whose records flow over a bounded channel to a display consumer), an absolute-count progress reporter, and the durable-record policy that decides which ephemeral entries survive teardown.
Producers stay oblivious: handlers and finders log only through the context logger and never import this package. The CLI surface installs the pipe's handler when stderr is an interactive terminal and drives the transient view (in the sibling internal/cliout/tui package); otherwise it leaves the plain leveled stderr handler in place. This is the audience separation the terminal-experience architecture directive (d-cpt-mvb) draws — terminal-UI code out of the producer layers.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsInteractive ¶
IsInteractive reports whether f is an interactive terminal suitable for a transient TUI. It returns false for non-terminals (pipes, files, /dev/null, agent stdio) and when NO_COLOR is set — both take the plain leveled path, so agents and piped consumers never see an alt-screen program. term.IsTerminal is used rather than a file-mode check because special devices like /dev/null are character devices but not terminals, and bubble tea opens /dev/tty directly and fails in non-interactive contexts.
func WriteEntries ¶
WriteEntries re-logs entries through the durable sink (typically the pre-swap slog.Default), reconstructing each record with its original level, message, and attributes. The sink's own level filter and formatting apply.
Types ¶
type FingersCrossed ¶
type FingersCrossed struct {
// Trigger is the level that arms the flush when observed.
Trigger slog.Level
// Tail is the number of most-recent entries (all captured levels) kept
// for the flush.
Tail int
}
FingersCrossed configures the failure-triggered tail flush.
type LogConsumer ¶
type LogConsumer struct {
// contains filtered or unexported fields
}
LogConsumer is the display end of a log pipe. Recv hands the next entry to the view loop; Close signals end-of-work so a drained Recv reports done.
func NewLogPipe ¶
func NewLogPipe(capture slog.Leveler) (slog.Handler, *LogConsumer)
NewLogPipe builds a log pipe: a slog.Handler the caller installs for the operation, and a LogConsumer the display loop drains. capture is the floor level admitted into the pipe — the display may filter higher, and the policy recorder needs everything down to this floor (see Policy.CaptureFloor). Neither end references a bubble tea program, so there is no construction cycle between the pipe and the model that consumes it.
func (*LogConsumer) Close ¶
func (c *LogConsumer) Close()
Close signals that no further entries will be produced. Idempotent. After Close and once the channel is drained, Recv returns ok=false.
func (*LogConsumer) Recv ¶
func (c *LogConsumer) Recv() (LogEntry, bool)
Recv returns the next entry, or ok=false once work has finished and the channel is drained. Buffered entries are always preferred over the done signal so the tail isn't dropped at teardown.
type LogEntry ¶
LogEntry is a snapshot of a slog.Record taken at Handle time. The record itself must not be retained past Handle, so the level, message, and the fully-accumulated attribute set (including any WithAttrs / WithGroup state) are copied out here. Rendered with structured styling at display time, not flattened to a string at capture.
type Policy ¶
type Policy struct {
// Display is the level floor for the live view — typically chattier than
// the durable floor, since the view is transient.
Display slog.Leveler
// KeepAtOrAbove re-emits entries at or above this level to the durable
// sink on teardown, independent of the live display level.
KeepAtOrAbove slog.Level
// FingersCrossed, when set, retains a tail of recent entries at all
// captured levels and flushes them to the durable sink if an error
// occurs — so the context around a failure survives even though it was
// never shown.
FingersCrossed *FingersCrossed
}
Policy decouples the durable record from the ephemeral display. The live view can be chatty (it vanishes on teardown), while only deliberately kept entries survive to the durable stderr sink.
func (Policy) CaptureFloor ¶
CaptureFloor is the minimum level the log pipe must admit so the policy can do its job: the display floor, the keep threshold, and — when fingers-crossed is armed — everything down to Debug, since a flush must be able to show context that was never displayed.
type Progress ¶
type Progress struct {
Done int
Total int
Unit string // optional noun for rendering, e.g. "chunks"
Note string // optional live status of the work in flight, e.g. "embedding 4 entries · 37 chunks"
}
Progress is an absolute snapshot of how far an operation has come. Because every snapshot carries the running totals (not a delta), a dropped update self-corrects: the next one overwrites it with the true state.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder accumulates the entries a Policy will re-emit to the durable sink on teardown. It is fed every entry that enters the pipe (Observe) and produces the re-emit list (Flush). Fully independent of the display loop and of bubble tea, so it is unit-testable on its own.
func NewRecorder ¶
NewRecorder builds a recorder for the given policy.
func (*Recorder) Flush ¶
Flush returns the entries to re-emit to the durable sink, in arrival order and deduplicated. Always includes the kept entries; when fingers-crossed is armed it additionally includes the retained tail (all captured levels).
func (*Recorder) MarkFailed ¶
func (r *Recorder) MarkFailed()
MarkFailed arms the fingers-crossed flush from outside the log stream — used when the operation returns an error without having logged at the trigger level.
type Reporter ¶
type Reporter struct {
// contains filtered or unexported fields
}
Reporter receives absolute progress updates from the work goroutine and hands the latest to the display loop. It is a single-producer mailbox: the callbacks that drive it (OnBatchStart, OnEntryIndexed, …) fire sequentially from one goroutine, while the view loop is the sole consumer.
func NewReporter ¶
func NewReporter() *Reporter
NewReporter builds a reporter with an empty mailbox.
func (*Reporter) Close ¶
func (r *Reporter) Close()
Close signals that no further progress will be reported. Idempotent.
func (*Reporter) Recv ¶
Recv returns the next progress snapshot, or ok=false once Close has been called and no snapshot is pending. A pending snapshot is preferred over the close signal so the final state isn't dropped.
func (*Reporter) SetNote ¶ added in v0.14.0
SetNote sets a short live description of the work currently in flight (e.g. "embedding 4 entries · 37 chunks") and publishes a snapshot. Pass "" to clear it. Independent of the count so the view can name what's being processed while the determinate bar tracks how much work remains.