A command that runs for more than a second shows how far it got. This
guide adds the flag --progress, the displays, the event log and the
summary to the main of a program.
✓ fetching the base image 5.0s 2.0 GiB at 410 MiB/s reinstall ◦ 0:10.5 ╎ reinstalling the nodes ◉◉◉◉◉◌◌◌◌◌ 6/12 ◦ 4 running ◦ 2 queued ╎ ◡ exe07 2.8s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe08 2.1s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe09 1.4s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe10 0.7s/30s POST /redfish/v1/Systems/1 ╎ ✓ exe[01-06]
Procedure#
- Read the flag and the variable that stands in for it, such as
CLUSTERCTL_PROGRESS. Put them in acliprogress.Setting. - Ask the system about standard error:
cliprogress.IsTerminal,cliprogress.TerminalSize,cliprogress.InForegroundandcliprogress.UTF8Locale. - Pick the theme, and let
cliprogress.ThemeFortake out the colours thatNO_COLORandTERMforbid. - Call
cliprogress.Startwith a context that ends on the interrupt. An error is a usage error: report it as such. - Put the writer of your logger through
cliprogress.Session.Lines, and your own writes to the terminal throughcliprogress.Session.Writer. - Start the command’s span in
cliprogress.Session.Context, run the work, and end the span with the error. - Call
cliprogress.Session.Finish. Then put your writers back, and write the error.
flags := flag.NewFlagSet("clusterctl", flag.ContinueOnError)
mode := flags.String("progress", "", "how to show progress: auto, tty, counter, plain or none")
if err := flags.Parse(os.Args[1:]); err != nil {
return
}
given := false
flags.Visit(func(f *flag.Flag) { given = given || f.Name == "progress" })
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
theme, _ := display.ParseTheme("tide")
onTerminal := cliprogress.IsTerminal(os.Stderr)
session, err := cliprogress.Start(ctx, cliprogress.Options{
Program: "clusterctl",
Mode: cliprogress.Setting{
Flag: "--progress", Given: given, Value: *mode,
Variable: "CLUSTERCTL_PROGRESS", Env: os.Getenv("CLUSTERCTL_PROGRESS"),
},
Stderr: os.Stderr,
OnTerminal: onTerminal,
Dumb: os.Getenv("TERM") == "dumb",
Size: func() (int, int, error) { return cliprogress.TerminalSize(os.Stderr) },
Foreground: func() bool { return cliprogress.InForeground(os.Stderr) },
ASCII: !cliprogress.UTF8Locale(os.Getenv),
Theme: cliprogress.ThemeFor(theme, onTerminal, os.Getenv),
Noun: func(n int) string {
if n == 1 {
return "host"
}
return "hosts"
},
})
if err != nil {
fmt.Fprintln(os.Stderr, "clusterctl:", err) // a usage error
return
}
// The logger's lines go through the display while it draws.
log.SetOutput(session.Lines(os.Stderr))
ctx, cmd := progress.Start(session.Context(), progress.KindCommand, "reinstall")
err = reinstall(ctx, flags.Args())
cmd.End(err)
session.Finish(err != nil)
log.SetOutput(os.Stderr)
if err != nil {
fmt.Fprintln(os.Stderr, "clusterctl:", err)
}Example_mainWithProgress in internal/docsite/example_test.go. CI compiles it.CAUTION
End the command’s span before you call Finish. Finish closes the Bus,
and a span that is open then ends as canceled.
What the user gets#
--progress | Standard error shows |
|---|---|
auto (the default) | the tree on a terminal, nothing elsewhere |
tty | the tree, or the counter on a terminal too small for it |
counter | one line, redrawn |
plain | one line for each change worth one, also in a log |
none | what it shows without the kit, byte for byte |
The words, and their order, are an interface (decision 21). The texts of
the errors and notes that cliprogress writes are an interface too.
clock := time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC)
r, err := cliprogress.Start(context.Background(), cliprogress.Options{
Program: "prog",
Mode: cliprogress.Setting{Flag: "--progress", Given: true, Value: "plain"},
Stderr: os.Stdout,
Now: func() time.Time { return clock },
})
if err != nil {
fmt.Println(err) // a usage error
return
}
ctx, cmd := progress.Start(r.Context(), progress.KindCommand, "delete cluster")
_, step := progress.Start(ctx, progress.KindStep, "mesh")
clock = clock.Add(2 * time.Second)
failed := errors.New("network sind-mesh has active endpoints")
step.End(failed)
cmd.End(failed)
r.Finish(true)[0:00] delete cluster › mesh: start
[0:02] delete cluster › mesh: failed in 2.0s
prog: delete cluster: failed in 2.0sprogress/cliprogress/example_test.go. CI runs it and checks its output.Ask a question#
A question waits for its answer with no display drawn over it.
cliprogress.Ask takes the displays off the terminal
while it waits, and cliprogress.Yes reads the
answer.
answer, err := cliprogress.Ask(context.Background(), strings.NewReader("y\n"), os.Stdout, "Delete 3 hosts? [y/N] ")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(cliprogress.Yes(answer))Delete 3 hosts? [y/N] trueprogress/cliprogress/example_test.go. CI runs it and checks its output.