├── LICENSE ├── Readme.md ├── _example └── main.go ├── ci.yml ├── progress.go ├── progress_example_test.go └── progress_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Examples 4 | 5 | ![](https://user-images.githubusercontent.com/4562878/75091204-e7ad2580-556a-11ea-8302-4b38b0cfcad9.gif) 6 | 7 | --- 8 | 9 | [![GoDoc](https://godoc.org/github.com/tj/go-progress?status.svg)](https://godoc.org/github.com/tj/go-progress) 10 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 11 | ![](https://img.shields.io/badge/status-stable-green.svg) 12 | 13 | 14 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/tj/go-progress" 8 | "github.com/tj/go/term" 9 | 10 | color "github.com/aybabtme/rgbterm" 11 | ) 12 | 13 | // gray string. 14 | func gray(s string) string { 15 | return color.FgString(s, 150, 150, 150) 16 | } 17 | 18 | // purple string. 19 | func purple(s string) string { 20 | return color.FgString(s, 96, 97, 190) 21 | } 22 | 23 | func main() { 24 | term.HideCursor() 25 | defer term.ShowCursor() 26 | 27 | b := progress.NewInt(10) 28 | b.Width = 40 29 | b.StartDelimiter = gray("|") 30 | b.EndDelimiter = gray("|") 31 | b.Filled = purple("█") 32 | b.Empty = gray("░") 33 | 34 | render := term.Renderer() 35 | 36 | for i := 0; i <= 10; i++ { 37 | b.ValueInt(i) 38 | time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) 39 | render(term.CenterLine(b.String())) 40 | } 41 | 42 | term.ClearAll() 43 | } 44 | -------------------------------------------------------------------------------- /ci.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - go get -t ./... 7 | build: 8 | commands: 9 | - go test -cover -v ./... 10 | -------------------------------------------------------------------------------- /progress.go: -------------------------------------------------------------------------------- 1 | // Package progress provides a simple terminal progress bar. 2 | package progress 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "math" 10 | "strings" 11 | ) 12 | 13 | // Bar is a progress bar. 14 | type Bar struct { 15 | StartDelimiter string // StartDelimiter for the bar ("|"). 16 | EndDelimiter string // EndDelimiter for the bar ("|"). 17 | Filled string // Filled section representation ("█"). 18 | Empty string // Empty section representation ("░") 19 | Total float64 // Total value. 20 | Width int // Width of the bar. 21 | 22 | value float64 23 | tmpl *template.Template 24 | text string 25 | } 26 | 27 | // New returns a new bar with the given total. 28 | func New(total float64) *Bar { 29 | b := &Bar{ 30 | StartDelimiter: "|", 31 | EndDelimiter: "|", 32 | Filled: "█", 33 | Empty: "░", 34 | Total: total, 35 | Width: 60, 36 | } 37 | 38 | b.Template(`{{.Percent | printf "%3.0f"}}% {{.Bar}} {{.Text}}`) 39 | 40 | return b 41 | } 42 | 43 | // NewInt returns a new bar with the given total. 44 | func NewInt(total int) *Bar { 45 | return New(float64(total)) 46 | } 47 | 48 | // Text sets the text value. 49 | func (b *Bar) Text(s string) { 50 | b.text = s 51 | } 52 | 53 | // Value sets the value. 54 | func (b *Bar) Value(n float64) { 55 | if n > b.Total { 56 | panic("Bar update value cannot be greater than the total") 57 | } 58 | b.value = n 59 | } 60 | 61 | // ValueInt sets the value. 62 | func (b *Bar) ValueInt(n int) { 63 | b.Value(float64(n)) 64 | } 65 | 66 | // Percent returns the percentage 67 | func (b *Bar) percent() float64 { 68 | return (b.value / b.Total) * 100 69 | } 70 | 71 | // Bar returns the progress bar string. 72 | func (b *Bar) bar() string { 73 | p := b.value / b.Total 74 | filled := math.Ceil(float64(b.Width) * p) 75 | empty := math.Floor(float64(b.Width) - filled) 76 | s := b.StartDelimiter 77 | s += strings.Repeat(b.Filled, int(filled)) 78 | s += strings.Repeat(b.Empty, int(empty)) 79 | s += b.EndDelimiter 80 | return s 81 | } 82 | 83 | // String returns the progress bar. 84 | func (b *Bar) String() string { 85 | var buf bytes.Buffer 86 | 87 | data := struct { 88 | Value float64 89 | Total float64 90 | Percent float64 91 | StartDelimiter string 92 | EndDelimiter string 93 | Bar string 94 | Text string 95 | }{ 96 | Value: b.value, 97 | Text: b.text, 98 | StartDelimiter: b.StartDelimiter, 99 | EndDelimiter: b.EndDelimiter, 100 | Percent: b.percent(), 101 | Bar: b.bar(), 102 | } 103 | 104 | if err := b.tmpl.Execute(&buf, data); err != nil { 105 | panic(err) 106 | } 107 | 108 | return buf.String() 109 | } 110 | 111 | // WriteTo writes the progress bar to w. 112 | func (b *Bar) WriteTo(w io.Writer) (int64, error) { 113 | s := fmt.Sprintf("\r %s ", b.String()) 114 | _, err := io.WriteString(w, s) 115 | return int64(len(s)), err 116 | } 117 | 118 | // Template for rendering. This method will panic if the template fails to parse. 119 | func (b *Bar) Template(s string) { 120 | t, err := template.New("").Parse(s) 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | b.tmpl = t 126 | } 127 | -------------------------------------------------------------------------------- /progress_example_test.go: -------------------------------------------------------------------------------- 1 | package progress_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/tj/go-progress" 8 | ) 9 | 10 | func Example() { 11 | b := progress.NewInt(50) 12 | 13 | for i := 0; i <= 50; i++ { 14 | b.ValueInt(i) 15 | b.Text(fmt.Sprintf("iteration %d", i)) 16 | b.WriteTo(os.Stdout) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /progress_test.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/tj/assert" 12 | ) 13 | 14 | var preview = flag.Bool("preview", false, "Preview output rendering.") 15 | 16 | func TestBar_previewWidth(t *testing.T) { 17 | if !*preview { 18 | t.SkipNow() 19 | } 20 | 21 | b := NewInt(10) 22 | b.Width = 25 23 | b.Empty = " " 24 | 25 | for i := 0; i <= 10; i++ { 26 | b.ValueInt(i) 27 | time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) 28 | b.WriteTo(os.Stdout) 29 | } 30 | } 31 | 32 | func TestBar_previewDefaults(t *testing.T) { 33 | if !*preview { 34 | t.SkipNow() 35 | } 36 | 37 | b := NewInt(20) 38 | 39 | for i := 0; i <= 20; i++ { 40 | b.ValueInt(i) 41 | b.Text(fmt.Sprintf("iteration %d", i)) 42 | time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond) 43 | b.WriteTo(os.Stdout) 44 | } 45 | } 46 | 47 | func TestBarString(t *testing.T) { 48 | b := NewInt(1000) 49 | assert.Equal(t, ` 0% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| `, b.String()) 50 | 51 | b.ValueInt(250) 52 | assert.Equal(t, ` 25% |███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| `, b.String()) 53 | 54 | b.ValueInt(750) 55 | assert.Equal(t, ` 75% |█████████████████████████████████████████████░░░░░░░░░░░░░░░| `, b.String()) 56 | 57 | b.ValueInt(1000) 58 | assert.Equal(t, `100% |████████████████████████████████████████████████████████████| `, b.String()) 59 | } 60 | 61 | func TestBarText(t *testing.T) { 62 | b := NewInt(1000) 63 | 64 | b.Text("Building") 65 | assert.Equal(t, ` 0% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| Building`, b.String()) 66 | 67 | b.Text("Installing") 68 | b.ValueInt(250) 69 | assert.Equal(t, ` 25% |███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| Installing`, b.String()) 70 | } 71 | 72 | func TestBarTemplate(t *testing.T) { 73 | b := NewInt(1000) 74 | b.Template(`{{.Bar}} {{.Percent}}%`) 75 | 76 | assert.Equal(t, `|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| 0%`, b.String()) 77 | 78 | b.ValueInt(250) 79 | assert.Equal(t, `|███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░| 25%`, b.String()) 80 | 81 | b.ValueInt(750) 82 | assert.Equal(t, `|█████████████████████████████████████████████░░░░░░░░░░░░░░░| 75%`, b.String()) 83 | 84 | b.ValueInt(1000) 85 | assert.Equal(t, `|████████████████████████████████████████████████████████████| 100%`, b.String()) 86 | } 87 | 88 | func TestBarDelimiters(t *testing.T) { 89 | b := NewInt(1000) 90 | b.StartDelimiter = "[" 91 | b.EndDelimiter = "]" 92 | assert.Equal(t, ` 0% [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] `, b.String()) 93 | } 94 | --------------------------------------------------------------------------------