progressbar

package module
v1.2.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2025 License: Apache-2.0 Imports: 17 Imported by: 4

README

progressbar (-go)

Go GitHub tag (latest SemVer) go.dev

An asynchronous, multitask console/terminal progressbar widget. The main look of default stepper is:

stepper-0

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+.

History

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.

Guide

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:

anim

multibar2 is a complex sample app to show you more advanced usages.

What's Steppers

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())
What's Spinners

Spinner style is a rotating icon/text in generally.

go run ./examples/spinners
go run ./examples/spinners 0 # can be 0..75 (=progressbar.MaxSpinners())
Tasks & With groups
Using Tasks

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/tasks
Write Your Own Tasks With MultiPB and PB

The 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)
	}
}
Multiple Bars (and Multiple groups)

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 spinners
Customize the bar layout

The 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 a ColorTranslater to 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.

Grouped MPBar [Since ]

Using cursor lib

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.

Tips

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 0

Credit

This repo is inspired from python3 install tui, and schollz/progressbar, and more tui progress bars.

LICENSE

Apache 2.0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MaxSpinners

func MaxSpinners() int

func MaxSteppers

func MaxSteppers() int

Types

type BarT added in v1.2.0

type BarT interface {
	String(pb *pbar) string
	Bytes(pb *pbar) []byte

	Percent() string   // just for stepper
	PercentF() float64 // return 0.905
	PercentI() int     // '90.5' -> return 91

	Resumeable() bool
	SetResumeable(resumeable bool)
	SetInitialValue(initial int64)

	SetSchema(schema string)
	SetWidth(w int)
	SetIndentChars(s string)
	SetPrependText(s string)
	SetAppendText(s string)
	SetExtraTailSpaces(howMany int)

	SetBaseColor(clr int)
	SetHighlightColor(clr int)
}

type DownloadTask added in v1.1.11

type DownloadTask struct {
	Url, Filename, Title string

	Req  *http.Request
	Resp *http.Response
	File *os.File

	Writer io.Writer
	Buffer []byte
	// contains filtered or unexported fields
}

func (*DownloadTask) Close added in v1.1.11

func (s *DownloadTask) Close()

func (*DownloadTask) Complete added in v1.2.7

func (s *DownloadTask) Complete()

type DownloadTasks

type DownloadTasks struct {
	// contains filtered or unexported fields
}

func NewDownloadTasks

func NewDownloadTasks(bar MultiPB, opts ...DownloadTasksOpt) *DownloadTasks

NewDownloadTasks is a wrapped NewTasks to simplify the http downloading task.

Via this API you can easily download one and more http files.

type TitledUrl string

func (t TitledUrl) String() string { return string(t) }

func (t TitledUrl) Title() string {
	if parse, err := url.Parse(string(t)); err != nil {
		return string(t)
	}
	return path.Base(parse.Path)
}

func doEachGroup(group []string) {
	tasks := progressbar.NewDownloadTasks(progressbar.New(),
		// progressbar.WithTaskAddOnTaskCompleted(func...),
	)
	defer tasks.Close()

	for _, ver := range group {
		url1 := TitledUrl("https://dl.google.com/go/go" + ver + ".src.tar.gz")
		tasks.Add(url1.String(), url1,
			progressbar.WithBarStepper(whichStepper),
		)
	}

	tasks.Wait() // start waiting for all tasks completed gracefully
}

func downloadGroups() {
	for _, group := range [][]string{
		{"1.14.2", "1.15.1"},
		{"1.16.1", "1.17.1", "1.18.3"},
	} {
		doEachGroup(group)
	}
}

func (*DownloadTasks) Add

func (s *DownloadTasks) Add(url string, filename any, opts ...Opt)

Add a url as a downloading task, which will be started at background right now.

The downloaded content is stored into local file, `filename` specified. The `filename` will be shown in progressbar as a title. You can customize its title with `interface{ Title() string`. A sample could be:

type TitledUrl string

func (t TitledUrl) String() string { return string(t) }

