cheggaaa/pb: a terminal progress bar for Go, and how to wire it into an io.Copy loop
Console progress bar for Golang
At a glance
- What is it?
- cheggaaa/pb renders progress bars in Go terminals from a text/template, counts bytes through a proxy reader, and ships three ready-made templates. It fits streaming work; it is the wrong tool when you only know the total at the end.
- Who is it for?
- Adopt cheggaaa/pb if your Go program already knows the total number of items or bytes and writes to a terminal. Do not adopt it for a CLI that pipes output into a file or a log collector, because the default writer is os.Stderr and the bar's carriage-return rendering is meant for a terminal, and do not adopt it if you need a bar whose total changes as work is discovered.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- cheggaaa/pb3,728 ★ · +0
| Repository | 2026-09-24 | 2026-11-14 |
|---|---|---|
| cheggaaa/pb | 3.7k | 3.7k |
0 Stars in the last 51 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What cheggaaa/pb solves, and who is writing the loop that needs it
A Go program that copies 500 MiB, walks a directory, or processes a slice of records has no built-in way to tell the operator how far along it is. Printing a line per item floods the terminal; printing nothing leaves the user staring at a frozen prompt. cheggaaa/pb fills that gap with a bar that redraws in place, showing current count, total, a percentage, and a rate in items per second. The README's own sample output is `37158 / 100000 [---------------->_______________________________] 37.16% 916 p/s`, which is the whole product in one line.
The audience is Go developers writing command-line tools where the total is known up front: a fixed record count, a file size, a byte limit. It is a library, not a binary. You import it, you call Increment or wrap a reader, and you finish the bar when the loop ends. Nothing about it is a daemon, a TUI framework, or a logging system.
How the bar renders: text/template, elements, and a proxy reader for io.Copy
Rendering goes through Go's builtin text/template package, as the README states, and the available elements are listed in v3/element.go. That is a real design decision, not a detail: the bar's appearance is data, so you can pass a template string to pb.ProgressBarTemplate and get a different bar without touching the counting logic.
The `bar` element takes five standard parts in order: left border, fill, current, empty, right border. Two more parts are optional, the sixth for the empty left border and the seventh for the finished right border. The README's all-in-one example uses the cycle function to animate the fill through `↖`, `↗`, `↘`, `↙`, and pipes `speed` through `rndcolor`. Setting `UNICODE_PROGRESS_BAR=true` switches bars that use the default bar elements to built-in Unicode glyphs, on the condition that the active terminal font supports them.
The second mechanism is the proxy reader. `bar.NewProxyReader(reader)` wraps an io.Reader so that every Read call advances the bar by the number of bytes returned. That is why the IO example can hand the wrapped reader straight to io.Copy and never call Increment itself. For loops that are not byte streams, you call bar.Increment() once per unit of work instead. Both paths update the same counter; the difference is who reports progress.
Installing cheggaaa/pb v3 and running a first bar
The README gives one install command, and it targets the v3 module path:
go get github.com/cheggaaa/pb/v3A minimal program creates a bar with a known total, increments it in the loop, and finishes it. This is the README's quick start, trimmed to its essentials:
package main
import (
"time"
"github.com/cheggaaa/pb/v3"
)
func main() {
count := 100000
bar := pb.StartNew(count)
for i := 0; i < count; i++ {
bar.Increment()
time.Sleep(time.Millisecond)
}
bar.Finish()
}Run it and you should see a single line that rewrites itself, ending with a count, a percentage, and a rate. If you want the bar in a different shape from the start, the README names three templates you can substitute for StartNew: `pb.Default.Start(count)`, `pb.Simple.Start(count)`, and `pb.Full.Start(count)`.
For byte streams the pattern changes. The README's IO example uses `pb.Full.Start64(limit)` for an int64 total, then wraps the source reader:
bar := pb.Full.Start64(limit)
barReader := bar.NewProxyReader(reader)
io.Copy(writer, barReader)
bar.Finish()Before writing your own template, note the error-checking step the settings example shows: after `bar.SetTemplateString(myTemplate)`, call `bar.Err()` and return if it is non-nil. A malformed template otherwise surfaces later, at render time.
Tuning the bar: refresh rate, writer, and byte formatting
The settings example in the README covers the knobs most projects end up touching. `pb.New(count)` creates a bar without starting it, which matters when you need to configure it first. `bar.SetRefreshRate(time.Second)` changes the redraw interval; the README states the default is 200ms. `bar.SetWriter(os.Stdout)` overrides the writer, and the README states the default is os.Stderr.
That default is worth pausing on. A bar on stderr keeps stdout clean for piped data, which is the right call for a tool whose stdout is consumed by another program. It also means a user who redirects stderr to a file gets a file full of carriage-return updates rather than a tidy log.
Two formatting flags come from the same example. `bar.Set(pb.Bytes, true)` makes the bar format numbers as bytes (B, KiB, MiB and so on), and `bar.Set(pb.SIBytesPrefix, true)` switches the prefix family from IEC (B, KiB) to SI (B, kB). Those two settings are the difference between a bar that says 500 MiB and one that says 524 MB, so pick deliberately rather than by copy-paste.
Where cheggaaa/pb is the wrong tool
The library assumes you know the total before the loop starts. Every constructor in the README takes a count or a limit: `pb.New(count)`, `pb.StartNew(count)`, `pb.Full.Start64(limit)`. If your work is discovered as you go, such as a recursive walk where the file count is unknown until the walk finishes, the percentage and the total are both fiction. You can still show a spinner-like bar, but you are fighting the API's premise.
The second boundary is the terminal. The rendering relies on redrawing one line, and the default writer is os.Stderr. In a CI job that captures stderr into a log, or in a program whose output is piped, the bar becomes noise. The README does not document an automatic detection-and-disable path, so suppressing the bar in non-interactive contexts is something you arrange yourself.
The repository also splits platform files: pb_win.go, pb_plan9.go, pb_appengine.go, pb_x.go, plus pool_win.go, pool_x.go, termios_bsd.go and termios_sysv.go. That layout is normal for a Go library, but it means the code paths you exercise are not the only ones that ship. If you target an unusual platform, that is where to look first when behaviour differs.
cheggaaa/pb against mpb and uiprogress
The two names that come up alongside cheggaaa/pb are Vbauerster mpb and Gosuri uiprogress, and the split is architectural rather than cosmetic.
mpb is built around multiple bars rendered together, with a pool coordinating them. cheggaaa/pb does have pooling in this repository (pool.go, pool_win.go, pool_x.go), but its README leads with the single-bar case and the templates, and the examples shipped at the top level are example_test.go, example_copy_test.go and example_multiple_test.go. If your program runs several concurrent transfers and you want them stacked and redrawn as a group, mpb's model is the closer fit; if you want one bar with a template you control, cheggaaa/pb's text/template approach is the more direct route.
uiprogress is a different shape again: a UI container into which you place progress indicators, aimed at rendering several kinds of progress in a composed layout rather than at wrapping an io.Reader. The practical test is what your program already has. If it has an io.Reader and a byte count, cheggaaa/pb's proxy reader is a two-line integration. If it has a set of concurrent tasks that each need their own indicator inside a shared frame, that is uiprogress or mpb territory. None of the three is a drop-in for the others.
Maintenance, licence, and what upgrading costs
The repository is not archived, and the last push was on 2026-09-21, two days before this writing. That is a live codebase by the only measure available here. The repository has no published GitHub releases, so there is no version history to reason about; the go.mod declares `go 1.12`, which is the minimum language version the module claims, not a statement about current toolchains.
The dependency set is small and stable-looking: github.com/fatih/color v1.9.0, github.com/mattn/go-colorable v0.1.4, github.com/mattn/go-runewidth v0.0.4, and golang.org/x/sys v0.1.0. Color and runewidth are the ones you would notice if they moved, since they touch rendering and character width. The pinned versions in go.mod mean your build is reproducible until you run a module upgrade.
The licence is BSD-3-Clause. That is a permissive licence with a standard attribution clause, and the repository carries a LICENSE file at the top level. Whether that fits your distribution model, particularly if you ship a closed binary that embeds the library, is a question for your own legal review rather than something the README answers.
The upgrade cost is concentrated in one place. v1 documentation lives in README_V1.md, and the README points v1 users there, while the current module path is github.com/cheggaaa/pb/v3. Code written against v1 does not import the same path, so a migration is a code change, not a version bump in go.mod alone. If you are starting fresh, start on v3 and skip the question.
Editorial conclusion
Adopt cheggaaa/pb if your Go program already knows the total number of items or bytes and writes to a terminal. Do not adopt it for a CLI that pipes output into a file or a log collector, because the default writer is os.Stderr and the bar's carriage-return rendering is meant for a terminal, and do not adopt it if you need a bar whose total changes as work is discovered. Before wiring it in, verify two things: whether your build targets plan9 or appengine, since the repository carries pb_plan9.go, pb_appengine.go, pb_win.go and pb_x.go as separate files, and whether you need the v1 API, which lives on in README_V1.md rather than in v3.
Frequently asked questions
What is the purpose of a progress bar?
In cheggaaa/pb the bar reports how much of a known total has been processed, showing the current count, the total, a percentage and a rate such as 916 p/s. It exists for command-line programs where the operator would otherwise see no output until the work finished.
Are progress bars accurate?
Only as accurate as the total you give them. Every constructor in the README takes a count or limit, and the bar's percentage is derived from that fixed number, so an incorrect or unknown total produces a misleading percentage.
What are the different types of progress bars in cheggaaa/pb?
The README provides three built-in templates, reachable as pb.Default, pb.Simple and pb.Full, each started with a count or an int64 limit. Beyond those, you can supply your own template string through pb.ProgressBarTemplate or bar.SetTemplateString.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cheggaaa-pb)Star history chart for your README
[](https://hysenlabs.com/projects/cheggaaa-pb)Was this useful?