cliout

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: MIT Imports: 7 Imported by: 0

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

func IsInteractive(f *os.File) bool

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

func WriteEntries(ctx context.Context, sink *slog.Logger, entries []LogEntry)

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

type LogEntry struct {
	Time    time.Time
	Level   slog.Level
	Message string
	Attrs   []slog.Attr
}

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

func (p Policy) CaptureFloor() slog.Level

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.

func (Policy) ShowInDisplay

func (p Policy) ShowInDisplay(level slog.Level) bool

ShowInDisplay reports whether an entry at the given level belongs in the live view.

type Progress

type Progress struct {
	Done  int
	Total int
	Unit  string // optional noun for rendering, e.g. "entries"
}

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.

func (Progress) Ratio

func (p Progress) Ratio() float64

Ratio returns Done/Total clamped to [0,1], or 0 when Total is unknown.

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

func NewRecorder(p Policy) *Recorder

NewRecorder builds a recorder for the given policy.

func (*Recorder) Failed

func (r *Recorder) Failed() bool

Failed reports whether the fingers-crossed flush is armed.

func (*Recorder) Flush

func (r *Recorder) Flush() []LogEntry

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.

func (*Recorder) Observe

func (r *Recorder) Observe(e LogEntry)

Observe records an entry that entered the pipe. Entries at or above the keep threshold are retained for re-emit; when fingers-crossed is armed the entry also enters the tail ring, and an entry at the trigger level arms the flush.

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) Add

func (r *Reporter) Add(n int)

Add advances the completed count by n and publishes a snapshot.

func (*Reporter) Close

func (r *Reporter) Close()

Close signals that no further progress will be reported. Idempotent.

func (*Reporter) Recv

func (r *Reporter) Recv() (Progress, bool)

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) SetTotal

func (r *Reporter) SetTotal(n int)

SetTotal records the operation's total work units and publishes a snapshot.

func (*Reporter) SetUnit

func (r *Reporter) SetUnit(unit string)

SetUnit sets the noun used when rendering the count (e.g. "entries").

Directories

Path Synopsis
Package tui drives the transient interactive terminal view for long-running sdd commands.
Package tui drives the transient interactive terminal view for long-running sdd commands.

Jump to

Keyboard shortcuts

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