func (t TitledUrl) Title() string {
	if parse, err := url.Parse(string(t)); err != nil {
		return string(t)
	}
	return path.Base(parse.Path)
}

func doEachGroup(group []string) {
	tasks := progressbar.NewDownloadTasks(progressbar.New(),
		// progressbar.WithTaskAddOnTaskCompleted(func...),
	)
	defer tasks.Close()

	for _, ver := range group {
		url1 := TitledUrl("https://dl.google.com/go/go" + ver + ".src.tar.gz")
		tasks.Add(url1.String(), url1,
			progressbar.WithBarStepper(whichStepper),
		)
	}

	tasks.Wait() // start waiting for all tasks completed gracefully
}

func (*DownloadTasks) Close

func (s *DownloadTasks) Close()

func (*DownloadTasks) Wait

func (s *DownloadTasks) Wait()

type DownloadTasksOpt added in v1.1.11

type DownloadTasksOpt func(tsk *DownloadTasks)

func WithDownloadTaskLogger added in v1.2.8

func WithDownloadTaskLogger(logger *slog.Logger) DownloadTasksOpt

func WithDownloadTaskOnStart added in v1.1.11

func WithDownloadTaskOnStart(fn OnStartCB) DownloadTasksOpt

type GroupedPB added in v1.2.0

type GroupedPB interface {
	MultiPB

	AddToGroup(group string, maxBytes int64, title string, opts ...Opt) (index int)
	RemoveFromGroup(group string, index int)
}

func NewGPB added in v1.2.0

func NewGPB(opts ...MOpt) GroupedPB

NewGPB creates a grouped MultiPB (GroupedPB) instance.

type MOpt

type MOpt func(mpb *mpbar)

func WithOnDone

func WithOnDone(cb OnDone) MOpt

func WithOutputDevice added in v1.1.1

func WithOutputDevice(out io.Writer) MOpt

type MultiPB

type MultiPB interface {
	io.Writer
	Close()
	Cancel() // cancel the bar

	Add(maxBytes int64, title string, opts ...Opt) (index int)
	Remove(index int)

	Redraw()
	SignalExit() <-chan struct{}

	Bar(index int) BarT
	Percent(index int) string   // just for stepper
	PercentF(index int) float64 // return 0.905
	PercentI(index int) int     // '90.5' -> return 91
}

func Add

func Add(maxBytes int64, title string, opts ...Opt) MultiPB

func New

func New(opts ...MOpt) MultiPB

New creates a managed MultiPB progressbar object so you can setup the properties of the bar.

bar := progressbar.New()
bar.Add(
	resp.ContentLength,
	"downloading go1.14.2.src.tar.gz",
	// progressbar.WithSpinner(14),
	// progressbar.WithStepper(3),
	progressbar.WithBarStepper(0),
)

A MultiPB or PB progressbar object is a writable object which can receive the data writing via Writer interface:

f, _ := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY, 0o644)
_, _ = io.Copy(io.MultiWriter(f, bar), resp.Body)
f.Close()
bar.Close()

The MultiPB object can be added into Tasks container. For more information to see NewTasks() and NewDownloadTasks().

type OnCompleted

type OnCompleted func(bar PB)

type OnDataPrepared added in v1.1.1

type OnDataPrepared func(bar PB, data *SchemaData)

type OnDone

type OnDone func(mpb MultiPB)

type OnStart

type OnStart func(bar PB)

type OnStartCB added in v1.1.11

type OnStartCB func(task *DownloadTask, bar PB) (err error)

type Opt

type Opt func(pb *pbar)

func WithBarAppendText added in v1.1.1

func WithBarAppendText(str string) Opt

func WithBarExtraTailSpaces added in v1.1.1

func WithBarExtraTailSpaces(howMany int) Opt

WithBarExtraTailSpaces specifies how many spaces will be printed at end of each bar. These spaces can wipe out the dirty tail of line.

Default is 8 (spaces). You may specify -1 to disable extra spaces to be printed.

func WithBarIndentChars added in v1.1.1

func WithBarIndentChars(str string) Opt

func WithBarInitialValue added in v1.2.7

func WithBarInitialValue(v int64) Opt

