├── doc ├── example_full.gif ├── example_multi.gif └── example_simple.gif ├── .travis.yml ├── doc.go ├── go.mod ├── Makefile ├── example ├── simple │ └── simple.go ├── multi │ └── multi.go ├── incr │ └── incr.go ├── bypass │ └── bypass.go └── full │ └── full.go ├── go.sum ├── bar_test.go ├── util └── strutil │ ├── strutil_test.go │ └── strutil.go ├── progress_test.go ├── LICENSE ├── example_test.go ├── progress.go ├── README.md └── bar.go /doc/example_full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosuri/uiprogress/HEAD/doc/example_full.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | install: 4 | - go get ./... 5 | go: 6 | - tip 7 | -------------------------------------------------------------------------------- /doc/example_multi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosuri/uiprogress/HEAD/doc/example_multi.gif -------------------------------------------------------------------------------- /doc/example_simple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosuri/uiprogress/HEAD/doc/example_simple.gif -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package uiprogress is a library to render progress bars in terminal applications 2 | package uiprogress 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gosuri/uiprogress 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gosuri/uilive v0.0.4 7 | github.com/mattn/go-isatty v0.0.12 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test -race . 3 | @go test -race ./util/strutil 4 | 5 | examples: 6 | go run -race example/full/full.go 7 | go run -race example/incr/incr.go 8 | go run -race example/multi/multi.go 9 | go run -race example/simple/simple.go 10 | 11 | .PHONY: test examples 12 | -------------------------------------------------------------------------------- /example/simple/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gosuri/uiprogress" 7 | ) 8 | 9 | func main() { 10 | uiprogress.Start() // start rendering 11 | bar := uiprogress.AddBar(100) // Add a new bar 12 | 13 | // optionally, append and prepend completion and elapsed time 14 | bar.AppendCompleted() 15 | bar.PrependElapsed() 16 | 17 | for bar.Incr() { 18 | time.Sleep(time.Millisecond * 20) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= 2 | github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= 3 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 4 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 5 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 6 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | -------------------------------------------------------------------------------- /bar_test.go: -------------------------------------------------------------------------------- 1 | package uiprogress 2 | 3 | import ( 4 | "math/rand" 5 | "runtime" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestBarPrepend(t *testing.T) { 13 | b := NewBar(100) 14 | b.PrependCompleted() 15 | b.Set(50) 16 | if !strings.Contains(b.String(), "50") { 17 | t.Fatal("want", "50%", "in", b.String()) 18 | } 19 | } 20 | 21 | func TestBarIncr(t *testing.T) { 22 | b := NewBar(10000) 23 | runtime.GOMAXPROCS(runtime.NumCPU()) 24 | var wg sync.WaitGroup 25 | for i := 0; i < 10000; i++ { 26 | wg.Add(1) 27 | go func() { 28 | defer wg.Done() 29 | b.Incr() 30 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(10))) 31 | }() 32 | } 33 | wg.Wait() 34 | if b.Current() != 10000 { 35 | t.Fatal("need", 10000, "got", b.Current()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /util/strutil/strutil_test.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestResize(t *testing.T) { 9 | s := "foo" 10 | got := Resize(s, 5) 11 | if len(got) != 5 { 12 | t.Fatal("want", 5, "got", len(got)) 13 | } 14 | s = "foobar" 15 | got = Resize(s, 5) 16 | 17 | if got != "fo..." { 18 | t.Fatal("want", "fo...", "got", got) 19 | } 20 | } 21 | 22 | func TestPadRight(t *testing.T) { 23 | got := PadRight("foo", 5, '-') 24 | if got != "foo--" { 25 | t.Fatal("want", "foo--", "got", got) 26 | } 27 | } 28 | 29 | func TestPadLeft(t *testing.T) { 30 | got := PadLeft("foo", 5, '-') 31 | if got != "--foo" { 32 | t.Fatal("want", "--foo", "got", got) 33 | } 34 | } 35 | 36 | func TestPrettyTime(t *testing.T) { 37 | d, _ := time.ParseDuration("") 38 | got := PrettyTime(d) 39 | if got != "---" { 40 | t.Fatal("want", "---", "got", got) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/multi/multi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/gosuri/uiprogress" 8 | ) 9 | 10 | func main() { 11 | waitTime := time.Millisecond * 100 12 | uiprogress.Start() 13 | 14 | var wg sync.WaitGroup 15 | 16 | bar1 := uiprogress.AddBar(20).AppendCompleted().PrependElapsed() 17 | wg.Add(1) 18 | go func() { 19 | defer wg.Done() 20 | for bar1.Incr() { 21 | time.Sleep(waitTime) 22 | } 23 | }() 24 | 25 | bar2 := uiprogress.AddBar(40).AppendCompleted().PrependElapsed() 26 | wg.Add(1) 27 | go func() { 28 | defer wg.Done() 29 | for bar2.Incr() { 30 | time.Sleep(waitTime) 31 | } 32 | }() 33 | 34 | time.Sleep(time.Second) 35 | bar3 := uiprogress.AddBar(20).PrependElapsed().AppendCompleted() 36 | wg.Add(1) 37 | go func() { 38 | defer wg.Done() 39 | for bar3.Incr() { 40 | time.Sleep(waitTime) 41 | } 42 | }() 43 | 44 | wg.Wait() 45 | } 46 | -------------------------------------------------------------------------------- /progress_test.go: -------------------------------------------------------------------------------- 1 | package uiprogress 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestStoppingPrintout(t *testing.T) { 13 | progress := New() 14 | progress.SetRefreshInterval(time.Millisecond * 10) 15 | 16 | var buffer = &bytes.Buffer{} 17 | progress.SetOut(buffer) 18 | 19 | bar := progress.AddBar(100) 20 | progress.Start() 21 | 22 | var wg sync.WaitGroup 23 | 24 | wg.Add(1) 25 | 26 | go func() { 27 | for i := 0; i <= 80; i = i + 10 { 28 | bar.Set(i) 29 | time.Sleep(time.Millisecond * 5) 30 | } 31 | 32 | wg.Done() 33 | }() 34 | 35 | wg.Wait() 36 | 37 | progress.Stop() 38 | fmt.Fprintf(buffer, "foo") 39 | 40 | var wantSuffix = "[======================================================>-------------]\nfoo" 41 | 42 | if !strings.HasSuffix(buffer.String(), wantSuffix) { 43 | t.Errorf("Content that should be printed after stop not appearing on buffer.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/incr/incr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "runtime" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gosuri/uiprogress" 11 | ) 12 | 13 | func main() { 14 | runtime.GOMAXPROCS(runtime.NumCPU()) // use all available cpu cores 15 | 16 | // create a new bar and prepend the task progress to the bar and fanout into 1k go routines 17 | count := 1000 18 | bar := uiprogress.AddBar(count).AppendCompleted().PrependElapsed() 19 | bar.PrependFunc(func(b *uiprogress.Bar) string { 20 | return fmt.Sprintf("Task (%d/%d)", b.Current(), count) 21 | }) 22 | 23 | uiprogress.Start() 24 | var wg sync.WaitGroup 25 | 26 | // fanout into 1k go routines 27 | for i := 0; i < count; i++ { 28 | wg.Add(1) 29 | go func() { 30 | defer wg.Done() 31 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) 32 | bar.Incr() 33 | }() 34 | } 35 | time.Sleep(time.Second) // wait for a second for all the go routines to finish 36 | wg.Wait() 37 | uiprogress.Stop() 38 | } 39 | -------------------------------------------------------------------------------- /example/bypass/bypass.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gosuri/uiprogress" 9 | ) 10 | 11 | func main() { 12 | waitTime := time.Millisecond * 200 13 | p := uiprogress.New() 14 | p.Start() 15 | 16 | var wg sync.WaitGroup 17 | 18 | bar1 := p.AddBar(20).AppendCompleted().PrependElapsed() 19 | wg.Add(1) 20 | go func() { 21 | defer wg.Done() 22 | for bar1.Incr() { 23 | time.Sleep(waitTime) 24 | } 25 | fmt.Fprintln(p.Bypass(), "Bar1 finished") 26 | }() 27 | 28 | bar2 := p.AddBar(40).AppendCompleted().PrependElapsed() 29 | wg.Add(1) 30 | go func() { 31 | defer wg.Done() 32 | for bar2.Incr() { 33 | time.Sleep(waitTime) 34 | } 35 | fmt.Fprintln(p.Bypass(), "Bar2 finished") 36 | }() 37 | 38 | time.Sleep(time.Second) 39 | bar3 := p.AddBar(20).PrependElapsed().AppendCompleted() 40 | wg.Add(1) 41 | go func() { 42 | defer wg.Done() 43 | for bar3.Incr() { 44 | time.Sleep(waitTime) 45 | } 46 | fmt.Fprintln(p.Bypass(), "Bar3 finished") 47 | }() 48 | 49 | wg.Wait() 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2015, Greg Osuri 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /example/full/full.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gosuri/uiprogress" 10 | "github.com/gosuri/uiprogress/util/strutil" 11 | ) 12 | 13 | var steps = []string{ 14 | "downloading source", 15 | "installing deps", 16 | "compiling", 17 | "packaging", 18 | "seeding database", 19 | "deploying", 20 | "staring servers", 21 | } 22 | 23 | func main() { 24 | fmt.Println("apps: deployment started: app1, app2") 25 | uiprogress.Start() 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(1) 29 | go deploy("app1", &wg) 30 | wg.Add(1) 31 | go deploy("app2", &wg) 32 | wg.Wait() 33 | 34 | fmt.Println("apps: successfully deployed: app1, app2") 35 | } 36 | 37 | func deploy(app string, wg *sync.WaitGroup) { 38 | defer wg.Done() 39 | bar := uiprogress.AddBar(len(steps)).AppendCompleted().PrependElapsed() 40 | bar.Width = 50 41 | 42 | // prepend the deploy step to the bar 43 | bar.PrependFunc(func(b *uiprogress.Bar) string { 44 | return strutil.Resize(app+": "+steps[b.Current()-1], 22) 45 | }) 46 | 47 | rand.Seed(500) 48 | for bar.Incr() { 49 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(2000))) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /util/strutil/strutil.go: -------------------------------------------------------------------------------- 1 | // Package strutil provides various utilities for manipulating strings 2 | package strutil 3 | 4 | import ( 5 | "bytes" 6 | "time" 7 | ) 8 | 9 | // PadRight returns a new string of a specified length in which the end of the current string is padded with spaces or with a specified Unicode character. 10 | func PadRight(str string, length int, pad byte) string { 11 | if len(str) >= length { 12 | return str 13 | } 14 | buf := bytes.NewBufferString(str) 15 | for i := 0; i < length-len(str); i++ { 16 | buf.WriteByte(pad) 17 | } 18 | return buf.String() 19 | } 20 | 21 | // PadLeft returns a new string of a specified length in which the beginning of the current string is padded with spaces or with a specified Unicode character. 22 | func PadLeft(str string, length int, pad byte) string { 23 | if len(str) >= length { 24 | return str 25 | } 26 | var buf bytes.Buffer 27 | for i := 0; i < length-len(str); i++ { 28 | buf.WriteByte(pad) 29 | } 30 | buf.WriteString(str) 31 | return buf.String() 32 | } 33 | 34 | // Resize resizes the string with the given length. It ellipses with '...' when the string's length exceeds 35 | // the desired length or pads spaces to the right of the string when length is smaller than desired 36 | func Resize(s string, length uint) string { 37 | n := int(length) 38 | if len(s) == n { 39 | return s 40 | } 41 | // Pads only when length of the string smaller than len needed 42 | s = PadRight(s, n, ' ') 43 | if len(s) > n { 44 | b := []byte(s) 45 | var buf bytes.Buffer 46 | for i := 0; i < n-3; i++ { 47 | buf.WriteByte(b[i]) 48 | } 49 | buf.WriteString("...") 50 | s = buf.String() 51 | } 52 | return s 53 | } 54 | 55 | // PrettyTime returns the string representation of the duration. It rounds the time duration to a second and returns a "---" when duration is 0 56 | func PrettyTime(t time.Duration) string { 57 | if t == 0 { 58 | return "---" 59 | } 60 | return (t - (t % time.Second)).String() 61 | } 62 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package uiprogress_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "runtime" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gosuri/uiprogress" 11 | ) 12 | 13 | func Example() { 14 | uiprogress.Start() // start rendering 15 | bar := uiprogress.AddBar(100) // Add a new bar 16 | 17 | // optionally, append and prepend completion and elapsed time 18 | bar.AppendCompleted() 19 | bar.PrependElapsed() 20 | 21 | for bar.Incr() { 22 | time.Sleep(time.Millisecond * 20) 23 | } 24 | } 25 | 26 | func ExampleProgress_AddBar() { 27 | waitTime := time.Millisecond * 100 28 | uiprogress.Start() 29 | // start the progress bars in go routines 30 | var wg sync.WaitGroup 31 | 32 | bar1 := uiprogress.AddBar(20).AppendCompleted().PrependElapsed() 33 | wg.Add(1) 34 | go func() { 35 | defer wg.Done() 36 | for bar1.Incr() { 37 | time.Sleep(waitTime) 38 | } 39 | }() 40 | 41 | bar2 := uiprogress.AddBar(40).AppendCompleted().PrependElapsed() 42 | wg.Add(1) 43 | go func() { 44 | defer wg.Done() 45 | for bar2.Incr() { 46 | time.Sleep(waitTime) 47 | } 48 | }() 49 | 50 | time.Sleep(time.Second) 51 | bar3 := uiprogress.AddBar(20).PrependElapsed().AppendCompleted() 52 | wg.Add(1) 53 | go func() { 54 | defer wg.Done() 55 | for i := 1; i <= bar3.Total; i++ { 56 | bar3.Set(i) 57 | time.Sleep(waitTime) 58 | } 59 | }() 60 | // wait for all the go routines to finish 61 | wg.Wait() 62 | } 63 | 64 | func ExampleDecoratorFunc() { 65 | var steps = []string{"downloading source", "installing deps", "compiling", "packaging", "seeding database", "deploying", "staring servers"} 66 | bar := uiprogress.AddBar(len(steps)) 67 | 68 | // prepend the current step to the bar 69 | bar.PrependFunc(func(b *uiprogress.Bar) string { 70 | return "app: " + steps[b.Current()-1] 71 | }) 72 | 73 | for bar.Incr() { 74 | time.Sleep(time.Millisecond) 75 | } 76 | } 77 | 78 | func ExampleBar_Incr() { 79 | runtime.GOMAXPROCS(runtime.NumCPU()) // use all available cpu cores 80 | 81 | // create a new bar and prepend the task progress to the bar 82 | count := 1000 83 | bar := uiprogress.AddBar(count).AppendCompleted().PrependElapsed() 84 | bar.PrependFunc(func(b *uiprogress.Bar) string { 85 | return fmt.Sprintf("Task (%d/%d)", b.Current(), count) 86 | }) 87 | 88 | uiprogress.Start() 89 | var wg sync.WaitGroup 90 | 91 | // fanout into 1k go routines 92 | for i := 0; i < count; i++ { 93 | wg.Add(1) 94 | go func() { 95 | defer wg.Done() 96 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) 97 | bar.Incr() 98 | }() 99 | } 100 | time.Sleep(time.Second) // wait for a second for all the go routines to finish 101 | wg.Wait() 102 | uiprogress.Stop() 103 | } 104 | -------------------------------------------------------------------------------- /progress.go: -------------------------------------------------------------------------------- 1 | package uiprogress 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gosuri/uilive" 11 | ) 12 | 13 | // Out is the default writer to render progress bars to 14 | var Out = os.Stdout 15 | 16 | // RefreshInterval in the default time duration to wait for refreshing the output 17 | var RefreshInterval = time.Millisecond * 10 18 | 19 | // defaultProgress is the default progress 20 | var defaultProgress = New() 21 | 22 | // Progress represents the container that renders progress bars 23 | type Progress struct { 24 | // Out is the writer to render progress bars to 25 | Out io.Writer 26 | 27 | // Width is the width of the progress bars 28 | Width int 29 | 30 | // Bars is the collection of progress bars 31 | Bars []*Bar 32 | 33 | // RefreshInterval in the time duration to wait for refreshing the output 34 | RefreshInterval time.Duration 35 | 36 | lw *uilive.Writer 37 | ticker *time.Ticker 38 | tdone chan bool 39 | mtx *sync.RWMutex 40 | } 41 | 42 | // New returns a new progress bar with defaults 43 | func New() *Progress { 44 | lw := uilive.New() 45 | lw.Out = Out 46 | 47 | return &Progress{ 48 | Width: Width, 49 | Out: Out, 50 | Bars: make([]*Bar, 0), 51 | RefreshInterval: RefreshInterval, 52 | 53 | tdone: make(chan bool), 54 | lw: lw, 55 | mtx: &sync.RWMutex{}, 56 | } 57 | } 58 | 59 | // AddBar creates a new progress bar and adds it to the default progress container 60 | func AddBar(total int) *Bar { 61 | return defaultProgress.AddBar(total) 62 | } 63 | 64 | // Start starts the rendering the progress of progress bars using the DefaultProgress. It listens for updates using `bar.Set(n)` and new bars when added using `AddBar` 65 | func Start() { 66 | defaultProgress.Start() 67 | } 68 | 69 | // Stop stops listening 70 | func Stop() { 71 | defaultProgress.Stop() 72 | } 73 | 74 | // Listen listens for updates and renders the progress bars 75 | func Listen() { 76 | defaultProgress.Listen() 77 | } 78 | 79 | func (p *Progress) SetOut(o io.Writer) { 80 | p.mtx.Lock() 81 | defer p.mtx.Unlock() 82 | 83 | p.Out = o 84 | p.lw.Out = o 85 | } 86 | 87 | func (p *Progress) SetRefreshInterval(interval time.Duration) { 88 | p.mtx.Lock() 89 | defer p.mtx.Unlock() 90 | p.RefreshInterval = interval 91 | } 92 | 93 | // AddBar creates a new progress bar and adds to the container 94 | func (p *Progress) AddBar(total int) *Bar { 95 | p.mtx.Lock() 96 | defer p.mtx.Unlock() 97 | 98 | bar := NewBar(total) 99 | bar.Width = p.Width 100 | p.Bars = append(p.Bars, bar) 101 | return bar 102 | } 103 | 104 | // Listen listens for updates and renders the progress bars 105 | func (p *Progress) Listen() { 106 | for { 107 | 108 | p.mtx.Lock() 109 | interval := p.RefreshInterval 110 | p.mtx.Unlock() 111 | 112 | select { 113 | case <-time.After(interval): 114 | p.print() 115 | case <-p.tdone: 116 | p.print() 117 | close(p.tdone) 118 | return 119 | } 120 | } 121 | } 122 | 123 | func (p *Progress) print() { 124 | p.mtx.Lock() 125 | defer p.mtx.Unlock() 126 | for _, bar := range p.Bars { 127 | fmt.Fprintln(p.lw, bar.String()) 128 | } 129 | p.lw.Flush() 130 | } 131 | 132 | // Start starts the rendering the progress of progress bars. It listens for updates using `bar.Set(n)` and new bars when added using `AddBar` 133 | func (p *Progress) Start() { 134 | go p.Listen() 135 | } 136 | 137 | // Stop stops listening 138 | func (p *Progress) Stop() { 139 | p.tdone <- true 140 | <-p.tdone 141 | } 142 | 143 | // Bypass returns a writer which allows non-buffered data to be written to the underlying output 144 | func (p *Progress) Bypass() io.Writer { 145 | return p.lw.Bypass() 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uiprogress [![GoDoc](https://godoc.org/github.com/gosuri/uiprogress?status.svg)](https://godoc.org/github.com/gosuri/uiprogress) [![Build Status](https://travis-ci.org/gosuri/uiprogress.svg?branch=master)](https://travis-ci.org/gosuri/uiprogress) 2 | 3 | A Go library to render progress bars in terminal applications. It provides a set of flexible features with a customizable API. 4 | 5 | ![example](doc/example_full.gif) 6 | 7 | Progress bars improve readability for terminal applications with long outputs by providing a concise feedback loop. 8 | 9 | ## Features 10 | 11 | * __Multiple Bars__: uiprogress can render multiple progress bars that can be tracked concurrently 12 | * __Dynamic Addition__: Add additional progress bars any time, even after the progress tracking has started 13 | * __Prepend and Append Functions__: Append or prepend completion percent and time elapsed to the progress bars 14 | * __Custom Decorator Functions__: Add custom functions around the bar along with helper functions 15 | 16 | ## Usage 17 | 18 | To start listening for progress bars, call `uiprogress.Start()` and add a progress bar using `uiprogress.AddBar(total int)`. Update the progress using `bar.Incr()` or `bar.Set(n int)`. Full source code for the below example is available at [example/simple/simple.go](example/simple/simple.go) 19 | 20 | ```go 21 | uiprogress.Start() // start rendering 22 | bar := uiprogress.AddBar(100) // Add a new bar 23 | 24 | // optionally, append and prepend completion and elapsed time 25 | bar.AppendCompleted() 26 | bar.PrependElapsed() 27 | 28 | for bar.Incr() { 29 | time.Sleep(time.Millisecond * 20) 30 | } 31 | ``` 32 | 33 | This will render the below in the terminal 34 | 35 | ![example](doc/example_simple.gif) 36 | 37 | ### Using Custom Decorators 38 | 39 | You can also add a custom decorator function in addition to default `bar.AppendCompleted()` and `bar.PrependElapsed()` decorators. The below example tracks the current step for an application deploy progress. Source code for the below example is available at [example/full/full.go](example/full/full.go) 40 | 41 | ```go 42 | var steps = []string{"downloading source", "installing deps", "compiling", "packaging", "seeding database", "deploying", "staring servers"} 43 | bar := uiprogress.AddBar(len(steps)) 44 | 45 | // prepend the current step to the bar 46 | bar.PrependFunc(func(b *uiprogress.Bar) string { 47 | return "app: " + steps[b.Current()-1] 48 | }) 49 | 50 | for bar.Incr() { 51 | time.Sleep(time.Millisecond * 10) 52 | } 53 | ``` 54 | 55 | ### Rendering Multiple bars 56 | 57 | You can add multiple bars using `uiprogress.AddBar(n)`. The below example demonstrates updating multiple bars concurrently and adding a new bar later in the pipeline. Source for this example is available at [example/multi/multi.go](example/multi/multi.go) 58 | 59 | ```go 60 | waitTime := time.Millisecond * 100 61 | uiprogress.Start() 62 | 63 | // start the progress bars in go routines 64 | var wg sync.WaitGroup 65 | 66 | bar1 := uiprogress.AddBar(20).AppendCompleted().PrependElapsed() 67 | wg.Add(1) 68 | go func() { 69 | defer wg.Done() 70 | for bar1.Incr() { 71 | time.Sleep(waitTime) 72 | } 73 | }() 74 | 75 | bar2 := uiprogress.AddBar(40).AppendCompleted().PrependElapsed() 76 | wg.Add(1) 77 | go func() { 78 | defer wg.Done() 79 | for bar2.Incr() { 80 | time.Sleep(waitTime) 81 | } 82 | }() 83 | 84 | time.Sleep(time.Second) 85 | bar3 := uiprogress.AddBar(20).PrependElapsed().AppendCompleted() 86 | wg.Add(1) 87 | go func() { 88 | defer wg.Done() 89 | for i := 1; i <= bar3.Total; i++ { 90 | bar3.Set(i) 91 | time.Sleep(waitTime) 92 | } 93 | }() 94 | 95 | // wait for all the go routines to finish 96 | wg.Wait() 97 | ``` 98 | 99 | This will produce 100 | 101 | ![example](doc/example_multi.gif) 102 | 103 | ### `Incr` counter 104 | 105 | [Bar.Incr()](https://godoc.org/github.com/gosuri/uiprogress#Bar.Incr) is an atomic counter and can be used as a general tracker, making it ideal for tracking progress of work fanned out to a lots of go routines. The source code for the below example is available at [example/incr/incr.go](example/incr/incr.go) 106 | 107 | ```go 108 | runtime.GOMAXPROCS(runtime.NumCPU()) // use all available cpu cores 109 | 110 | // create a new bar and prepend the task progress to the bar and fanout into 1k go routines 111 | count := 1000 112 | bar := uiprogress.AddBar(count).AppendCompleted().PrependElapsed() 113 | bar.PrependFunc(func(b *uiprogress.Bar) string { 114 | return fmt.Sprintf("Task (%d/%d)", b.Current(), count) 115 | }) 116 | 117 | uiprogress.Start() 118 | var wg sync.WaitGroup 119 | 120 | // fanout into go routines 121 | for i := 0; i < count; i++ { 122 | wg.Add(1) 123 | go func() { 124 | defer wg.Done() 125 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) 126 | bar.Incr() 127 | }() 128 | } 129 | time.Sleep(time.Second) // wait for a second for all the go routines to finish 130 | wg.Wait() 131 | uiprogress.Stop() 132 | ``` 133 | 134 | ## Installation 135 | 136 | ```sh 137 | $ go get -v github.com/gosuri/uiprogress 138 | ``` 139 | ## Todos 140 | 141 | - [ ] Resize bars and decorators by auto detecting window's dimensions 142 | - [ ] Handle more progress bars than vertical screen allows 143 | 144 | ## License 145 | 146 | uiprogress is released under the MIT License. See [LICENSE](https://github.com/gosuri/uiprogress/blob/master/LICENSE). 147 | -------------------------------------------------------------------------------- /bar.go: -------------------------------------------------------------------------------- 1 | package uiprogress 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gosuri/uiprogress/util/strutil" 11 | ) 12 | 13 | var ( 14 | // Fill is the default character representing completed progress 15 | Fill byte = '=' 16 | 17 | // Head is the default character that moves when progress is updated 18 | Head byte = '>' 19 | 20 | // Empty is the default character that represents the empty progress 21 | Empty byte = '-' 22 | 23 | // LeftEnd is the default character in the left most part of the progress indicator 24 | LeftEnd byte = '[' 25 | 26 | // RightEnd is the default character in the right most part of the progress indicator 27 | RightEnd byte = ']' 28 | 29 | // Width is the default width of the progress bar 30 | Width = 70 31 | 32 | // ErrMaxCurrentReached is error when trying to set current value that exceeds the total value 33 | ErrMaxCurrentReached = errors.New("errors: current value is greater total value") 34 | ) 35 | 36 | // Bar represents a progress bar 37 | type Bar struct { 38 | // Total of the total for the progress bar 39 | Total int 40 | 41 | // LeftEnd is character in the left most part of the progress indicator. Defaults to '[' 42 | LeftEnd byte 43 | 44 | // RightEnd is character in the right most part of the progress indicator. Defaults to ']' 45 | RightEnd byte 46 | 47 | // Fill is the character representing completed progress. Defaults to '=' 48 | Fill byte 49 | 50 | // Head is the character that moves when progress is updated. Defaults to '>' 51 | Head byte 52 | 53 | // Empty is the character that represents the empty progress. Default is '-' 54 | Empty byte 55 | 56 | // TimeStated is time progress began 57 | TimeStarted time.Time 58 | 59 | // Width is the width of the progress bar 60 | Width int 61 | 62 | // timeElased is the time elapsed for the progress 63 | timeElapsed time.Duration 64 | current int 65 | 66 | mtx *sync.RWMutex 67 | 68 | appendFuncs []DecoratorFunc 69 | prependFuncs []DecoratorFunc 70 | } 71 | 72 | // DecoratorFunc is a function that can be prepended and appended to the progress bar 73 | type DecoratorFunc func(b *Bar) string 74 | 75 | // NewBar returns a new progress bar 76 | func NewBar(total int) *Bar { 77 | return &Bar{ 78 | Total: total, 79 | Width: Width, 80 | LeftEnd: LeftEnd, 81 | RightEnd: RightEnd, 82 | Head: Head, 83 | Fill: Fill, 84 | Empty: Empty, 85 | 86 | mtx: &sync.RWMutex{}, 87 | } 88 | } 89 | 90 | // Set the current count of the bar. It returns ErrMaxCurrentReached when trying n exceeds the total value. This is atomic operation and concurrency safe. 91 | func (b *Bar) Set(n int) error { 92 | b.mtx.Lock() 93 | defer b.mtx.Unlock() 94 | 95 | if n > b.Total { 96 | return ErrMaxCurrentReached 97 | } 98 | b.current = n 99 | return nil 100 | } 101 | 102 | // Incr increments the current value by 1, time elapsed to current time and returns true. It returns false if the cursor has reached or exceeds total value. 103 | func (b *Bar) Incr() bool { 104 | b.mtx.Lock() 105 | defer b.mtx.Unlock() 106 | 107 | n := b.current + 1 108 | if n > b.Total { 109 | return false 110 | } 111 | var t time.Time 112 | if b.TimeStarted == t { 113 | b.TimeStarted = time.Now() 114 | } 115 | b.timeElapsed = time.Since(b.TimeStarted) 116 | b.current = n 117 | return true 118 | } 119 | 120 | // Current returns the current progress of the bar 121 | func (b *Bar) Current() int { 122 | b.mtx.RLock() 123 | defer b.mtx.RUnlock() 124 | return b.current 125 | } 126 | 127 | // AppendFunc runs the decorator function and renders the output on the right of the progress bar 128 | func (b *Bar) AppendFunc(f DecoratorFunc) *Bar { 129 | b.mtx.Lock() 130 | defer b.mtx.Unlock() 131 | b.appendFuncs = append(b.appendFuncs, f) 132 | return b 133 | } 134 | 135 | // AppendCompleted appends the completion percent to the progress bar 136 | func (b *Bar) AppendCompleted() *Bar { 137 | b.AppendFunc(func(b *Bar) string { 138 | return b.CompletedPercentString() 139 | }) 140 | return b 141 | } 142 | 143 | // AppendElapsed appends the time elapsed the be progress bar 144 | func (b *Bar) AppendElapsed() *Bar { 145 | b.AppendFunc(func(b *Bar) string { 146 | return strutil.PadLeft(b.TimeElapsedString(), 5, ' ') 147 | }) 148 | return b 149 | } 150 | 151 | // PrependFunc runs decorator function and render the output left the progress bar 152 | func (b *Bar) PrependFunc(f DecoratorFunc) *Bar { 153 | b.mtx.Lock() 154 | defer b.mtx.Unlock() 155 | b.prependFuncs = append(b.prependFuncs, f) 156 | return b 157 | } 158 | 159 | // PrependCompleted prepends the precent completed to the progress bar 160 | func (b *Bar) PrependCompleted() *Bar { 161 | b.PrependFunc(func(b *Bar) string { 162 | return b.CompletedPercentString() 163 | }) 164 | return b 165 | } 166 | 167 | // PrependElapsed prepends the time elapsed to the begining of the bar 168 | func (b *Bar) PrependElapsed() *Bar { 169 | b.PrependFunc(func(b *Bar) string { 170 | return strutil.PadLeft(b.TimeElapsedString(), 5, ' ') 171 | }) 172 | return b 173 | } 174 | 175 | // Bytes returns the byte presentation of the progress bar 176 | func (b *Bar) Bytes() []byte { 177 | completedWidth := int(float64(b.Width) * (b.CompletedPercent() / 100.00)) 178 | 179 | // add fill and empty bits 180 | var buf bytes.Buffer 181 | for i := 0; i < completedWidth; i++ { 182 | buf.WriteByte(b.Fill) 183 | } 184 | for i := 0; i < b.Width-completedWidth; i++ { 185 | buf.WriteByte(b.Empty) 186 | } 187 | 188 | // set head bit 189 | pb := buf.Bytes() 190 | if completedWidth > 0 && completedWidth < b.Width { 191 | pb[completedWidth-1] = b.Head 192 | } 193 | 194 | // set left and right ends bits 195 | pb[0], pb[len(pb)-1] = b.LeftEnd, b.RightEnd 196 | 197 | // render append functions to the right of the bar 198 | for _, f := range b.appendFuncs { 199 | pb = append(pb, ' ') 200 | pb = append(pb, []byte(f(b))...) 201 | } 202 | 203 | // render prepend functions to the left of the bar 204 | for _, f := range b.prependFuncs { 205 | args := []byte(f(b)) 206 | args = append(args, ' ') 207 | pb = append(args, pb...) 208 | } 209 | return pb 210 | } 211 | 212 | // String returns the string representation of the bar 213 | func (b *Bar) String() string { 214 | return string(b.Bytes()) 215 | } 216 | 217 | // CompletedPercent return the percent completed 218 | func (b *Bar) CompletedPercent() float64 { 219 | return (float64(b.Current()) / float64(b.Total)) * 100.00 220 | } 221 | 222 | // CompletedPercentString returns the formatted string representation of the completed percent 223 | func (b *Bar) CompletedPercentString() string { 224 | return fmt.Sprintf("%3.f%%", b.CompletedPercent()) 225 | } 226 | 227 | // TimeElapsed returns the time elapsed 228 | func (b *Bar) TimeElapsed() time.Duration { 229 | b.mtx.RLock() 230 | defer b.mtx.RUnlock() 231 | return b.timeElapsed 232 | } 233 | 234 | // TimeElapsedString returns the formatted string represenation of the time elapsed 235 | func (b *Bar) TimeElapsedString() string { 236 | return strutil.PrettyTime(b.TimeElapsed()) 237 | } 238 | --------------------------------------------------------------------------------