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 ¶
- Variables
- func IsInteractive(f *os.File) bool
- func PhaseLabel(p model.Phase) string
- func RenderCount(p Progress) string
- func RenderEntry(e LogEntry) string
- func WriteEntries(ctx context.Context, sink *slog.Logger, entries []LogEntry)
- type Coordinator
- type CoordinatorConfig
- type DisplayStarter
- type FingersCrossed
- type LogConsumer
- type LogEntry
- type Policy
- type Progress
- type Recorder
- type Reporter
- func (r *Reporter) Add(n int)
- func (r *Reporter) Close()
- func (r *Reporter) Notify(fn func(Progress))
- func (r *Reporter) Recv() (Progress, bool)
- func (r *Reporter) SetNote(note string)
- func (r *Reporter) SetPhase(phase model.Phase)
- func (r *Reporter) SetTotal(n int)
- func (r *Reporter) SetUnit(unit string)
Constants ¶
This section is empty.
Variables ¶
var ( StyleLabel = styles.Heading // operation label StyleBody = styles.Body // message text )
Footer chrome + log-level styles, drawn from the shared palette so the coordinator's output reads consistently with the presenters surface.
var ErrUserCancelled = errors.New("cancelled by user")
ErrUserCancelled is the sentinel a coordinator returns when the user interrupts the work (ctrl+c cancels the work context). The top-level CLI handler maps it to a calm "cancelled." message and exit 130 — no raw "context canceled" string ever reaches the user.
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 PhaseLabel ¶ added in v0.17.0
PhaseLabel is the footer text for a phase. Rendering lives here, not on the domain type, so internal/model stays dependency-free display vocabulary.
func RenderCount ¶ added in v0.17.0
RenderCount formats the absolute progress count beside the spinner, e.g. "42/120 entries". Empty when no total is known and nothing has completed.
func RenderEntry ¶ added in v0.17.0
RenderEntry formats one log entry as a styled line: level badge, message, then space-separated key=value attributes. It is the single line renderer for both the plain stderr path (dormant/armed) and the live tea.Printf path.
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 Coordinator ¶ added in v0.17.0
type Coordinator struct {
// contains filtered or unexported fields
}
Coordinator owns the terminal-output lifecycle for one long-running command. It is the slog.Handler installed on the work context (once, via slogutils.WithLogger — no global logger swap), routing each record by state: plain to stderr while dormant, buffered while armed, and into the running program while live. The Recorder observes every record for the teardown re-emit, independent of what the display does.
func NewCoordinator ¶ added in v0.17.0
func NewCoordinator(cfg CoordinatorConfig) *Coordinator
NewCoordinator builds a dormant coordinator. Call SetStarter before Run.
func (*Coordinator) Interrupt ¶ added in v0.17.0
func (c *Coordinator) Interrupt()
Interrupt cancels the work context. Both the SIGINT handler (dormant/armed, terminal in cooked mode) and the live program's ctrl+c key path converge here; the resulting context.Canceled from work is translated to ErrUserCancelled.
func (*Coordinator) Run ¶ added in v0.17.0
Run installs the coordinator as the context logger, launches work on its own goroutine, and drives the lifecycle: it starts the program if the armed debounce expires, or returns without one if work finishes first. On teardown it re-emits kept / fingers-crossed entries to the durable sink for footer-only views. A context.Canceled from work surfaces as ErrUserCancelled.
func (*Coordinator) SetStarter ¶ added in v0.17.0
func (c *Coordinator) SetStarter(s DisplayStarter)
SetStarter injects the live-program starter (provided by internal/cliout/tui).
type CoordinatorConfig ¶ added in v0.17.0
type CoordinatorConfig struct {
Policy Policy
Stderr io.Writer
StreamLogs bool
Debounce time.Duration
// Progress, when set, arms the coordinator on its first published event so
// the footer appears for progress-only work (no display-eligible log yet).
Progress *Reporter
}
CoordinatorConfig configures a Coordinator. Debounce defaults to armDebounce when zero; Stderr defaults to a no-op writer when nil.
type DisplayStarter ¶ added in v0.17.0
type DisplayStarter interface {
Start(backlog []LogEntry, live *LogConsumer) (unpainted []LogEntry, err error)
}
DisplayStarter runs the live terminal program. The coordinator owns the decision to start it (armed debounce expiry) and calls Start once, on the driver goroutine, so it blocks there until the program exits. backlog holds the display lines buffered before the program existed (its opening backlog); live delivers subsequent lines, and its Close signals end-of-work so the program quits. Start returns any lines it never painted (work finished before the first-paint gate) so the coordinator can print them plainly after teardown. Implemented by internal/cliout/tui — this interface keeps cliout free of any bubble tea import.
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 NewLogConsumer ¶ added in v0.17.0
func NewLogConsumer(capacity int) *LogConsumer
NewLogConsumer builds a consumer over a channel of the given capacity. The coordinator uses this to hand live lines to a running program; Offer feeds it.
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) Offer ¶ added in v0.17.0
func (c *LogConsumer) Offer(e LogEntry)
Offer sends an entry non-blocking: a full channel drops it rather than stalling the producer, bounding time coupling between producer and display.
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"
Phase model.Phase // active stage; drives the footer label and arms the coordinator
}
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) Notify ¶ added in v0.17.0
Notify registers a hook fired with each published snapshot — the coordinator uses it to arm on the first real progress event. Single-subscriber: a later call replaces the hook.
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.
func (*Reporter) SetPhase ¶ added in v0.17.0
SetPhase records the operation's active stage and publishes a snapshot. A non-empty phase is a display-worthy event: it declares real work, so it arms the coordinator even before any total is known (the footer label derives from it). Sticky until the next call — pass "" only to clear it.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package tui is sdd's bubble tea layer: the transient coordinator display for long-running commands (Interactive, which lets a cliout.Coordinator own the dormant → armed → live → done lifecycle so instant work never starts a program) and the reusable init input prompts (text, select, multi-select, confirm).
|
Package tui is sdd's bubble tea layer: the transient coordinator display for long-running commands (Interactive, which lets a cliout.Coordinator own the dormant → armed → live → done lifecycle so instant work never starts a program) and the reusable init input prompts (text, select, multi-select, confirm). |