func WithBarOnCompleted

func WithBarOnCompleted(cb OnCompleted) Opt

func WithBarOnDataPrepared added in v1.1.1

func WithBarOnDataPrepared(cb OnDataPrepared) Opt

func WithBarOnStart

func WithBarOnStart(cb OnStart) Opt

func WithBarPrependText added in v1.1.1

func WithBarPrependText(str string) Opt

func WithBarResumeable added in v1.2.7

func WithBarResumeable(b bool) Opt

func WithBarSpinner

func WithBarSpinner(whichOne int) Opt

func WithBarStepper

func WithBarStepper(whichOne int) Opt

func WithBarStepperPostInit added in v1.2.0

func WithBarStepperPostInit(cb func(bar BarT)) Opt

func WithBarTextSchema

func WithBarTextSchema(schema string) Opt

WithBarTextSchema allows cha

"{{.Indent}}{{.Prepend}} {{.Bar}} {{.Percent}} | {{.Title}} | {{.Current}}/{{.Total}} {{.Speed}} {{.Elapsed}} {{.Append}}"

func WithBarUpperBound

func WithBarUpperBound(ub int64) Opt

func WithBarWidth

func WithBarWidth(w int) Opt

func WithBarWorker

func WithBarWorker(w Worker) Opt

type PB

type PB interface {
	io.Writer
	Close()
	String() string

	Percent() string   // just for stepper
	PercentF() float64 // return 0.905
	PercentI() int     // '90.5' -> return 91

	Bar() BarT
	Resumeable() bool
	SetResumeable(resumeable bool)
	SetInitialValue(initial int64)

	UpdateRange(min, max int64) // modify the bounds
	Step(delta int64)           // update the progress

	LowerBound() int64 // unsafe getter for lowerBound
	UpperBound() int64 // unsafe getter for upperBound
	Progress() int64   // unsafe progress getter

	// Bounds return lowerBound, upperBound and progress atomically.
	Bounds() (lb, ub, progress int64)
}

type SchemaData added in v1.1.1

type SchemaData struct {
	Data any // your customized data structure here

	Indent  string
	Prepend string
	Bar     string
	Percent string
	Title   string
	Current string
	Total   string
	Elapsed string
	Speed   string
	Append  string

	PercentFloat float64
	ElapsedTime  time.Duration
}

type TaskOpt

type TaskOpt func(s *taskOptions)

func WithTaskAddBarOptions

func WithTaskAddBarOptions(opts ...Opt) TaskOpt

func WithTaskAddBarTitle

func WithTaskAddBarTitle(title string) TaskOpt

func WithTaskAddOnTaskCompleted

func WithTaskAddOnTaskCompleted(cb OnCompleted) TaskOpt

func WithTaskAddOnTaskInitializing

func WithTaskAddOnTaskInitializing(cb OnStart) TaskOpt

func WithTaskAddOnTaskProgressing

func WithTaskAddOnTaskProgressing(cb Worker) TaskOpt

type Tasks

type Tasks struct {
	// contains filtered or unexported fields
}

func NewTasks

func NewTasks(bar MultiPB) *Tasks

NewTasks creates a Tasks container which you can add the tasks in it.

tasks := progressbar.NewTasks(progressbar.New())
defer tasks.Close()

max := count
_, h, _ := terminal.GetSize(int(os.Stdout.Fd()))
if max >= h {
	max = h
}

for i := whichStepper; i < whichStepper+max; i++ {
	tasks.Add(
		progressbar.WithTaskAddBarOptions(
			progressbar.WithBarStepper(i),
			progressbar.WithBarUpperBound(100),
			progressbar.WithBarWidth(32),
		),
		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)
			}
		}),
	)
}

tasks.Wait()

Above.

func (*Tasks) Add

func (s *Tasks) Add(opts ...TaskOpt) *Tasks

func (*Tasks) Close

func (s *Tasks) Close()

func (*Tasks) Wait

func (s *Tasks) Wait()

type Worker

type Worker func(bar PB, exitCh <-chan struct{}) (stop bool)

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL