An asynchronous, multitask console/terminal progressbar widget. The main look of default stepper is:
Its original sample is pip installing ui, or python rich-like progressbar.
To simplify our maintaining jobs, this repo was only tested at go1.18+.
We're happy to annouce the new v2 released.
This is a seamless upgrade for your legacy v1 codes.
But new NewV2(opts...) will take a fully-rewritten progressbar and a better accurate behavior to you.
In our old v1 releases, some tasks ended but its bar stunned at 99.x%, we guess that you'd like to see a 100% fully-completed bar, right? The little trouble is gone now.
In other sides, we keep the codes under the control of reusing and isolations, which improvements would bring a most stable progressbar for the repeatable, reuseable jobs of yours.
- See the CHANGELOG
Since v1.2.5, the minimal toolchain upgraded to go1.23.7.
Since v1.2, we upgrade and rewrite a new implementation of GPB so that we can provide grouped progressbar with titles. It stay in unstabled state but it worked for me. Sometimes you can rollback to v1.1.x to keep the old progrmatic logics unchanged.
- See the CHANGELOG
progressbar provides a friendly interface to make things simple, for creating the tasks with a terminal progressbar.
It assumes you're commonly running several asynchronous tasks with rich terminal UI progressing display. The progressing UI can be a bar (called Stepper) or a spinner.
A demo of multibar looks like:
multibar2 is a complex sample app to show you more advanced usages.
Stepper style is like a horizontal bar with progressing tick(s).
go run ./examples/steppers go run ./examples/steppers 0 # can be 0..3 (=progressbar.MaxSteppers())Spinner style is a rotating icon/text in generally.
go run ./examples/spinners go run ./examples/spinners 0 # can be 0..75 (=progressbar.MaxSpinners())Since v2, we enable NewV2() to take a stable, accurate CLI progressbar to you:
import "github.com/hedzr/progressbar/v2" func downloadGroupsV2Worked() { // const mySchema = `{{.Indent}}{{.Prepend}} <font color="green">{{.Title}}</font> {{.Percent}} {{.Bar}} {{.Current}}/{{.Total}} {{.Speed}} {{.Elapsed}} {{.Append}}` // var versions = []string{"1.16.1", "1.17.1", "1.18.1", "1.19.1", "1.20.1", "1.21.1", "1.22.1", "1.23.1", "1.24.1"} var versions = []string{"1.24.1"} var mpb *progressbar.MPBV2 if schema := os.Getenv("SCHEMA"); schema != "" { mpb = progressbar.NewV2(progressbar.WithSchema(schema)) } else { mpb = progressbar.NewV2() } defer mpb.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // define a counter job here rng := rand.New(rand.NewSource(time.Now().UnixNano())) job := func(bar *progressbar.MPBV2, grp *progressbar.GroupV2, tsk *progressbar.TaskBar, progress int64, args ...any) (delta int64, err error) { time.Sleep(time.Duration(rng.Intn(60)+30) * time.Millisecond) delta += int64(rng.Intn(5) + 1) return } // define the downloading job adder here verIdx := 0 addDownloadJob := func(bar *progressbar.MPBV2, i, j int) { ver := versions[verIdx] url1 := TitledUrl("https://dl.google.com/go/go" + ver + ".src.tar.gz") // url := fmt.Sprintf("https://dl.google.com/go/go%v.src.tar.gz", ver) bar.AddDownloadingBar( "Group "+strconv.Itoa(i), "Task #"+strconv.Itoa(j), &progressbar.DownloadTask{ Url: url1.String(), Filename: url1.Title(), Title: url1.Title(), }, // more opts can be set here // progressbar.WithTaskBarStepper(whichStepper), // progressbar.WithTaskBarSpinner(whichSpinner), ) verIdx++ } total, num, numTasks := int64(100), 2, 3 // we would add some progressing task groups, for i := range num { // in a single task group, we add some tasks, for j := range numTasks { // one of which is a downloading task. if (j == numTasks-1 || i == 0) && verIdx < len(versions) { addDownloadJob(mpb, i, j) continue } // and the rests are counter tasks. mpb.AddBar("Group "+strconv.Itoa(i), "Task #"+strconv.Itoa(j), 0, total, counterJob) } } // var wg sync.WaitGroup // wg.Add(num * numTasks) // so you will get a multi-group multi-task progress bar by Run it. mpb.Run(ctx) }To try the above sample codes, run under a terminal:
go run ./examples/mpbv2The notable thing is, once you've upgraded to progressbar.v2, the legacy v1 code would keep work without any changes.
By using progressbar.NewTasks(), you can add new task bundled with a progressbar.
func forAllSpinners() { tasks := progressbar.NewTasks(progressbar.New()) defer tasks.Close() for i := whichSpinner; i < whichSpinner+5; i++ { tasks.Add( progressbar.WithTaskAddBarOptions( progressbar.WithBarSpinner(i), progressbar.WithBarUpperBound(100), progressbar.WithBarWidth(8), // progressbar.WithBarTextSchema(schema), ), progressbar.WithTaskAddBarTitle(fmt.Sprintf("Task %v", i)), progressbar.WithTaskAddOnTaskProgressing(func(bar progressbar.PB, exitCh <-chan struct{}) { for max, ix := bar.UpperBound(), int64(0); ix < max; ix++ { ms := time.Duration(200 + rand.Intn(1800)) //nolint:gosec //just a demo time.Sleep(time.Millisecond * ms) bar.Step(1) } }), ) } tasks.Wait() // start waiting for all tasks completed gracefully }To have a see to run:
go run ./examples/tasksThe above sample shows you how a Task could be encouraged by progressbar.WithTaskAddOnTaskProgressing, WithTaskAddOnTaskInitializing and WithTaskAddOnTaskCompleted.
You can write your Task and feedback the progress to multi-pbar (MultiPB) or pbar (PB), see the source code taskdownload.go.
The key point is, wrapping your task runner, maybe called as worker, as a PB.Worker, and add it with WithBarWorker.
Expand to get implementations
func (s *DownloadTasks) Add(url, filename string, opts ...Opt) { task := new(aTask) task.wg = &s.wg task.url = url task.fn = filename var o []Opt o = append(o, WithBarWorker(task.doWorker), WithBarOnCompleted(task.onCompleted), WithBarOnStart(task.onStart), ) o = append(o, opts...) s.bar.Add( 100, task.fn, // fmt.Sprintf("downloading %v", s.fn), // // WithBarSpinner(14), // // WithBarStepper(3), // WithBarStepper(0), // WithBarWorker(s.doWorker), // WithBarOnCompleted(s.onCompleted), // WithBarOnStart(s.onStart), o..., ) s.wg.Add(1) } func (s *aTask) doWorker(bar PB, exitCh <-chan struct{}) { // _, _ = io.Copy(s.w, s.resp.Body) for { n, err := s.resp.Body.Read(s.buf) if err != nil && !errors.Is(err, io.EOF) { log.Printf("Error: %v", err) return } if n == 0 { break } if _, err = s.w.Write(s.buf[:n]); err != nil { log.Printf("Error: %v", err) return } select { case <-exitCh: return default: // avoid block at <-exitCh } // time.Sleep(time.Millisecond * 100) } } func (s *aTask) onCompleted(bar PB) { wg := s.wg s.wg = nil wg.Done() atomic.AddInt32(&s.doneCount, 1) } func (s *aTask) onStart(bar PB) { if s.req == nil { var err error s.req, err = http.NewRequest("GET", s.url, nil) //nolint:gocritic if err != nil { log.Printf("Error: %v", err) } s.f, err = os.OpenFile(s.fn, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { log.Printf("Error: %v", err) } s.resp, err = http.DefaultClient.Do(s.req) if err != nil { log.Printf("Error: %v", err) } bar.UpdateRange(0, s.resp.ContentLength) s.w = io.MultiWriter(s.f, bar) const BUFFERSIZE = 4096 s.buf = make([]byte, BUFFERSIZE) } }For using Stepper instead of Spinner, these fragments can be applied:
tasks.Add(url, fn, progressbar.WithBarStepper(whichStepper), )If you're looking for a downloader with progress bar, our progressbar.NewDownloadTasks is better choice because it had wrapped all things in one.
To start many groups of tasks like docker pull to get the layers, just add them:
func doEachGroup(group []string) { tasks := progressbar.NewDownloadTasks(progressbar.New()) defer tasks.Close() for _, ver := range group { url := "https://dl.google.com/go/go" + ver + ".src.tar.gz" // url := fmt.Sprintf("https://dl.google.com/go/go%v.src.tar.gz", ver) fn := "go" + ver + ".src.tar.gz" // fn := fmt.Sprintf("go%v.src.tar.gz", ver) tasks.Add(url, fn, progressbar.WithBarStepper(whichStepper), ) } tasks.Wait() // start waiting for all tasks completed gracefully } func downloadGroups() { for _, group := range [][]string{ {"1.14.2", "1.15.1"}, # first group, {"1.16.1", "1.17.1", "1.18.3"}, # and the second one, } { doEachGroup(group) } }Run it(s):
go run ./examples/multibar go run ./examples/multibar 3 # to select a stepper # Or using spinner style go run ./examples/multibar_spinner go run ./examples/multibar_spinner 7 # to select a spinnersThe default bar layout of a stepper is
// see it in stepper.go var defaultSchema = `{{.Indent}}{{.Prepend}} {{.Bar}} {{.Percent}} | <font color="green">{{.Title}}</font> | {{.Current}}/{{.Total}} {{.Speed}} {{.Elapsed}} {{.Append}}`But you can always replace it with your own. A sample is examples/tasks. The demo app shows the real way:
package main const schema = `{{.Indent}}{{.Prepend}} {{.Bar}} {{.Percent}} | <b><font color="green">{{.Title}}</font></b> {{.Append}}` tasks.Add( progressbar.WithTaskAddBarOptions( progressbar.WithBarUpperBound(100), //progressbar.WithBarSpinner(i), // if you're looking for a spinner instead stepper //progressbar.WithBarWidth(8), progressbar.WithBarStepper(0), progressbar.WithBarTextSchema(schema), // change the bar layout here ), // ... progressbar.WithTaskAddBarTitle(fmt.Sprintf("Task %v", i)), progressbar.WithTaskAddOnTaskProgressing(func(bar progressbar.PB, exitCh <-chan struct{}) { for max, ix := bar.UpperBound(), int64(0); ix < max; ix++ { ms := time.Duration(20 + rand.Intn(500)) //nolint:gosec //just a demo time.Sleep(time.Millisecond * ms) bar.Step(1) } }), )Simple html tags (b, i, u, font, strong, em, cite, mark, del, kbd, code, html, head, body) can be embedded if ANSI Escaped Color codes is hard to use.
The predefined named colors are also available:
// These color names can be used in <font color=''> html tag: cptCM = map[string]int{ "black": FgBlack, "red": FgRed, "green": FgGreen, "yellow": FgYellow, "blue": FgBlue, "magenta": FgMagenta, "cyan": FgCyan, "lightgray": FgLightGray, "light-gray": FgLightGray, "darkgray": FgDarkGray, "dark-gray": FgDarkGray, "lightred": FgLightRed, "light-red": FgLightRed, "lightgreen": FgLightGreen, "light-green": FgLightGreen, "lightyellow": FgLightYellow, "light-yellow": FgLightYellow, "lightblue": FgLightBlue, "light-blue": FgLightBlue, "lightmagenta": FgLightMagenta, "light-magenta": FgLightMagenta, "lightcyan": FgLightCyan, "light-cyan": FgLightCyan, "white": FgWhite, }
tool.GetCPT()returns aColorTranslaterto help you strips the basic HTML tags and render them with ANSI escape sequences.
If you wanna build a better Percent or Elapsed, try formatting with PercentFloat and ElapsedTime field:
const schema = `{{.PercentFloat|printf "%3.1f%%" }}, {{.ElapsedTime}}`To observe the supplied data to the schema, try WithBarOnDataPrepared(cb):
tasks.Add( progressbar.WithTaskAddBarOptions( progressbar.WithBarStepper(i), progressbar.WithBarUpperBound(100), progressbar.WithBarWidth(32), progressbar.WithBarTextSchema(schema), progressbar.WithBarExtraTailSpaces(16), progressbar.WithBarPrependText("[[[x]]]"), progressbar.WithBarAppendText("[[[z]]]"), progressbar.WithBarOnDataPrepared(func(bar progressbar.PB, data *progressbar.SchemaData) { data.ElapsedTime *= 2 }), ), progressbar.WithTaskAddBarTitle("Task "+strconv.Itoa(i)), // fmt.Sprintf("Task %v", i)), progressbar.WithTaskAddOnTaskProgressing(func(bar progressbar.PB, exitCh <-chan struct{}) { for max, ix := bar.UpperBound(), int64(0); ix < max; ix++ { ms := time.Duration(10 + rand.Intn(300)) //nolint:gosec //just a demo time.Sleep(time.Millisecond * ms) bar.Step(1) } }), )The API to change a spinner's display layout is same to above.
There is a tiny terminal cursor operating subpackage, cursor. It's cross-platforms to show and hide cursor, move cursor up, left with/out wipe out the characters. Notes that is not a TUI cursor controlling library.
To review all possible looks, try our samples:
# To run all stocked steppers in a screen go run ./examples/steppers # To run certain a stepper go run ./examples/steppers 0 # To run all stocked spinners in a screen go run ./examples/spinners # To run certain a stepper go run ./examples/spinners 0This repo is inspired from python3 install tui, and schollz/progressbar, and more tui progress bars.
Apache 2.0

