Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

111 lines
3.4 KiB
Go

package progress
import (
"context"
"io"
)
// Spinner is the single-operation face of the Tracker, kept for the many
// call sites that want "animate this one label, then ✓ or ✗". It is a thin
// facade: on a capable terminal the Tracker renders a live compact header
// (spinner, title, status, elapsed) and turns Reporter stage transitions
// into a checklist of step rows; elsewhere it degrades to clean line output.
// Implements Reporter so it can be installed via WithReporter.
type Spinner struct {
t *Tracker
}
// NewSpinner constructs a Spinner bound to w. When w isn't a TTY (or NO_COLOR
// / TERM=dumb / CI is set), the spinner is created in plain-text mode.
func NewSpinner(w io.Writer) *Spinner {
return &Spinner{t: NewTracker(w)}
}
// Disable forces the spinner into plain-text mode. Effective before Start.
func (s *Spinner) Disable() { s.t.disable() }
// Enabled reports whether the spinner is animating.
func (s *Spinner) Enabled() bool { return s.t.Animated() }
// Start begins animating with the given label.
func (s *Spinner) Start(label string) { s.t.Start(label) }
// Set updates the label and sub-status mid-animation. Either may be empty to
// leave the existing value.
func (s *Spinner) Set(label, sub string) {
s.t.SetTitle(label)
s.t.SetStatus(sub)
}
// Report implements Reporter. Each distinct stage becomes its own live step
// row (with counters and, when the total is known, a progress bar); in plain
// mode stages print as start / finish line pairs with a slow heartbeat.
func (s *Spinner) Report(stage string, current, total int) {
s.t.Report(stage, current, total)
}
// Done stops the spinner and replaces the frame with a green ✓ summary.
func (s *Spinner) Done() { s.t.Done("", "") }
// Fail stops the spinner and replaces the frame with a red ✗ summary.
func (s *Spinner) Fail(err error) { s.t.Fail(err) }
// Tracker exposes the underlying tracker for call sites that outgrow the
// single-label surface (explicit steps, log lines above the animation).
func (s *Spinner) Tracker() *Tracker { return s.t }
// Multi fans out reporter ticks to all of rs. Nil entries are skipped.
func Multi(rs ...Reporter) Reporter {
out := make([]Reporter, 0, len(rs))
for _, r := range rs {
if r == nil {
continue
}
out = append(out, r)
}
switch len(out) {
case 0:
return Nop{}
case 1:
return out[0]
default:
return multiReporter(out)
}
}
type multiReporter []Reporter
func (m multiReporter) Report(stage string, current, total int) {
for _, r := range m {
r.Report(stage, current, total)
}
}
// Run animates a spinner around fn. The context passed to fn carries the
// spinner as a Reporter, so any progress.FromContext(ctx).Report(…) inside fn
// drives the live step rows. The spinner is finished (✓ or ✗) before Run
// returns.
func Run(ctx context.Context, w io.Writer, label string, fn func(context.Context) error) error {
sp := NewSpinner(w)
return runWith(ctx, sp, label, fn)
}
// RunDisabled is Run with the spinner forced into plain-text mode.
func RunDisabled(ctx context.Context, w io.Writer, label string, fn func(context.Context) error) error {
sp := NewSpinner(w)
sp.Disable()
return runWith(ctx, sp, label, fn)
}
func runWith(ctx context.Context, sp *Spinner, label string, fn func(context.Context) error) error {
sp.Start(label)
ctx = WithReporter(ctx, sp)
err := fn(ctx)
if err != nil {
sp.Fail(err)
} else {
sp.Done()
}
return err
}