# CLI UX best practices: 3 patterns for improving progress displays

> 3 popular UI patterns for keeping users informed about ongoing processes: the spinner, the X of Y pattern, and the progress bar, the pros and cons of each, and tips for choosing the one that best suits your needs.

- Date: 2024-04-15T00:00:00.000Z
- Authors: Roman Shamin, Travis Turner
- Categories: Design, DX
- URL: https://evilmartians.com/chronicles/cli-ux-best-practices-3-patterns-for-improving-progress-displays

---

Enhance the way your CLIs report progress to users. While there are many developer experience improvements we could make to most command-line apps, I see this one as absolutely essential. If you are creating an app or a shell script for a terminal emulator and you’re pressed for time but can only squeeze in one more improvement, make sure it is how your app displays progress for long-running processes.

No matter how fast computers get, there will always be tasks that will take some noticeable amount of time. Compiling source code might be quick for a small Go application, but can drag for hours for huge applications with C++ codebase. Download times? Those are all about your connection bandwidth. And the list goes on.

*Read also Evil Martians' article*: https://evilmartians.com/chronicles/devs-in-mind-how-to-design-interfaces-for-developer-tools

In this article, I'll take a look at three popular user interface patterns for keeping users informed about ongoing processes: the spinner, the X of Y pattern, and the progress bar. We'll look at the pros and cons of each of them and I'll share tips for choosing the one that best suits your needs.

## Here's why it matters: control and user diversity

First off, people love feeling in control—it's a psychological thing. In user interfaces, there's something satisfying about monitoring actions like code compilation or file downloads. Even if we can't change the outcome, just seeing the progress gives us a sense of control.

“Converging evidence from animal research, clinical studies, and neuroimaging work suggest that the need for control is a biological imperative for survival”

[Born to Choose: The Origins and Value of the Need for Control](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2944661/)

Second, don't assume command-line apps are only for the stereotypical bearded Linux users. There's a new generation of developers using your CLIs daily for compiling, building, and deploying apps. And folks who've been using laptops and smartphones since they were in diapers expect a lot from software UX.

That's why it's crucial for any interface, graphical or command-line, to provide updates on ongoing actions. It's simply good UX practice. Now, let's see them in action.

## Avoid the silent treatment

Sure, there are cases when a command-line app should be quiet; take a compiler that's typically run through a build system, like how `rustc`—the Rust compiler—is done, often executed by `cargo`. In scenarios like this, it's fine for a compiler to maintain its relative silence.

Consider adding a `--quiet` flag to offer a silent mode for your app.

[CLI Guidelines](https://clig.dev/#arguments-and-flags)

> But in most other situations, I strongly advise against leaving users staring at a blinking cursor on a dark terminal screen. Instead, make an effort to display meaningful status updates for actions that take time. Additionally, ensure a neat and readable log is left behind once an app has finished its job.

Seriously, take this advice to heart, and treat your users with care by implementing one of the UI patterns suggested here to elevate the user experience of your CLI.

## Spinners: for when you're in the dark

Adding a spinning animation with a status line is a simple way to show that something is happening, especially during a lengthy process.

Here's an example showing how you could implement a spinner in Go using Charm's [`huh?`](https://github.com/charmbracelet/huh?tab=readme-ov-file#bonus-spinner) library:

```go
// import ("github.com/charmbracelet/huh/spinner")

_ = spinner.New().
  Title("Saving config file...").
  Action(longOperation).
  Run()
```

However, the simplicity the spinner offers also has its downsides: a basic spinner doesn't offer much insight into the progress, and even more critically, it won't alert the user if the app gets stuck in a loop.

In the command-line realm, a popular solution for the last problem is to program your spinner to update only when a specific action has been completed. For instance, if your app is processing files, the spinner could tick each time it finishes processing another file. This way, the spinner not only shows ongoing activity but also signals if the process has been stuck for too long, thus indicating a potential issue.

*Image: Braille alphabet characters are quite a popular choice for spinners*

So, when is using a spinner your best bet?

- For one or a few sequential tasks that should wrap up quickly, that is, in just a few seconds.
- When there's no extra information to display (more on this in the next section).

## The X of Y pattern: ideal when you have data

I'm really keen on making this pattern everyone's go-to choice. Although there are some cases where it is unfeasible or overly burdensome to gather data, developers can typically access some metrics about the action in progress.

The standard is the `X / Y` format: display what has been completed (X) and, when feasible, the total that needs to be completed (Y).

Implementing this pattern with the [`uilive`](https://github.com/gosuri/uilive) Go library is straightforward and only requires a few lines of code.

```go
// import ("github.com/gosuri/uilive")

fmt.Println("Downloading libs...")
writer := uilive.New()
writer.Start()

for lib, count := range libs {
  for i := 0; i <= count; i++ {
    _, _ = fmt.Fprintf(writer, "  %s (%d / %dKB)\n", lib, i, count)
    // time.Sleep(time.Millisecond * 25)
  }
  _, _ = fmt.Fprintf(writer.Bypass(), "✓ %s\n", lib)
}

writer.Stop()
```

This approach is spot-on for reporting progress. If the numbers are climbing, you're on track; if they're static, this means there might be a hiccup somewhere. Further, if you've got the Y value, we can help users roughly estimate the remaining wait time. So, why not add a spinner for that extra flair? It might just the finishing touch that makes everything pop!

In short, opt for the X of Y pattern whenever you're handling step-by-step processes and can measure the progress of each, and make it your standard!

## Progress bars: for multiple simultaneous processes

The progress bar really shines when it builds upon the X of Y pattern. And it really takes things up a notch! Users get all the info they had before, plus a visual gauge of progress that helps with ballpark time estimates.

Here’s how multiple simultaneous progress bars could be rendered via the [Multi Progress Bar](https://github.com/vbauerster/mpb) lib:

```go
// import (
//  "github.com/vbauerster/mpb/v8"
//	"github.com/vbauerster/mpb/v8/decor"
// )

fmt.Println("Downloading libs...")
var wg sync.WaitGroup
p := mpb.New(mpb.WithWaitGroup(&wg))
wg.Add(numBars)

for lib, count := range libs {
  name := fmt.Sprintf("  %s", lib)
  bar := p.New(int64(count),
    mpb.BarStyle().Lbound(" ").Filler("█").Tip("█").Padding("░").Rbound(" "),
    mpb.PrependDecorators(
      decor.Name(name, decor.WCSyncSpaceR),
      decor.CurrentNoUnit("%d /", decor.WCSyncSpace),
      decor.TotalNoUnit("%dKB", decor.WCSyncSpace),
    ),
    mpb.AppendDecorators(
      decor.OnComplete(
        decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "Done",
      ),
    ),
  )

  // simulating some work
  go func() {
    defer wg.Done()
    // ...
  }()
}

p.Wait()
```

> While progress bars are a GUI standard, in the CLI world, they might be overkill. My advice? Think twice before you decide to use a progress bar.

Progress bars are best suited when used for several lengthy, similar processes running in parallel. In these cases, keeping an eye on multiple X of Y numbers can get tricky, and that's where visual gauges come in handy. If you’re just dealing with a single task or a couple of steps in sequence, it might be better to skip the bars.

Another popular solution to avoid overwhelming users with too many progress bars is to use a single progress bar that reports the overall progress of multiple actions, rather than having one bar for each action.

*Image: A single progress bar to show total progress*

This example is inspired by the Charm's [package manager](https://github.com/charmbracelet/bubbletea/tree/master/examples/package-manager) demo.

## P. S. Keep the log clean

Before we go into logs just a bit, first off, get in the habit of clearing spinners and progress bars once an action is completed. Most libraries automatically delete them, but just make sure that's happening.

Second, aim for output that tells a clear, detailed story of the command's execution: use green colors and checkmark icons to make successful outcomes pop out at a glance.

Also, under certain conditions, like when the `--no-color` flag is used or the `NO_COLOR` environment variable has been set, you might want to disable colors in the output.

[CLI Guidelines](https://clig.dev/#output)

Check how your app's standard output stream looks in a text file by redirecting it:

```sh
$ app > file.txt
```

Also, do keep in mind that one command's output might be another's input via the pipe operator (`|`). It's very likely someone will want to `grep` through your CLI's output, so make sure your app doesn't leave behind any clutter.

If you can't offer machine-readable output in any other way, think about adding a `--plain` flag to your CLI.

[CLI Guidelines](https://clig.dev/#output)

And a personal plea: don't skip updating your status messages from `ing`’s to `ed`’s. CLIs often use the gerund form for ongoing actions: download-_ing_, creat-_ing_, etc. These make sense during the action, but once it's done, they're misleading. The log should switch to past simple or present perfect tense to accurately reflect completed actions. Revisit the video examples in this article—they all adhere to this practice.

---

I'm fairly confident the CLI world is being neglected because user experience designers aren't paying enough attention. Startups often pour effort into enhancing web app UX, but CLIs, which are crucial for everyday developer tasks, don't seem to get the same focus.

[Tell us](https://evilmartians.com/contact-us) about your command-line app's goals and challenges, and we'll offer practical, time-tested advice for your product—all at no cost!

---

This article was written by [Evil Martians](https://evilmartians.com), a product development consultancy trusted by teams from early-stage startups to public companies. If you're working through a hard problem, [we can help](https://evilmartians.com/contact-us)
