├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_V1.md ├── example_copy_test.go ├── example_multiple_test.go ├── example_test.go ├── format.go ├── format_test.go ├── go.mod ├── go.sum ├── pb.go ├── pb_appengine.go ├── pb_plan9.go ├── pb_test.go ├── pb_win.go ├── pb_x.go ├── pool.go ├── pool_win.go ├── pool_x.go ├── reader.go ├── runecount.go ├── runecount_test.go ├── termios_bsd.go ├── termios_sysv.go ├── v3 ├── LICENSE ├── element.go ├── element_test.go ├── go.mod ├── go.sum ├── io.go ├── io_test.go ├── pb.go ├── pb_test.go ├── pool.go ├── pool_win.go ├── pool_x.go ├── preset.go ├── preset_test.go ├── speed.go ├── template.go ├── template_test.go ├── termutil │ ├── term.go │ ├── term_aix.go │ ├── term_appengine.go │ ├── term_bsd.go │ ├── term_linux.go │ ├── term_nix.go │ ├── term_plan9.go │ ├── term_solaris.go │ ├── term_win.go │ └── term_x.go ├── util.go └── util_test.go └── writer.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/v3" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.23 20 | 21 | - name: Test 22 | working-directory: ./v3 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | arch: 3 | - amd64 4 | - ppc64le 5 | go: 6 | - 1.12.x 7 | - 1.15.x 8 | sudo: false 9 | os: 10 | - linux 11 | - osx 12 | before_install: 13 | - go get github.com/mattn/goveralls 14 | script: 15 | - $GOPATH/bin/goveralls -package github.com/cheggaaa/pb/v3 -repotoken QT1y5Iujb8ete6JOiE0ytKFlBDv9vheWc 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024, Sergey Cherepanov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminal progress bar for Go 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/cheggaaa/pb/badge.svg)](https://coveralls.io/github/cheggaaa/pb) 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/cheggaaa/pb/v3 9 | ``` 10 | 11 | Documentation for v1 bar available [here](README_V1.md). 12 | 13 | ## Quick start 14 | 15 | ```Go 16 | package main 17 | 18 | import ( 19 | "time" 20 | 21 | "github.com/cheggaaa/pb/v3" 22 | ) 23 | 24 | func main() { 25 | count := 100000 26 | 27 | // create and start new bar 28 | bar := pb.StartNew(count) 29 | 30 | // start bar from 'default' template 31 | // bar := pb.Default.Start(count) 32 | 33 | // start bar from 'simple' template 34 | // bar := pb.Simple.Start(count) 35 | 36 | // start bar from 'full' template 37 | // bar := pb.Full.Start(count) 38 | 39 | for i := 0; i < count; i++ { 40 | bar.Increment() 41 | time.Sleep(time.Millisecond) 42 | } 43 | 44 | // finish bar 45 | bar.Finish() 46 | } 47 | ``` 48 | 49 | Result will be like this: 50 | 51 | ``` 52 | > go run test.go 53 | 37158 / 100000 [---------------->_______________________________] 37.16% 916 p/s 54 | ``` 55 | 56 | ## Settings 57 | 58 | ```Go 59 | // create bar 60 | bar := pb.New(count) 61 | 62 | // refresh info every second (default 200ms) 63 | bar.SetRefreshRate(time.Second) 64 | 65 | // force set io.Writer, by default it's os.Stderr 66 | bar.SetWriter(os.Stdout) 67 | 68 | // bar will format numbers as bytes (B, KiB, MiB, etc) 69 | bar.Set(pb.Bytes, true) 70 | 71 | // bar use SI bytes prefix names (B, kB) instead of IEC (B, KiB) 72 | bar.Set(pb.SIBytesPrefix, true) 73 | 74 | // set custom bar template 75 | bar.SetTemplateString(myTemplate) 76 | 77 | // check for error after template set 78 | if err := bar.Err(); err != nil { 79 | return 80 | } 81 | 82 | // start bar 83 | bar.Start() 84 | ``` 85 | 86 | ## Progress bar for IO Operations 87 | 88 | ```Go 89 | package main 90 | 91 | import ( 92 | "crypto/rand" 93 | "io" 94 | "io/ioutil" 95 | 96 | "github.com/cheggaaa/pb/v3" 97 | ) 98 | 99 | func main() { 100 | var limit int64 = 1024 * 1024 * 500 101 | 102 | // we will copy 500 MiB from /dev/rand to /dev/null 103 | reader := io.LimitReader(rand.Reader, limit) 104 | writer := ioutil.Discard 105 | 106 | // start new bar 107 | bar := pb.Full.Start64(limit) 108 | 109 | // create proxy reader 110 | barReader := bar.NewProxyReader(reader) 111 | 112 | // copy from proxy reader 113 | io.Copy(writer, barReader) 114 | 115 | // finish bar 116 | bar.Finish() 117 | } 118 | ``` 119 | 120 | ## Custom Progress Bar templates 121 | 122 | Rendering based on builtin [text/template](https://pkg.go.dev/text/template) package. You can use existing pb's elements or create you own. 123 | 124 | All available elements are described in the [element.go](v3/element.go) file. 125 | 126 | #### All in one example: 127 | 128 | ```Go 129 | tmpl := `{{ red "With funcs:" }} {{ bar . "<" "-" (cycle . "↖" "↗" "↘" "↙" ) "." ">"}} {{speed . | rndcolor }} {{percent .}} {{string . "my_green_string" | green}} {{string . "my_blue_string" | blue}}` 130 | 131 | // start bar based on our template 132 | bar := pb.ProgressBarTemplate(tmpl).Start64(limit) 133 | 134 | // set values for string elements 135 | bar.Set("my_green_string", "green").Set("my_blue_string", "blue") 136 | ``` 137 | -------------------------------------------------------------------------------- /README_V1.md: -------------------------------------------------------------------------------- 1 | # Terminal progress bar for Go 2 | 3 | Simple progress bar for console programs. 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/cheggaaa/pb 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```Go 14 | package main 15 | 16 | import ( 17 | "github.com/cheggaaa/pb" 18 | "time" 19 | ) 20 | 21 | func main() { 22 | count := 100000 23 | bar := pb.StartNew(count) 24 | for i := 0; i < count; i++ { 25 | bar.Increment() 26 | time.Sleep(time.Millisecond) 27 | } 28 | bar.FinishPrint("The End!") 29 | } 30 | 31 | ``` 32 | 33 | Result will be like this: 34 | 35 | ``` 36 | > go run test.go 37 | 37158 / 100000 [================>_______________________________] 37.16% 1m11s 38 | ``` 39 | 40 | ## Customization 41 | 42 | ```Go 43 | // create bar 44 | bar := pb.New(count) 45 | 46 | // refresh info every second (default 200ms) 47 | bar.SetRefreshRate(time.Second) 48 | 49 | // show percents (by default already true) 50 | bar.ShowPercent = true 51 | 52 | // show bar (by default already true) 53 | bar.ShowBar = true 54 | 55 | // no counters 56 | bar.ShowCounters = false 57 | 58 | // show "time left" 59 | bar.ShowTimeLeft = true 60 | 61 | // show average speed 62 | bar.ShowSpeed = true 63 | 64 | // sets the width of the progress bar 65 | bar.SetWidth(80) 66 | 67 | // sets the width of the progress bar, but if terminal size smaller will be ignored 68 | bar.SetMaxWidth(80) 69 | 70 | // convert output to readable format (like KB, MB) 71 | bar.SetUnits(pb.U_BYTES) 72 | 73 | // and start 74 | bar.Start() 75 | ``` 76 | 77 | ## Progress bar for IO Operations 78 | 79 | ```go 80 | // create and start bar 81 | bar := pb.New(myDataLen).SetUnits(pb.U_BYTES) 82 | bar.Start() 83 | 84 | // my io.Reader 85 | r := myReader 86 | 87 | // my io.Writer 88 | w := myWriter 89 | 90 | // create proxy reader 91 | reader := bar.NewProxyReader(r) 92 | 93 | // and copy from pb reader 94 | io.Copy(w, reader) 95 | 96 | ``` 97 | 98 | ```go 99 | // create and start bar 100 | bar := pb.New(myDataLen).SetUnits(pb.U_BYTES) 101 | bar.Start() 102 | 103 | // my io.Reader 104 | r := myReader 105 | 106 | // my io.Writer 107 | w := myWriter 108 | 109 | // create multi writer 110 | writer := io.MultiWriter(w, bar) 111 | 112 | // and copy 113 | io.Copy(writer, r) 114 | 115 | bar.Finish() 116 | ``` 117 | 118 | ## Custom Progress Bar Look-and-feel 119 | 120 | ```go 121 | bar.Format("<.- >") 122 | ``` 123 | 124 | ## Multiple Progress Bars (experimental and unstable) 125 | 126 | Do not print to terminal while pool is active. 127 | 128 | ```go 129 | package main 130 | 131 | import ( 132 | "math/rand" 133 | "sync" 134 | "time" 135 | 136 | "github.com/cheggaaa/pb" 137 | ) 138 | 139 | func main() { 140 | // create bars 141 | first := pb.New(200).Prefix("First ") 142 | second := pb.New(200).Prefix("Second ") 143 | third := pb.New(200).Prefix("Third ") 144 | // start pool 145 | pool, err := pb.StartPool(first, second, third) 146 | if err != nil { 147 | panic(err) 148 | } 149 | // update bars 150 | wg := new(sync.WaitGroup) 151 | for _, bar := range []*pb.ProgressBar{first, second, third} { 152 | wg.Add(1) 153 | go func(cb *pb.ProgressBar) { 154 | for n := 0; n < 200; n++ { 155 | cb.Increment() 156 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(100))) 157 | } 158 | cb.Finish() 159 | wg.Done() 160 | }(bar) 161 | } 162 | wg.Wait() 163 | // close pool 164 | pool.Stop() 165 | } 166 | ``` 167 | 168 | The result will be as follows: 169 | 170 | ``` 171 | $ go run example/multiple.go 172 | First 34 / 200 [=========>---------------------------------------------] 17.00% 00m08s 173 | Second 42 / 200 [===========>------------------------------------------] 21.00% 00m06s 174 | Third 36 / 200 [=========>---------------------------------------------] 18.00% 00m08s 175 | ``` 176 | -------------------------------------------------------------------------------- /example_copy_test.go: -------------------------------------------------------------------------------- 1 | package pb_test 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/cheggaaa/pb" 13 | ) 14 | 15 | func Example_copy() { 16 | // check args 17 | if len(os.Args) < 3 { 18 | printUsage() 19 | return 20 | } 21 | sourceName, destName := os.Args[1], os.Args[2] 22 | 23 | // check source 24 | var source io.Reader 25 | var sourceSize int64 26 | if strings.HasPrefix(sourceName, "http://") { 27 | // open as url 28 | resp, err := http.Get(sourceName) 29 | if err != nil { 30 | fmt.Printf("Can't get %s: %v\n", sourceName, err) 31 | return 32 | } 33 | defer resp.Body.Close() 34 | if resp.StatusCode != http.StatusOK { 35 | fmt.Printf("Server return non-200 status: %v\n", resp.Status) 36 | return 37 | } 38 | i, _ := strconv.Atoi(resp.Header.Get("Content-Length")) 39 | sourceSize = int64(i) 40 | source = resp.Body 41 | } else { 42 | // open as file 43 | s, err := os.Open(sourceName) 44 | if err != nil { 45 | fmt.Printf("Can't open %s: %v\n", sourceName, err) 46 | return 47 | } 48 | defer s.Close() 49 | // get source size 50 | sourceStat, err := s.Stat() 51 | if err != nil { 52 | fmt.Printf("Can't stat %s: %v\n", sourceName, err) 53 | return 54 | } 55 | sourceSize = sourceStat.Size() 56 | source = s 57 | } 58 | 59 | // create dest 60 | dest, err := os.Create(destName) 61 | if err != nil { 62 | fmt.Printf("Can't create %s: %v\n", destName, err) 63 | return 64 | } 65 | defer dest.Close() 66 | 67 | // create bar 68 | bar := pb.New(int(sourceSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10) 69 | bar.ShowSpeed = true 70 | bar.Start() 71 | 72 | // create proxy reader 73 | reader := bar.NewProxyReader(source) 74 | 75 | // and copy from reader 76 | io.Copy(dest, reader) 77 | bar.Finish() 78 | } 79 | 80 | func printUsage() { 81 | fmt.Println("copy [source file or url] [dest file]") 82 | } 83 | -------------------------------------------------------------------------------- /example_multiple_test.go: -------------------------------------------------------------------------------- 1 | package pb_test 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/cheggaaa/pb" 9 | ) 10 | 11 | func Example_multiple() { 12 | // create bars 13 | first := pb.New(200).Prefix("First ") 14 | second := pb.New(200).Prefix("Second ") 15 | third := pb.New(200).Prefix("Third ") 16 | // start pool 17 | pool, err := pb.StartPool(first, second, third) 18 | if err != nil { 19 | panic(err) 20 | } 21 | // update bars 22 | wg := new(sync.WaitGroup) 23 | for _, bar := range []*pb.ProgressBar{first, second, third} { 24 | wg.Add(1) 25 | go func(cb *pb.ProgressBar) { 26 | for n := 0; n < 200; n++ { 27 | cb.Increment() 28 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(100))) 29 | } 30 | cb.Finish() 31 | wg.Done() 32 | }(bar) 33 | } 34 | wg.Wait() 35 | // close pool 36 | pool.Stop() 37 | } 38 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package pb_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cheggaaa/pb" 7 | ) 8 | 9 | func Example() { 10 | count := 5000 11 | bar := pb.New(count) 12 | 13 | // show percents (by default already true) 14 | bar.ShowPercent = true 15 | 16 | // show bar (by default already true) 17 | bar.ShowBar = true 18 | 19 | bar.ShowCounters = true 20 | 21 | bar.ShowTimeLeft = true 22 | 23 | // and start 24 | bar.Start() 25 | for i := 0; i < count; i++ { 26 | bar.Increment() 27 | time.Sleep(time.Millisecond) 28 | } 29 | bar.FinishPrint("The End!") 30 | } 31 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Units int 9 | 10 | const ( 11 | // U_NO are default units, they represent a simple value and are not formatted at all. 12 | U_NO Units = iota 13 | // U_BYTES units are formatted in a human readable way (B, KiB, MiB, ...) 14 | U_BYTES 15 | // U_BYTES_DEC units are like U_BYTES, but base 10 (B, KB, MB, ...) 16 | U_BYTES_DEC 17 | // U_DURATION units are formatted in a human readable way (3h14m15s) 18 | U_DURATION 19 | ) 20 | 21 | const ( 22 | KiB = 1024 23 | MiB = 1048576 24 | GiB = 1073741824 25 | TiB = 1099511627776 26 | 27 | KB = 1e3 28 | MB = 1e6 29 | GB = 1e9 30 | TB = 1e12 31 | ) 32 | 33 | func Format(i int64) *formatter { 34 | return &formatter{n: i} 35 | } 36 | 37 | type formatter struct { 38 | n int64 39 | unit Units 40 | width int 41 | perSec bool 42 | } 43 | 44 | func (f *formatter) To(unit Units) *formatter { 45 | f.unit = unit 46 | return f 47 | } 48 | 49 | func (f *formatter) Width(width int) *formatter { 50 | f.width = width 51 | return f 52 | } 53 | 54 | func (f *formatter) PerSec() *formatter { 55 | f.perSec = true 56 | return f 57 | } 58 | 59 | func (f *formatter) String() (out string) { 60 | switch f.unit { 61 | case U_BYTES: 62 | out = formatBytes(f.n) 63 | case U_BYTES_DEC: 64 | out = formatBytesDec(f.n) 65 | case U_DURATION: 66 | out = formatDuration(f.n) 67 | default: 68 | out = fmt.Sprintf(fmt.Sprintf("%%%dd", f.width), f.n) 69 | } 70 | if f.perSec { 71 | out += "/s" 72 | } 73 | return 74 | } 75 | 76 | // Convert bytes to human readable string. Like 2 MiB, 64.2 KiB, 52 B 77 | func formatBytes(i int64) (result string) { 78 | switch { 79 | case i >= TiB: 80 | result = fmt.Sprintf("%.02f TiB", float64(i)/TiB) 81 | case i >= GiB: 82 | result = fmt.Sprintf("%.02f GiB", float64(i)/GiB) 83 | case i >= MiB: 84 | result = fmt.Sprintf("%.02f MiB", float64(i)/MiB) 85 | case i >= KiB: 86 | result = fmt.Sprintf("%.02f KiB", float64(i)/KiB) 87 | default: 88 | result = fmt.Sprintf("%d B", i) 89 | } 90 | return 91 | } 92 | 93 | // Convert bytes to base-10 human readable string. Like 2 MB, 64.2 KB, 52 B 94 | func formatBytesDec(i int64) (result string) { 95 | switch { 96 | case i >= TB: 97 | result = fmt.Sprintf("%.02f TB", float64(i)/TB) 98 | case i >= GB: 99 | result = fmt.Sprintf("%.02f GB", float64(i)/GB) 100 | case i >= MB: 101 | result = fmt.Sprintf("%.02f MB", float64(i)/MB) 102 | case i >= KB: 103 | result = fmt.Sprintf("%.02f KB", float64(i)/KB) 104 | default: 105 | result = fmt.Sprintf("%d B", i) 106 | } 107 | return 108 | } 109 | 110 | func formatDuration(n int64) (result string) { 111 | d := time.Duration(n) 112 | if d > time.Hour*24 { 113 | result = fmt.Sprintf("%dd", d/24/time.Hour) 114 | d -= (d / time.Hour / 24) * (time.Hour * 24) 115 | } 116 | if d > time.Hour { 117 | result = fmt.Sprintf("%s%dh", result, d/time.Hour) 118 | d -= d / time.Hour * time.Hour 119 | } 120 | m := d / time.Minute 121 | d -= m * time.Minute 122 | s := d / time.Second 123 | result = fmt.Sprintf("%s%02dm%02ds", result, m, s) 124 | return 125 | } 126 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func Test_DefaultsToInteger(t *testing.T) { 10 | value := int64(1000) 11 | expected := strconv.Itoa(int(value)) 12 | actual := Format(value).String() 13 | 14 | if actual != expected { 15 | t.Errorf("Expected {%s} was {%s}", expected, actual) 16 | } 17 | } 18 | 19 | func Test_CanFormatAsInteger(t *testing.T) { 20 | value := int64(1000) 21 | expected := strconv.Itoa(int(value)) 22 | actual := Format(value).To(U_NO).String() 23 | 24 | if actual != expected { 25 | t.Errorf("Expected {%s} was {%s}", expected, actual) 26 | } 27 | } 28 | 29 | func Test_CanFormatAsBytes(t *testing.T) { 30 | inputs := []struct { 31 | v int64 32 | e string 33 | }{ 34 | {v: 1000, e: "1000 B"}, 35 | {v: 1024, e: "1.00 KiB"}, 36 | {v: 3*MiB + 140*KiB, e: "3.14 MiB"}, 37 | {v: 2 * GiB, e: "2.00 GiB"}, 38 | {v: 2048 * GiB, e: "2.00 TiB"}, 39 | } 40 | 41 | for _, input := range inputs { 42 | actual := Format(input.v).To(U_BYTES).String() 43 | if actual != input.e { 44 | t.Errorf("Expected {%s} was {%s}", input.e, actual) 45 | } 46 | } 47 | } 48 | 49 | func Test_CanFormatAsBytesDec(t *testing.T) { 50 | inputs := []struct { 51 | v int64 52 | e string 53 | }{ 54 | {v: 999, e: "999 B"}, 55 | {v: 1024, e: "1.02 KB"}, 56 | {v: 3*MB + 140*KB, e: "3.14 MB"}, 57 | {v: 2 * GB, e: "2.00 GB"}, 58 | {v: 2048 * GB, e: "2.05 TB"}, 59 | } 60 | 61 | for _, input := range inputs { 62 | actual := Format(input.v).To(U_BYTES_DEC).String() 63 | if actual != input.e { 64 | t.Errorf("Expected {%s} was {%s}", input.e, actual) 65 | } 66 | } 67 | } 68 | 69 | func Test_CanFormatDuration(t *testing.T) { 70 | value := 10 * time.Minute 71 | expected := "10m00s" 72 | actual := Format(int64(value)).To(U_DURATION).String() 73 | if actual != expected { 74 | t.Errorf("Expected {%s} was {%s}", expected, actual) 75 | } 76 | } 77 | 78 | func Test_CanFormatLongDuration(t *testing.T) { 79 | value := 62 * time.Hour + 13 * time.Second 80 | expected := "2d14h00m13s" 81 | actual := Format(int64(value)).To(U_DURATION).String() 82 | if actual != expected { 83 | t.Errorf("Expected {%s} was {%s}", expected, actual) 84 | } 85 | } 86 | 87 | func Test_DefaultUnitsWidth(t *testing.T) { 88 | value := 10 89 | expected := " 10" 90 | actual := Format(int64(value)).Width(7).String() 91 | if actual != expected { 92 | t.Errorf("Expected {%s} was {%s}", expected, actual) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cheggaaa/pb 2 | 3 | require ( 4 | github.com/fatih/color v1.9.0 5 | github.com/mattn/go-colorable v0.1.4 6 | github.com/mattn/go-runewidth v0.0.4 7 | golang.org/x/sys v0.1.0 8 | ) 9 | 10 | go 1.12 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 2 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 3 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 4 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 5 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 6 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 7 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 8 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 9 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 10 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 13 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | -------------------------------------------------------------------------------- /pb.go: -------------------------------------------------------------------------------- 1 | // Simple console progress bars 2 | package pb 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "math" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | "unicode/utf8" 13 | ) 14 | 15 | // Current version 16 | const Version = "1.0.29" 17 | 18 | const ( 19 | // Default refresh rate - 200ms 20 | DEFAULT_REFRESH_RATE = time.Millisecond * 200 21 | FORMAT = "[=>-]" 22 | ) 23 | 24 | // DEPRECATED 25 | // variables for backward compatibility, from now do not work 26 | // use pb.Format and pb.SetRefreshRate 27 | var ( 28 | DefaultRefreshRate = DEFAULT_REFRESH_RATE 29 | BarStart, BarEnd, Empty, Current, CurrentN string 30 | ) 31 | 32 | // Create new progress bar object 33 | func New(total int) *ProgressBar { 34 | return New64(int64(total)) 35 | } 36 | 37 | // Create new progress bar object using int64 as total 38 | func New64(total int64) *ProgressBar { 39 | pb := &ProgressBar{ 40 | Total: total, 41 | RefreshRate: DEFAULT_REFRESH_RATE, 42 | ShowPercent: true, 43 | ShowCounters: true, 44 | ShowBar: true, 45 | ShowTimeLeft: true, 46 | ShowElapsedTime: false, 47 | ShowFinalTime: true, 48 | Units: U_NO, 49 | ManualUpdate: false, 50 | finish: make(chan struct{}), 51 | } 52 | return pb.Format(FORMAT) 53 | } 54 | 55 | // Create new object and start 56 | func StartNew(total int) *ProgressBar { 57 | return New(total).Start() 58 | } 59 | 60 | // Callback for custom output 61 | // For example: 62 | // bar.Callback = func(s string) { 63 | // mySuperPrint(s) 64 | // } 65 | // 66 | type Callback func(out string) 67 | 68 | type ProgressBar struct { 69 | current int64 // current must be first member of struct (https://code.google.com/p/go/issues/detail?id=5278) 70 | previous int64 71 | 72 | Total int64 73 | RefreshRate time.Duration 74 | ShowPercent, ShowCounters bool 75 | ShowSpeed, ShowTimeLeft, ShowBar bool 76 | ShowFinalTime, ShowElapsedTime bool 77 | HideCountersTotal bool 78 | Output io.Writer 79 | Callback Callback 80 | NotPrint bool 81 | Units Units 82 | Width int 83 | ForceWidth bool 84 | ManualUpdate bool 85 | AutoStat bool 86 | 87 | // Default width for the time box. 88 | UnitsWidth int 89 | TimeBoxWidth int 90 | 91 | finishOnce sync.Once //Guards isFinish 92 | finish chan struct{} 93 | isFinish bool 94 | 95 | startTime time.Time 96 | startValue int64 97 | 98 | changeTime time.Time 99 | 100 | prefix, postfix string 101 | 102 | mu sync.Mutex 103 | lastPrint string 104 | 105 | BarStart string 106 | BarEnd string 107 | Empty string 108 | Current string 109 | CurrentN string 110 | 111 | AlwaysUpdate bool 112 | } 113 | 114 | // Start print 115 | func (pb *ProgressBar) Start() *ProgressBar { 116 | pb.startTime = time.Now() 117 | pb.startValue = atomic.LoadInt64(&pb.current) 118 | if atomic.LoadInt64(&pb.Total) == 0 { 119 | pb.ShowTimeLeft = false 120 | pb.ShowPercent = false 121 | pb.AutoStat = false 122 | } 123 | if !pb.ManualUpdate { 124 | pb.Update() // Initial printing of the bar before running the bar refresher. 125 | go pb.refresher() 126 | } 127 | return pb 128 | } 129 | 130 | // Increment current value 131 | func (pb *ProgressBar) Increment() int { 132 | return pb.Add(1) 133 | } 134 | 135 | // Get current value 136 | func (pb *ProgressBar) Get() int64 { 137 | c := atomic.LoadInt64(&pb.current) 138 | return c 139 | } 140 | 141 | // Set current value 142 | func (pb *ProgressBar) Set(current int) *ProgressBar { 143 | return pb.Set64(int64(current)) 144 | } 145 | 146 | // Set64 sets the current value as int64 147 | func (pb *ProgressBar) Set64(current int64) *ProgressBar { 148 | atomic.StoreInt64(&pb.current, current) 149 | return pb 150 | } 151 | 152 | // Add to current value 153 | func (pb *ProgressBar) Add(add int) int { 154 | return int(pb.Add64(int64(add))) 155 | } 156 | 157 | func (pb *ProgressBar) Add64(add int64) int64 { 158 | return atomic.AddInt64(&pb.current, add) 159 | } 160 | 161 | // Set prefix string 162 | func (pb *ProgressBar) Prefix(prefix string) *ProgressBar { 163 | pb.mu.Lock() 164 | defer pb.mu.Unlock() 165 | pb.prefix = prefix 166 | return pb 167 | } 168 | 169 | // Set postfix string 170 | func (pb *ProgressBar) Postfix(postfix string) *ProgressBar { 171 | pb.mu.Lock() 172 | defer pb.mu.Unlock() 173 | pb.postfix = postfix 174 | return pb 175 | } 176 | 177 | // Set custom format for bar 178 | // Example: bar.Format("[=>_]") 179 | // Example: bar.Format("[\x00=\x00>\x00-\x00]") // \x00 is the delimiter 180 | func (pb *ProgressBar) Format(format string) *ProgressBar { 181 | var formatEntries []string 182 | if utf8.RuneCountInString(format) == 5 { 183 | formatEntries = strings.Split(format, "") 184 | } else { 185 | formatEntries = strings.Split(format, "\x00") 186 | } 187 | if len(formatEntries) == 5 { 188 | pb.BarStart = formatEntries[0] 189 | pb.BarEnd = formatEntries[4] 190 | pb.Empty = formatEntries[3] 191 | pb.Current = formatEntries[1] 192 | pb.CurrentN = formatEntries[2] 193 | } 194 | return pb 195 | } 196 | 197 | // Set bar refresh rate 198 | func (pb *ProgressBar) SetRefreshRate(rate time.Duration) *ProgressBar { 199 | pb.RefreshRate = rate 200 | return pb 201 | } 202 | 203 | // Set units 204 | // bar.SetUnits(U_NO) - by default 205 | // bar.SetUnits(U_BYTES) - for Mb, Kb, etc 206 | func (pb *ProgressBar) SetUnits(units Units) *ProgressBar { 207 | pb.Units = units 208 | return pb 209 | } 210 | 211 | // Set max width, if width is bigger than terminal width, will be ignored 212 | func (pb *ProgressBar) SetMaxWidth(width int) *ProgressBar { 213 | pb.Width = width 214 | pb.ForceWidth = false 215 | return pb 216 | } 217 | 218 | // Set bar width 219 | func (pb *ProgressBar) SetWidth(width int) *ProgressBar { 220 | pb.Width = width 221 | pb.ForceWidth = true 222 | return pb 223 | } 224 | 225 | // End print 226 | func (pb *ProgressBar) Finish() { 227 | //Protect multiple calls 228 | pb.finishOnce.Do(func() { 229 | close(pb.finish) 230 | pb.write(atomic.LoadInt64(&pb.Total), atomic.LoadInt64(&pb.current)) 231 | pb.mu.Lock() 232 | defer pb.mu.Unlock() 233 | switch { 234 | case pb.Output != nil: 235 | fmt.Fprintln(pb.Output) 236 | case !pb.NotPrint: 237 | fmt.Println() 238 | } 239 | pb.isFinish = true 240 | }) 241 | } 242 | 243 | // IsFinished return boolean 244 | func (pb *ProgressBar) IsFinished() bool { 245 | pb.mu.Lock() 246 | defer pb.mu.Unlock() 247 | return pb.isFinish 248 | } 249 | 250 | // End print and write string 'str' 251 | func (pb *ProgressBar) FinishPrint(str string) { 252 | pb.Finish() 253 | if pb.Output != nil { 254 | fmt.Fprintln(pb.Output, str) 255 | } else { 256 | fmt.Println(str) 257 | } 258 | } 259 | 260 | // implement io.Writer 261 | func (pb *ProgressBar) Write(p []byte) (n int, err error) { 262 | n = len(p) 263 | pb.Add(n) 264 | return 265 | } 266 | 267 | // implement io.Reader 268 | func (pb *ProgressBar) Read(p []byte) (n int, err error) { 269 | n = len(p) 270 | pb.Add(n) 271 | return 272 | } 273 | 274 | // Create new proxy reader over bar 275 | // Takes io.Reader or io.ReadCloser 276 | func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader { 277 | return &Reader{r, pb} 278 | } 279 | 280 | // Create new proxy writer over bar 281 | // Takes io.Writer or io.WriteCloser 282 | func (pb *ProgressBar) NewProxyWriter(r io.Writer) *Writer { 283 | return &Writer{r, pb} 284 | } 285 | 286 | func (pb *ProgressBar) write(total, current int64) { 287 | pb.mu.Lock() 288 | defer pb.mu.Unlock() 289 | width := pb.GetWidth() 290 | 291 | var percentBox, countersBox, timeLeftBox, timeSpentBox, speedBox, barBox, end, out string 292 | 293 | // percents 294 | if pb.ShowPercent { 295 | var percent float64 296 | if total > 0 { 297 | percent = float64(current) / (float64(total) / float64(100)) 298 | } else { 299 | percent = float64(current) / float64(100) 300 | } 301 | percentBox = fmt.Sprintf(" %6.02f%%", percent) 302 | } 303 | 304 | // counters 305 | if pb.ShowCounters { 306 | current := Format(current).To(pb.Units).Width(pb.UnitsWidth) 307 | if total > 0 { 308 | if pb.HideCountersTotal { 309 | countersBox = fmt.Sprintf(" %s ", current) 310 | } else { 311 | totalS := Format(total).To(pb.Units).Width(pb.UnitsWidth) 312 | countersBox = fmt.Sprintf(" %s / %s ", current, totalS) 313 | } 314 | } else { 315 | if pb.HideCountersTotal { 316 | countersBox = fmt.Sprintf(" %s ", current) 317 | } else { 318 | countersBox = fmt.Sprintf(" %s / ? ", current) 319 | } 320 | } 321 | } 322 | 323 | // time left 324 | currentFromStart := current - pb.startValue 325 | fromStart := time.Since(pb.startTime) 326 | lastChangeTime := pb.changeTime 327 | fromChange := lastChangeTime.Sub(pb.startTime) 328 | 329 | if pb.ShowElapsedTime { 330 | timeSpentBox = fmt.Sprintf(" %s ", (fromStart/time.Second)*time.Second) 331 | } 332 | 333 | select { 334 | case <-pb.finish: 335 | if pb.ShowFinalTime { 336 | var left = (fromStart / time.Second) * time.Second 337 | timeLeftBox = fmt.Sprintf(" %s", left.String()) 338 | } 339 | default: 340 | if pb.ShowTimeLeft && currentFromStart > 0 { 341 | perEntry := fromChange / time.Duration(currentFromStart) 342 | var left time.Duration 343 | if total > 0 { 344 | left = time.Duration(total-current) * perEntry 345 | left -= time.Since(lastChangeTime) 346 | left = (left / time.Second) * time.Second 347 | } 348 | if left > 0 { 349 | timeLeft := Format(int64(left)).To(U_DURATION).String() 350 | timeLeftBox = fmt.Sprintf(" %s", timeLeft) 351 | } 352 | } 353 | } 354 | 355 | if len(timeLeftBox) < pb.TimeBoxWidth { 356 | timeLeftBox = fmt.Sprintf("%s%s", strings.Repeat(" ", pb.TimeBoxWidth-len(timeLeftBox)), timeLeftBox) 357 | } 358 | 359 | // speed 360 | if pb.ShowSpeed && currentFromStart > 0 { 361 | fromStart := time.Since(pb.startTime) 362 | speed := float64(currentFromStart) / (float64(fromStart) / float64(time.Second)) 363 | speedBox = " " + Format(int64(speed)).To(pb.Units).Width(pb.UnitsWidth).PerSec().String() 364 | } 365 | 366 | barWidth := escapeAwareRuneCountInString(countersBox + pb.BarStart + pb.BarEnd + percentBox + timeSpentBox + timeLeftBox + speedBox + pb.prefix + pb.postfix) 367 | // bar 368 | if pb.ShowBar { 369 | size := width - barWidth 370 | if size > 0 { 371 | if total > 0 { 372 | curSize := int(math.Ceil((float64(current) / float64(total)) * float64(size))) 373 | emptySize := size - curSize 374 | barBox = pb.BarStart 375 | if emptySize < 0 { 376 | emptySize = 0 377 | } 378 | if curSize > size { 379 | curSize = size 380 | } 381 | 382 | cursorLen := escapeAwareRuneCountInString(pb.Current) 383 | if emptySize <= 0 { 384 | barBox += strings.Repeat(pb.Current, curSize/cursorLen) 385 | } else if curSize > 0 { 386 | cursorEndLen := escapeAwareRuneCountInString(pb.CurrentN) 387 | cursorRepetitions := (curSize - cursorEndLen) / cursorLen 388 | barBox += strings.Repeat(pb.Current, cursorRepetitions) 389 | barBox += pb.CurrentN 390 | } 391 | 392 | emptyLen := escapeAwareRuneCountInString(pb.Empty) 393 | barBox += strings.Repeat(pb.Empty, emptySize/emptyLen) 394 | barBox += pb.BarEnd 395 | } else { 396 | pos := size - int(current)%int(size) 397 | barBox = pb.BarStart 398 | if pos-1 > 0 { 399 | barBox += strings.Repeat(pb.Empty, pos-1) 400 | } 401 | barBox += pb.Current 402 | if size-pos-1 > 0 { 403 | barBox += strings.Repeat(pb.Empty, size-pos-1) 404 | } 405 | barBox += pb.BarEnd 406 | } 407 | } 408 | } 409 | 410 | // check len 411 | out = pb.prefix + timeSpentBox + countersBox + barBox + percentBox + speedBox + timeLeftBox + pb.postfix 412 | 413 | if cl := escapeAwareRuneCountInString(out); cl < width { 414 | end = strings.Repeat(" ", width-cl) 415 | } 416 | 417 | // and print! 418 | pb.lastPrint = out + end 419 | isFinish := pb.isFinish 420 | 421 | switch { 422 | case isFinish: 423 | return 424 | case pb.Output != nil: 425 | fmt.Fprint(pb.Output, "\r"+out+end) 426 | case pb.Callback != nil: 427 | pb.Callback(out + end) 428 | case !pb.NotPrint: 429 | fmt.Print("\r" + out + end) 430 | } 431 | } 432 | 433 | // GetTerminalWidth - returns terminal width for all platforms. 434 | func GetTerminalWidth() (int, error) { 435 | return terminalWidth() 436 | } 437 | 438 | func (pb *ProgressBar) GetWidth() int { 439 | if pb.ForceWidth { 440 | return pb.Width 441 | } 442 | 443 | width := pb.Width 444 | termWidth, _ := terminalWidth() 445 | if width == 0 || termWidth <= width { 446 | width = termWidth 447 | } 448 | 449 | return width 450 | } 451 | 452 | // Write the current state of the progressbar 453 | func (pb *ProgressBar) Update() { 454 | c := atomic.LoadInt64(&pb.current) 455 | p := atomic.LoadInt64(&pb.previous) 456 | t := atomic.LoadInt64(&pb.Total) 457 | if p != c { 458 | pb.mu.Lock() 459 | pb.changeTime = time.Now() 460 | pb.mu.Unlock() 461 | atomic.StoreInt64(&pb.previous, c) 462 | } 463 | pb.write(t, c) 464 | if pb.AutoStat { 465 | if c == 0 { 466 | pb.startTime = time.Now() 467 | pb.startValue = 0 468 | } else if c >= t && !pb.isFinish{ 469 | pb.Finish() 470 | } 471 | } 472 | } 473 | 474 | // String return the last bar print 475 | func (pb *ProgressBar) String() string { 476 | pb.mu.Lock() 477 | defer pb.mu.Unlock() 478 | return pb.lastPrint 479 | } 480 | 481 | // SetTotal atomically sets new total count 482 | func (pb *ProgressBar) SetTotal(total int) *ProgressBar { 483 | return pb.SetTotal64(int64(total)) 484 | } 485 | 486 | // SetTotal64 atomically sets new total count 487 | func (pb *ProgressBar) SetTotal64(total int64) *ProgressBar { 488 | atomic.StoreInt64(&pb.Total, total) 489 | return pb 490 | } 491 | 492 | // Reset bar and set new total count 493 | // Does effect only on finished bar 494 | func (pb *ProgressBar) Reset(total int) *ProgressBar { 495 | pb.mu.Lock() 496 | defer pb.mu.Unlock() 497 | if pb.isFinish { 498 | pb.SetTotal(total).Set(0) 499 | atomic.StoreInt64(&pb.previous, 0) 500 | } 501 | return pb 502 | } 503 | 504 | // Internal loop for refreshing the progressbar 505 | func (pb *ProgressBar) refresher() { 506 | for { 507 | select { 508 | case <-pb.finish: 509 | return 510 | case <-time.After(pb.RefreshRate): 511 | pb.Update() 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /pb_appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine js 2 | 3 | package pb 4 | 5 | import "errors" 6 | 7 | // terminalWidth returns width of the terminal, which is not supported 8 | // and should always failed on appengine classic which is a sandboxed PaaS. 9 | func terminalWidth() (int, error) { 10 | return 0, errors.New("Not supported") 11 | } 12 | -------------------------------------------------------------------------------- /pb_plan9.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | ) 10 | 11 | var ErrPoolWasStarted = errors.New("Bar pool was started") 12 | 13 | var ( 14 | echoLockMutex sync.Mutex 15 | consctl *os.File 16 | ) 17 | 18 | // terminalWidth returns width of the terminal. 19 | func terminalWidth() (int, error) { 20 | return 0, errors.New("Not Supported") 21 | } 22 | 23 | func lockEcho() (shutdownCh chan struct{}, err error) { 24 | echoLockMutex.Lock() 25 | defer echoLockMutex.Unlock() 26 | 27 | if consctl != nil { 28 | return nil, ErrPoolWasStarted 29 | } 30 | consctl, err = os.OpenFile("/dev/consctl", os.O_WRONLY, 0) 31 | if err != nil { 32 | return nil, err 33 | } 34 | _, err = consctl.WriteString("rawon") 35 | if err != nil { 36 | consctl.Close() 37 | consctl = nil 38 | return nil, err 39 | } 40 | shutdownCh = make(chan struct{}) 41 | go catchTerminate(shutdownCh) 42 | return 43 | } 44 | 45 | func unlockEcho() error { 46 | echoLockMutex.Lock() 47 | defer echoLockMutex.Unlock() 48 | 49 | if consctl == nil { 50 | return nil 51 | } 52 | if err := consctl.Close(); err != nil { 53 | return err 54 | } 55 | consctl = nil 56 | return nil 57 | } 58 | 59 | // listen exit signals and restore terminal state 60 | func catchTerminate(shutdownCh chan struct{}) { 61 | sig := make(chan os.Signal, 1) 62 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGKILL) 63 | defer signal.Stop(sig) 64 | select { 65 | case <-shutdownCh: 66 | unlockEcho() 67 | case <-sig: 68 | unlockEcho() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pb_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/fatih/color" 11 | "github.com/mattn/go-colorable" 12 | ) 13 | 14 | func Test_IncrementAddsOne(t *testing.T) { 15 | count := 5000 16 | bar := New(count) 17 | expected := 1 18 | actual := bar.Increment() 19 | 20 | if actual != expected { 21 | t.Errorf("Expected {%d} was {%d}", expected, actual) 22 | } 23 | } 24 | 25 | func Test_Width(t *testing.T) { 26 | count := 5000 27 | bar := New(count) 28 | width := 100 29 | bar.SetWidth(100).Callback = func(out string) { 30 | if len(out) != width { 31 | t.Errorf("Bar width expected {%d} was {%d}", len(out), width) 32 | } 33 | } 34 | bar.Start() 35 | bar.Increment() 36 | bar.Finish() 37 | } 38 | 39 | func Test_MultipleFinish(t *testing.T) { 40 | bar := New(5000) 41 | bar.Add(2000) 42 | bar.Finish() 43 | bar.Finish() 44 | } 45 | 46 | func TestWriteRace(t *testing.T) { 47 | outBuffer := &bytes.Buffer{} 48 | totalCount := 20 49 | bar := New(totalCount) 50 | bar.Output = outBuffer 51 | bar.Start() 52 | var wg sync.WaitGroup 53 | for i := 0; i < totalCount; i++ { 54 | wg.Add(1) 55 | go func() { 56 | bar.Increment() 57 | time.Sleep(250 * time.Millisecond) 58 | wg.Done() 59 | }() 60 | } 61 | wg.Wait() 62 | bar.Finish() 63 | } 64 | 65 | func Test_Format(t *testing.T) { 66 | bar := New(5000).Format(strings.Join([]string{ 67 | color.GreenString("["), 68 | color.New(color.BgGreen).SprintFunc()("o"), 69 | color.New(color.BgHiGreen).SprintFunc()("o"), 70 | color.New(color.BgRed).SprintFunc()("o"), 71 | color.GreenString("]"), 72 | }, "\x00")) 73 | w := colorable.NewColorableStdout() 74 | bar.Callback = func(out string) { 75 | w.Write([]byte(out)) 76 | } 77 | bar.Add(2000) 78 | bar.Finish() 79 | bar.Finish() 80 | } 81 | 82 | func Test_MultiCharacter(t *testing.T) { 83 | bar := New(5).Format(strings.Join([]string{"[[[", "---", ">>", "....", "]]"}, "\x00")) 84 | bar.Start() 85 | for i := 0; i < 5; i++ { 86 | time.Sleep(500 * time.Millisecond) 87 | bar.Increment() 88 | } 89 | 90 | time.Sleep(500 * time.Millisecond) 91 | bar.Finish() 92 | } 93 | 94 | func Test_AutoStat(t *testing.T) { 95 | bar := New(5) 96 | bar.AutoStat = true 97 | bar.Start() 98 | time.Sleep(2 * time.Second) 99 | //real start work 100 | for i := 0; i < 5; i++ { 101 | time.Sleep(500 * time.Millisecond) 102 | bar.Increment() 103 | } 104 | //real finish work 105 | time.Sleep(2 * time.Second) 106 | bar.Finish() 107 | } 108 | 109 | func Test_Finish_PrintNewline(t *testing.T) { 110 | bar := New(5) 111 | buf := &bytes.Buffer{} 112 | bar.Output = buf 113 | bar.Finish() 114 | 115 | expected := "\n" 116 | actual := buf.String() 117 | //Finish should write newline to bar.Output 118 | if !strings.HasSuffix(actual, expected) { 119 | t.Errorf("Expected %q to have suffix %q", expected, actual) 120 | } 121 | } 122 | 123 | func Test_FinishPrint(t *testing.T) { 124 | bar := New(5) 125 | buf := &bytes.Buffer{} 126 | bar.Output = buf 127 | bar.FinishPrint("foo") 128 | 129 | expected := "foo\n" 130 | actual := buf.String() 131 | //FinishPrint should write to bar.Output 132 | if !strings.HasSuffix(actual, expected) { 133 | t.Errorf("Expected %q to have suffix %q", expected, actual) 134 | } 135 | } 136 | 137 | func Test_Reset(t *testing.T) { 138 | bar := StartNew(5) 139 | for i := 0; i < 5; i++ { 140 | bar.Increment() 141 | } 142 | if actual := bar.Get(); actual != 5 { 143 | t.Errorf("Expected: %d; actual: %d", 5, actual) 144 | } 145 | bar.Finish() 146 | bar.Reset(10).Start() 147 | defer bar.Finish() 148 | if actual := bar.Get(); actual != 0 { 149 | t.Errorf("Expected: %d; actual: %d", 0, actual) 150 | } 151 | if actual := bar.Total; actual != 10 { 152 | t.Errorf("Expected: %d; actual: %d", 10, actual) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pb_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package pb 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | "sync" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | var tty = os.Stdin 15 | 16 | var ( 17 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 18 | 19 | // GetConsoleScreenBufferInfo retrieves information about the 20 | // specified console screen buffer. 21 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 22 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 23 | 24 | // GetConsoleMode retrieves the current input mode of a console's 25 | // input buffer or the current output mode of a console screen buffer. 26 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx 27 | getConsoleMode = kernel32.NewProc("GetConsoleMode") 28 | 29 | // SetConsoleMode sets the input mode of a console's input buffer 30 | // or the output mode of a console screen buffer. 31 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx 32 | setConsoleMode = kernel32.NewProc("SetConsoleMode") 33 | 34 | // SetConsoleCursorPosition sets the cursor position in the 35 | // specified console screen buffer. 36 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx 37 | setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 38 | ) 39 | 40 | type ( 41 | // Defines the coordinates of the upper left and lower right corners 42 | // of a rectangle. 43 | // See 44 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx 45 | smallRect struct { 46 | Left, Top, Right, Bottom int16 47 | } 48 | 49 | // Defines the coordinates of a character cell in a console screen 50 | // buffer. The origin of the coordinate system (0,0) is at the top, left cell 51 | // of the buffer. 52 | // See 53 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx 54 | coordinates struct { 55 | X, Y int16 56 | } 57 | 58 | word int16 59 | 60 | // Contains information about a console screen buffer. 61 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx 62 | consoleScreenBufferInfo struct { 63 | dwSize coordinates 64 | dwCursorPosition coordinates 65 | wAttributes word 66 | srWindow smallRect 67 | dwMaximumWindowSize coordinates 68 | } 69 | ) 70 | 71 | // terminalWidth returns width of the terminal. 72 | func terminalWidth() (width int, err error) { 73 | var info consoleScreenBufferInfo 74 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 75 | if e != 0 { 76 | return 0, error(e) 77 | } 78 | return int(info.dwSize.X) - 1, nil 79 | } 80 | 81 | func getCursorPos() (pos coordinates, err error) { 82 | var info consoleScreenBufferInfo 83 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 84 | if e != 0 { 85 | return info.dwCursorPosition, error(e) 86 | } 87 | return info.dwCursorPosition, nil 88 | } 89 | 90 | func setCursorPos(pos coordinates) error { 91 | _, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0) 92 | if e != 0 { 93 | return error(e) 94 | } 95 | return nil 96 | } 97 | 98 | var ErrPoolWasStarted = errors.New("Bar pool was started") 99 | 100 | var echoLocked bool 101 | var echoLockMutex sync.Mutex 102 | 103 | var oldState word 104 | 105 | func lockEcho() (shutdownCh chan struct{}, err error) { 106 | echoLockMutex.Lock() 107 | defer echoLockMutex.Unlock() 108 | if echoLocked { 109 | err = ErrPoolWasStarted 110 | return 111 | } 112 | echoLocked = true 113 | 114 | if _, _, e := syscall.Syscall(getConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&oldState)), 0); e != 0 { 115 | err = fmt.Errorf("Can't get terminal settings: %v", e) 116 | return 117 | } 118 | 119 | newState := oldState 120 | const ENABLE_ECHO_INPUT = 0x0004 121 | const ENABLE_LINE_INPUT = 0x0002 122 | newState = newState & (^(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) 123 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(newState), 0); e != 0 { 124 | err = fmt.Errorf("Can't set terminal settings: %v", e) 125 | return 126 | } 127 | 128 | shutdownCh = make(chan struct{}) 129 | return 130 | } 131 | 132 | func unlockEcho() (err error) { 133 | echoLockMutex.Lock() 134 | defer echoLockMutex.Unlock() 135 | if !echoLocked { 136 | return 137 | } 138 | echoLocked = false 139 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(oldState), 0); e != 0 { 140 | err = fmt.Errorf("Can't set terminal settings") 141 | } 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /pb_x.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly aix zos 2 | // +build !appengine !js 3 | 4 | package pb 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | "syscall" 13 | 14 | "golang.org/x/sys/unix" 15 | ) 16 | 17 | var ErrPoolWasStarted = errors.New("Bar pool was started") 18 | 19 | var ( 20 | echoLockMutex sync.Mutex 21 | origTermStatePtr *unix.Termios 22 | tty *os.File 23 | istty bool 24 | ) 25 | 26 | func init() { 27 | echoLockMutex.Lock() 28 | defer echoLockMutex.Unlock() 29 | 30 | var err error 31 | tty, err = os.Open("/dev/tty") 32 | istty = true 33 | if err != nil { 34 | tty = os.Stdin 35 | istty = false 36 | } 37 | } 38 | 39 | // terminalWidth returns width of the terminal. 40 | func terminalWidth() (int, error) { 41 | if !istty { 42 | return 0, errors.New("Not Supported") 43 | } 44 | echoLockMutex.Lock() 45 | defer echoLockMutex.Unlock() 46 | 47 | fd := int(tty.Fd()) 48 | 49 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | return int(ws.Col), nil 55 | } 56 | 57 | func lockEcho() (shutdownCh chan struct{}, err error) { 58 | echoLockMutex.Lock() 59 | defer echoLockMutex.Unlock() 60 | if istty { 61 | if origTermStatePtr != nil { 62 | return shutdownCh, ErrPoolWasStarted 63 | } 64 | 65 | fd := int(tty.Fd()) 66 | 67 | origTermStatePtr, err = unix.IoctlGetTermios(fd, ioctlReadTermios) 68 | if err != nil { 69 | return nil, fmt.Errorf("Can't get terminal settings: %v", err) 70 | } 71 | 72 | oldTermios := *origTermStatePtr 73 | newTermios := oldTermios 74 | newTermios.Lflag &^= syscall.ECHO 75 | newTermios.Lflag |= syscall.ICANON | syscall.ISIG 76 | newTermios.Iflag |= syscall.ICRNL 77 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newTermios); err != nil { 78 | return nil, fmt.Errorf("Can't set terminal settings: %v", err) 79 | } 80 | 81 | } 82 | shutdownCh = make(chan struct{}) 83 | go catchTerminate(shutdownCh) 84 | return 85 | } 86 | 87 | func unlockEcho() error { 88 | echoLockMutex.Lock() 89 | defer echoLockMutex.Unlock() 90 | if istty { 91 | if origTermStatePtr == nil { 92 | return nil 93 | } 94 | 95 | fd := int(tty.Fd()) 96 | 97 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, origTermStatePtr); err != nil { 98 | return fmt.Errorf("Can't set terminal settings: %v", err) 99 | } 100 | 101 | } 102 | origTermStatePtr = nil 103 | 104 | return nil 105 | } 106 | 107 | // listen exit signals and restore terminal state 108 | func catchTerminate(shutdownCh chan struct{}) { 109 | sig := make(chan os.Signal, 1) 110 | signal.Notify(sig, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL) 111 | defer signal.Stop(sig) 112 | select { 113 | case <-shutdownCh: 114 | unlockEcho() 115 | case <-sig: 116 | unlockEcho() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly windows plan9 aix zos 2 | 3 | package pb 4 | 5 | import ( 6 | "io" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Create and start new pool with given bars 12 | // You need call pool.Stop() after work 13 | func StartPool(pbs ...*ProgressBar) (pool *Pool, err error) { 14 | pool = new(Pool) 15 | if err = pool.Start(); err != nil { 16 | return 17 | } 18 | pool.Add(pbs...) 19 | return 20 | } 21 | 22 | // NewPool initialises a pool with progress bars, but 23 | // doesn't start it. You need to call Start manually 24 | func NewPool(pbs ...*ProgressBar) (pool *Pool) { 25 | pool = new(Pool) 26 | pool.Add(pbs...) 27 | return 28 | } 29 | 30 | type Pool struct { 31 | Output io.Writer 32 | RefreshRate time.Duration 33 | bars []*ProgressBar 34 | lastBarsCount int 35 | shutdownCh chan struct{} 36 | workerCh chan struct{} 37 | m sync.Mutex 38 | finishOnce sync.Once 39 | } 40 | 41 | // Add progress bars. 42 | func (p *Pool) Add(pbs ...*ProgressBar) { 43 | p.m.Lock() 44 | defer p.m.Unlock() 45 | for _, bar := range pbs { 46 | bar.ManualUpdate = true 47 | bar.NotPrint = true 48 | bar.Start() 49 | p.bars = append(p.bars, bar) 50 | } 51 | } 52 | 53 | func (p *Pool) Start() (err error) { 54 | p.RefreshRate = DefaultRefreshRate 55 | p.shutdownCh, err = lockEcho() 56 | if err != nil { 57 | return 58 | } 59 | p.workerCh = make(chan struct{}) 60 | go p.writer() 61 | return 62 | } 63 | 64 | func (p *Pool) writer() { 65 | var first = true 66 | defer func() { 67 | if !first { 68 | p.print(false) 69 | } else { 70 | p.print(true) 71 | p.print(false) 72 | } 73 | close(p.workerCh) 74 | }() 75 | 76 | for { 77 | select { 78 | case <-time.After(p.RefreshRate): 79 | if p.print(first) { 80 | p.print(false) 81 | return 82 | } 83 | first = false 84 | case <-p.shutdownCh: 85 | return 86 | } 87 | } 88 | } 89 | 90 | // Stop Restore terminal state and close pool 91 | func (p *Pool) Stop() error { 92 | p.finishOnce.Do(func() { 93 | if p.shutdownCh != nil { 94 | close(p.shutdownCh) 95 | } 96 | }) 97 | 98 | // Wait for the worker to complete 99 | <-p.workerCh 100 | 101 | 102 | return unlockEcho() 103 | } 104 | -------------------------------------------------------------------------------- /pool_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package pb 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | ) 9 | 10 | func (p *Pool) print(first bool) bool { 11 | p.m.Lock() 12 | defer p.m.Unlock() 13 | var out string 14 | if !first { 15 | coords, err := getCursorPos() 16 | if err != nil { 17 | log.Panic(err) 18 | } 19 | coords.Y -= int16(p.lastBarsCount) 20 | if coords.Y < 0 { 21 | coords.Y = 0 22 | } 23 | coords.X = 0 24 | 25 | err = setCursorPos(coords) 26 | if err != nil { 27 | log.Panic(err) 28 | } 29 | } 30 | isFinished := true 31 | for _, bar := range p.bars { 32 | if !bar.IsFinished() { 33 | isFinished = false 34 | } 35 | bar.Update() 36 | out += fmt.Sprintf("\r%s\n", bar.String()) 37 | } 38 | if p.Output != nil { 39 | fmt.Fprint(p.Output, out) 40 | } else { 41 | fmt.Print(out) 42 | } 43 | p.lastBarsCount = len(p.bars) 44 | return isFinished 45 | } 46 | -------------------------------------------------------------------------------- /pool_x.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly plan9 aix zos 2 | 3 | package pb 4 | 5 | import "fmt" 6 | 7 | func (p *Pool) print(first bool) bool { 8 | p.m.Lock() 9 | defer p.m.Unlock() 10 | var out string 11 | if !first { 12 | out = fmt.Sprintf("\033[%dA", p.lastBarsCount) 13 | } 14 | isFinished := true 15 | for _, bar := range p.bars { 16 | if !bar.IsFinished() { 17 | isFinished = false 18 | } 19 | bar.Update() 20 | out += fmt.Sprintf("\r%s\n", bar.String()) 21 | } 22 | if p.Output != nil { 23 | fmt.Fprint(p.Output, out) 24 | } else { 25 | fmt.Print(out) 26 | } 27 | p.lastBarsCount = len(p.bars) 28 | return isFinished 29 | } 30 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // It's proxy reader, implement io.Reader 8 | type Reader struct { 9 | io.Reader 10 | bar *ProgressBar 11 | } 12 | 13 | func (r *Reader) Read(p []byte) (n int, err error) { 14 | n, err = r.Reader.Read(p) 15 | r.bar.Add(n) 16 | return 17 | } 18 | 19 | // Close the reader when it implements io.Closer 20 | func (r *Reader) Close() (err error) { 21 | r.bar.Finish() 22 | if closer, ok := r.Reader.(io.Closer); ok { 23 | return closer.Close() 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /runecount.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "github.com/mattn/go-runewidth" 5 | "regexp" 6 | ) 7 | 8 | // Finds the control character sequences (like colors) 9 | var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d") 10 | 11 | func escapeAwareRuneCountInString(s string) int { 12 | n := runewidth.StringWidth(s) 13 | for _, sm := range ctrlFinder.FindAllString(s, -1) { 14 | n -= runewidth.StringWidth(sm) 15 | } 16 | return n 17 | } 18 | -------------------------------------------------------------------------------- /runecount_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import "testing" 4 | 5 | func Test_RuneCount(t *testing.T) { 6 | s := string([]byte{ 7 | 27, 91, 51, 49, 109, // {Red} 8 | 72, 101, 108, 108, 111, // Hello 9 | 44, 32, // , 10 | 112, 108, 97, 121, 103, 114, 111, 117, 110, 100, // Playground 11 | 27, 91, 48, 109, // {Reset} 12 | }) 13 | if e, l := 17, escapeAwareRuneCountInString(s); l != e { 14 | t.Errorf("Invalid length %d, expected %d", l, e) 15 | } 16 | s = "進捗 " 17 | if e, l := 5, escapeAwareRuneCountInString(s); l != e { 18 | t.Errorf("Invalid length %d, expected %d", l, e) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /termios_bsd.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd netbsd openbsd dragonfly 2 | // +build !appengine 3 | 4 | package pb 5 | 6 | import "syscall" 7 | 8 | const ioctlReadTermios = syscall.TIOCGETA 9 | const ioctlWriteTermios = syscall.TIOCSETA 10 | -------------------------------------------------------------------------------- /termios_sysv.go: -------------------------------------------------------------------------------- 1 | // +build linux solaris aix zos 2 | // +build !appengine 3 | 4 | package pb 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | const ioctlReadTermios = unix.TCGETS 9 | const ioctlWriteTermios = unix.TCSETS 10 | -------------------------------------------------------------------------------- /v3/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024, Sergey Cherepanov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /v3/element.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const ( 13 | adElPlaceholder = "%_ad_el_%" 14 | adElPlaceholderLen = len(adElPlaceholder) 15 | ) 16 | 17 | var ( 18 | defaultBarEls = [5]string{"[", "-", ">", "_", "]"} 19 | ) 20 | 21 | // Element is an interface for bar elements 22 | type Element interface { 23 | ProgressElement(state *State, args ...string) string 24 | } 25 | 26 | // ElementFunc type implements Element interface and created for simplify elements 27 | type ElementFunc func(state *State, args ...string) string 28 | 29 | // ProgressElement just call self func 30 | func (e ElementFunc) ProgressElement(state *State, args ...string) string { 31 | return e(state, args...) 32 | } 33 | 34 | var elementsM sync.Mutex 35 | 36 | var elements = map[string]Element{ 37 | "percent": ElementPercent, 38 | "counters": ElementCounters, 39 | "bar": adaptiveWrap(ElementBar), 40 | "speed": ElementSpeed, 41 | "rtime": ElementRemainingTime, 42 | "etime": ElementElapsedTime, 43 | "string": ElementString, 44 | "cycle": ElementCycle, 45 | } 46 | 47 | // RegisterElement give you a chance to use custom elements 48 | func RegisterElement(name string, el Element, adaptive bool) { 49 | if adaptive { 50 | el = adaptiveWrap(el) 51 | } 52 | elementsM.Lock() 53 | elements[name] = el 54 | elementsM.Unlock() 55 | } 56 | 57 | type argsHelper []string 58 | 59 | func (args argsHelper) getOr(n int, value string) string { 60 | if len(args) > n { 61 | return args[n] 62 | } 63 | return value 64 | } 65 | 66 | func (args argsHelper) getNotEmptyOr(n int, value string) (v string) { 67 | if v = args.getOr(n, value); v == "" { 68 | return value 69 | } 70 | return 71 | } 72 | 73 | func adaptiveWrap(el Element) Element { 74 | return ElementFunc(func(state *State, args ...string) string { 75 | state.recalc = append(state.recalc, ElementFunc(func(s *State, _ ...string) (result string) { 76 | s.adaptive = true 77 | result = el.ProgressElement(s, args...) 78 | s.adaptive = false 79 | return 80 | })) 81 | return adElPlaceholder 82 | }) 83 | } 84 | 85 | // ElementPercent shows current percent of progress. 86 | // Optionally can take one or two string arguments. 87 | // First string will be used as value for format float64, default is "%.02f%%". 88 | // Second string will be used when percent can't be calculated, default is "?%" 89 | // In template use as follows: {{percent .}} or {{percent . "%.03f%%"}} or {{percent . "%.03f%%" "?"}} 90 | var ElementPercent ElementFunc = func(state *State, args ...string) string { 91 | argsh := argsHelper(args) 92 | if state.Total() > 0 { 93 | return fmt.Sprintf( 94 | argsh.getNotEmptyOr(0, "%.02f%%"), 95 | float64(state.Value())/(float64(state.Total())/float64(100)), 96 | ) 97 | } 98 | return argsh.getOr(1, "?%") 99 | } 100 | 101 | // ElementCounters shows current and total values. 102 | // Optionally can take one or two string arguments. 103 | // First string will be used as format value when Total is present (>0). Default is "%s / %s" 104 | // Second string will be used when total <= 0. Default is "%[1]s" 105 | // In template use as follows: {{counters .}} or {{counters . "%s/%s"}} or {{counters . "%s/%s" "%s/?"}} 106 | var ElementCounters ElementFunc = func(state *State, args ...string) string { 107 | var f string 108 | if state.Total() > 0 { 109 | f = argsHelper(args).getNotEmptyOr(0, "%s / %s") 110 | } else { 111 | f = argsHelper(args).getNotEmptyOr(1, "%[1]s") 112 | } 113 | return fmt.Sprintf(f, state.Format(state.Value()), state.Format(state.Total())) 114 | } 115 | 116 | type elementKey int 117 | 118 | const ( 119 | barObj elementKey = iota 120 | speedObj 121 | cycleObj 122 | ) 123 | 124 | type bar struct { 125 | eb [5][]byte // elements in bytes 126 | cc [5]int // cell counts 127 | buf *bytes.Buffer 128 | } 129 | 130 | func (p *bar) write(state *State, eln, width int) int { 131 | repeat := width / p.cc[eln] 132 | remainder := width % p.cc[eln] 133 | for i := 0; i < repeat; i++ { 134 | p.buf.Write(p.eb[eln]) 135 | } 136 | if remainder > 0 { 137 | StripStringToBuffer(string(p.eb[eln]), remainder, p.buf) 138 | } 139 | return width 140 | } 141 | 142 | func getProgressObj(state *State, args ...string) (p *bar) { 143 | var ok bool 144 | if p, ok = state.Get(barObj).(*bar); !ok { 145 | p = &bar{ 146 | buf: bytes.NewBuffer(nil), 147 | } 148 | state.Set(barObj, p) 149 | } 150 | argsH := argsHelper(args) 151 | for i := range p.eb { 152 | arg := argsH.getNotEmptyOr(i, defaultBarEls[i]) 153 | if string(p.eb[i]) != arg { 154 | p.cc[i] = CellCount(arg) 155 | p.eb[i] = []byte(arg) 156 | if p.cc[i] == 0 { 157 | p.cc[i] = 1 158 | p.eb[i] = []byte(" ") 159 | } 160 | } 161 | } 162 | return 163 | } 164 | 165 | // ElementBar make progress bar view [-->__] 166 | // Optionally can take up to 5 string arguments. Defaults is "[", "-", ">", "_", "]" 167 | // In template use as follows: {{bar . }} or {{bar . "<" "oOo" "|" "~" ">"}} 168 | // Color args: {{bar . (red "[") (green "-") ... 169 | var ElementBar ElementFunc = func(state *State, args ...string) string { 170 | // init 171 | var p = getProgressObj(state, args...) 172 | 173 | total, value := state.Total(), state.Value() 174 | if total < 0 { 175 | total = -total 176 | } 177 | if value < 0 { 178 | value = -value 179 | } 180 | 181 | // check for overflow 182 | if total != 0 && value > total { 183 | total = value 184 | } 185 | 186 | p.buf.Reset() 187 | 188 | var widthLeft = state.AdaptiveElWidth() 189 | if widthLeft <= 0 || !state.IsAdaptiveWidth() { 190 | widthLeft = 30 191 | } 192 | 193 | // write left border 194 | if p.cc[0] < widthLeft { 195 | widthLeft -= p.write(state, 0, p.cc[0]) 196 | } else { 197 | p.write(state, 0, widthLeft) 198 | return p.buf.String() 199 | } 200 | 201 | // check right border size 202 | if p.cc[4] < widthLeft { 203 | // write later 204 | widthLeft -= p.cc[4] 205 | } else { 206 | p.write(state, 4, widthLeft) 207 | return p.buf.String() 208 | } 209 | 210 | var curCount int 211 | 212 | if total > 0 { 213 | // calculate count of currenct space 214 | curCount = int(math.Ceil((float64(value) / float64(total)) * float64(widthLeft))) 215 | } 216 | 217 | // write bar 218 | if total == value && state.IsFinished() { 219 | widthLeft -= p.write(state, 1, curCount) 220 | } else if toWrite := curCount - p.cc[2]; toWrite > 0 { 221 | widthLeft -= p.write(state, 1, toWrite) 222 | widthLeft -= p.write(state, 2, p.cc[2]) 223 | } else if curCount > 0 { 224 | widthLeft -= p.write(state, 2, curCount) 225 | } 226 | if widthLeft > 0 { 227 | widthLeft -= p.write(state, 3, widthLeft) 228 | } 229 | // write right border 230 | p.write(state, 4, p.cc[4]) 231 | // cut result and return string 232 | return p.buf.String() 233 | } 234 | 235 | func elapsedTime(state *State) string { 236 | elapsed := state.Time().Sub(state.StartTime()) 237 | var precision time.Duration 238 | var ok bool 239 | if precision, ok = state.Get(TimeRound).(time.Duration); !ok { 240 | // default behavior: round to nearest .1s when elapsed < 10s 241 | // 242 | // we compare with 9.95s as opposed to 10s to avoid an annoying 243 | // interaction with the fixed precision display code below, 244 | // where 9.9s would be rounded to 10s but printed as 10.0s, and 245 | // then 10.0s would be rounded to 10s and printed as 10s 246 | if elapsed < 9950*time.Millisecond { 247 | precision = 100 * time.Millisecond 248 | } else { 249 | precision = time.Second 250 | } 251 | } 252 | rounded := elapsed.Round(precision) 253 | if precision < time.Second && rounded >= time.Second { 254 | // special handling to ensure string is shown with the given 255 | // precision, with trailing zeros after the decimal point if 256 | // necessary 257 | reference := (2*time.Second - time.Nanosecond).Truncate(precision).String() 258 | // reference looks like "1.9[...]9s", telling us how many 259 | // decimal digits we need 260 | neededDecimals := len(reference) - 3 261 | s := rounded.String() 262 | dotIndex := strings.LastIndex(s, ".") 263 | if dotIndex != -1 { 264 | // s has the form "[stuff].[decimals]s" 265 | decimals := len(s) - dotIndex - 2 266 | extraZeros := neededDecimals - decimals 267 | return fmt.Sprintf("%s%ss", s[:len(s)-1], strings.Repeat("0", extraZeros)) 268 | } else { 269 | // s has the form "[stuff]s" 270 | return fmt.Sprintf("%s.%ss", s[:len(s)-1], strings.Repeat("0", neededDecimals)) 271 | } 272 | } else { 273 | return rounded.String() 274 | } 275 | } 276 | 277 | // ElementRemainingTime calculates remaining time based on speed (EWMA) 278 | // Optionally can take one or two string arguments. 279 | // First string will be used as value for format time duration string, default is "%s". 280 | // Second string will be used when bar finished and value indicates elapsed time, default is "%s" 281 | // Third string will be used when value not available, default is "?" 282 | // In template use as follows: {{rtime .}} or {{rtime . "%s remain"}} or {{rtime . "%s remain" "%s total" "???"}} 283 | var ElementRemainingTime ElementFunc = func(state *State, args ...string) string { 284 | if state.IsFinished() { 285 | return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state)) 286 | } 287 | sp := getSpeedObj(state).value(state) 288 | if sp > 0 { 289 | remain := float64(state.Total() - state.Value()) 290 | remainDur := time.Duration(remain/sp) * time.Second 291 | return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), remainDur) 292 | } 293 | return argsHelper(args).getOr(2, "?") 294 | } 295 | 296 | // ElementElapsedTime shows elapsed time 297 | // Optionally can take one argument - it's format for time string. 298 | // In template use as follows: {{etime .}} or {{etime . "%s elapsed"}} 299 | var ElementElapsedTime ElementFunc = func(state *State, args ...string) string { 300 | return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state)) 301 | } 302 | 303 | // ElementString get value from bar by given key and print them 304 | // bar.Set("myKey", "string to print") 305 | // In template use as follows: {{string . "myKey"}} 306 | var ElementString ElementFunc = func(state *State, args ...string) string { 307 | if len(args) == 0 { 308 | return "" 309 | } 310 | v := state.Get(args[0]) 311 | if v == nil { 312 | return "" 313 | } 314 | return fmt.Sprint(v) 315 | } 316 | 317 | // ElementCycle return next argument for every call 318 | // In template use as follows: {{cycle . "1" "2" "3"}} 319 | // Or mix width other elements: {{ bar . "" "" (cycle . "↖" "↗" "↘" "↙" )}} 320 | var ElementCycle ElementFunc = func(state *State, args ...string) string { 321 | if len(args) == 0 { 322 | return "" 323 | } 324 | n, _ := state.Get(cycleObj).(int) 325 | if n >= len(args) { 326 | n = 0 327 | } 328 | state.Set(cycleObj, n+1) 329 | return args[n] 330 | } 331 | -------------------------------------------------------------------------------- /v3/element_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | func testState(total, value int64, maxWidth int, bools ...bool) (s *State) { 13 | s = &State{ 14 | total: total, 15 | current: value, 16 | adaptiveElWidth: maxWidth, 17 | ProgressBar: new(ProgressBar), 18 | } 19 | if len(bools) > 0 { 20 | s.Set(Bytes, bools[0]) 21 | } 22 | if len(bools) > 1 && bools[1] { 23 | s.adaptive = true 24 | } 25 | return 26 | } 27 | 28 | func testElementBarString(t *testing.T, state *State, el Element, want string, args ...string) { 29 | if state.ProgressBar == nil { 30 | state.ProgressBar = new(ProgressBar) 31 | } 32 | res := el.ProgressElement(state, args...) 33 | if res != want { 34 | t.Errorf("Unexpected result: '%s'; want: '%s'", res, want) 35 | } 36 | if state.IsAdaptiveWidth() && state.AdaptiveElWidth() != CellCount(res) { 37 | t.Errorf("Unepected width: %d; want: %d", CellCount(res), state.AdaptiveElWidth()) 38 | } 39 | } 40 | 41 | func TestElementPercent(t *testing.T) { 42 | testElementBarString(t, testState(100, 50, 0), ElementPercent, "50.00%") 43 | testElementBarString(t, testState(100, 50, 0), ElementPercent, "50 percent", "%v percent") 44 | testElementBarString(t, testState(0, 50, 0), ElementPercent, "?%") 45 | testElementBarString(t, testState(0, 50, 0), ElementPercent, "unkn", "%v%%", "unkn") 46 | } 47 | 48 | func TestElementCounters(t *testing.T) { 49 | testElementBarString(t, testState(100, 50, 0), ElementCounters, "50 / 100") 50 | testElementBarString(t, testState(100, 50, 0), ElementCounters, "50 of 100", "%s of %s") 51 | testElementBarString(t, testState(100, 50, 0, true), ElementCounters, "50 B of 100 B", "%s of %s") 52 | testElementBarString(t, testState(100, 50, 0, true), ElementCounters, "50 B / 100 B") 53 | testElementBarString(t, testState(0, 50, 0, true), ElementCounters, "50 B") 54 | testElementBarString(t, testState(0, 50, 0, true), ElementCounters, "50 B / ?", "", "%[1]s / ?") 55 | } 56 | 57 | func TestElementBar(t *testing.T) { 58 | // short 59 | testElementBarString(t, testState(100, 50, 1, false, true), ElementBar, "[") 60 | testElementBarString(t, testState(100, 50, 2, false, true), ElementBar, "[]") 61 | testElementBarString(t, testState(100, 50, 3, false, true), ElementBar, "[>]") 62 | testElementBarString(t, testState(100, 50, 4, false, true), ElementBar, "[>_]") 63 | testElementBarString(t, testState(100, 50, 5, false, true), ElementBar, "[->_]") 64 | // middle 65 | testElementBarString(t, testState(100, 50, 10, false, true), ElementBar, "[--->____]") 66 | testElementBarString(t, testState(100, 50, 10, false, true), ElementBar, "<--->____>", "<", "", "", "", ">") 67 | // finished 68 | st := testState(100, 100, 10, false, true) 69 | st.finished = true 70 | testElementBarString(t, st, ElementBar, "[--------]") 71 | // empty color 72 | st = testState(100, 50, 10, false, true) 73 | st.Set(Terminal, true) 74 | color.NoColor = false 75 | testElementBarString(t, st, ElementBar, " --->____]", color.RedString("%s", "")) 76 | // empty 77 | testElementBarString(t, testState(0, 50, 10, false, true), ElementBar, "[________]") 78 | // full 79 | testElementBarString(t, testState(20, 20, 10, false, true), ElementBar, "[------->]") 80 | // everflow 81 | testElementBarString(t, testState(20, 50, 10, false, true), ElementBar, "[------->]") 82 | // small width 83 | testElementBarString(t, testState(20, 50, 2, false, true), ElementBar, "[]") 84 | testElementBarString(t, testState(20, 50, 1, false, true), ElementBar, "[") 85 | // negative counters 86 | testElementBarString(t, testState(-50, -150, 10, false, true), ElementBar, "[------->]") 87 | testElementBarString(t, testState(-150, -50, 10, false, true), ElementBar, "[-->_____]") 88 | testElementBarString(t, testState(50, -150, 10, false, true), ElementBar, "[------->]") 89 | testElementBarString(t, testState(-50, 150, 10, false, true), ElementBar, "[------->]") 90 | // long entities / unicode 91 | f1 := []string{"進捗|", "многобайт", "active", "пусто", "|end"} 92 | testElementBarString(t, testState(100, 50, 1, false, true), ElementBar, " ", f1...) 93 | testElementBarString(t, testState(100, 50, 3, false, true), ElementBar, "進 ", f1...) 94 | testElementBarString(t, testState(100, 50, 4, false, true), ElementBar, "進捗", f1...) 95 | testElementBarString(t, testState(100, 50, 29, false, true), ElementBar, "進捗|многactiveпустопусто|end", f1...) 96 | testElementBarString(t, testState(100, 50, 11, false, true), ElementBar, "進捗|aп|end", f1...) 97 | 98 | // unicode 99 | f2 := []string{"⚑", ".", ">", "⟞", "⚐"} 100 | testElementBarString(t, testState(100, 50, 8, false, true), ElementBar, "⚑..>⟞⟞⟞⚐", f2...) 101 | 102 | // no adaptive 103 | testElementBarString(t, testState(0, 50, 10), ElementBar, "[____________________________]") 104 | 105 | var formats = [][]string{ 106 | []string{}, 107 | f1, f2, 108 | } 109 | 110 | // all widths / extreme values 111 | // check for panic and correct width 112 | for _, f := range formats { 113 | for tt := int64(-2); tt < 12; tt++ { 114 | for v := int64(-2); v < 12; v++ { 115 | state := testState(tt, v, 0, false, true) 116 | for w := -2; w < 20; w++ { 117 | state.adaptiveElWidth = w 118 | res := ElementBar(state, f...) 119 | var we = w 120 | if we <= 0 { 121 | we = 30 122 | } 123 | if CellCount(res) != we { 124 | t.Errorf("Unexpected len(%d): '%s'", we, res) 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | func TestElementSpeed(t *testing.T) { 133 | var state = testState(1000, 0, 0, false) 134 | state.time = time.Now() 135 | for i := int64(0); i < 10; i++ { 136 | state.id = uint64(i) + 1 137 | state.current += 42 138 | state.time = state.time.Add(time.Second) 139 | state.finished = i == 9 140 | if state.finished { 141 | state.current += 100 142 | } 143 | r := ElementSpeed(state) 144 | r2 := ElementSpeed(state) 145 | if r != r2 { 146 | t.Errorf("Must be the same: '%s' vs '%s'", r, r2) 147 | } 148 | if i < 1 { 149 | // do not calc first result 150 | if w := "? p/s"; r != w { 151 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 152 | } 153 | } else if state.finished { 154 | if w := "58 p/s"; r != w { 155 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 156 | } 157 | state.time = state.time.Add(-time.Hour) 158 | r = ElementSpeed(state) 159 | if w := "? p/s"; r != w { 160 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 161 | } 162 | } else { 163 | if w := "42 p/s"; r != w { 164 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 165 | } 166 | } 167 | } 168 | } 169 | 170 | func TestElementRemainingTime(t *testing.T) { 171 | var state = testState(100, 0, 0, false) 172 | state.time = time.Now() 173 | state.startTime = state.time 174 | for i := int64(0); i < 10; i++ { 175 | state.id = uint64(i) + 1 176 | state.time = state.time.Add(time.Second) 177 | state.finished = i == 9 178 | r := ElementRemainingTime(state) 179 | if i < 1 { 180 | // do not calc first two results 181 | if w := "?"; r != w { 182 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 183 | } 184 | } else if state.finished { 185 | // final elapsed time 186 | if w := "10s"; r != w { 187 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 188 | } 189 | } else { 190 | w := fmt.Sprintf("%ds", 10-i) 191 | if r != w { 192 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 193 | } 194 | } 195 | state.current += 10 196 | } 197 | } 198 | 199 | func TestElementElapsedTime(t *testing.T) { 200 | t.Run("default behavior", func(t *testing.T) { 201 | var state = testState(1000, 0, 0, false) 202 | state.startTime = time.Now() 203 | state.time = state.startTime 204 | for i := int64(0); i <= 12; i++ { 205 | r := ElementElapsedTime(state) 206 | w := fmt.Sprintf("%d.0s", i) 207 | if i == 0 || i >= 10 { 208 | w = fmt.Sprintf("%ds", i) 209 | } 210 | if r != w { 211 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 212 | } 213 | state.time = state.time.Add(time.Second) 214 | } 215 | }) 216 | t.Run("with round set", func(t *testing.T) { 217 | var state = testState(1000, 0, 0, false) 218 | state.Set(TimeRound, time.Second) 219 | state.startTime = time.Now() 220 | state.time = state.startTime 221 | for i := int64(0); i <= 10; i++ { 222 | r := ElementElapsedTime(state) 223 | w := fmt.Sprintf("%ds", i) 224 | if r != w { 225 | t.Errorf("Unexpected result[%d]: '%s' vs '%s'", i, r, w) 226 | } 227 | state.time = state.time.Add(time.Second) 228 | } 229 | }) 230 | } 231 | 232 | func TestElementString(t *testing.T) { 233 | var state = testState(0, 0, 0, false) 234 | testElementBarString(t, state, ElementString, "", "myKey") 235 | state.Set("myKey", "my value") 236 | testElementBarString(t, state, ElementString, "my value", "myKey") 237 | state.Set("myKey", "my value1") 238 | testElementBarString(t, state, ElementString, "my value1", "myKey") 239 | testElementBarString(t, state, ElementString, "") 240 | } 241 | 242 | func TestElementCycle(t *testing.T) { 243 | var state = testState(0, 0, 0, false) 244 | testElementBarString(t, state, ElementCycle, "") 245 | testElementBarString(t, state, ElementCycle, "1", "1", "2", "3") 246 | testElementBarString(t, state, ElementCycle, "2", "1", "2", "3") 247 | testElementBarString(t, state, ElementCycle, "3", "1", "2", "3") 248 | testElementBarString(t, state, ElementCycle, "1", "1", "2", "3") 249 | testElementBarString(t, state, ElementCycle, "2", "1", "2") 250 | testElementBarString(t, state, ElementCycle, "1", "1", "2") 251 | } 252 | 253 | func TestAdaptiveWrap(t *testing.T) { 254 | var state = testState(0, 0, 0, false) 255 | state.id = 1 256 | state.Set("myKey", "my value") 257 | el := adaptiveWrap(ElementString) 258 | testElementBarString(t, state, el, adElPlaceholder, "myKey") 259 | if v := state.recalc[0].ProgressElement(state); v != "my value" { 260 | t.Errorf("Unexpected result: %s", v) 261 | } 262 | state.id = 2 263 | testElementBarString(t, state, el, adElPlaceholder, "myKey1") 264 | state.Set("myKey", "my value1") 265 | if v := state.recalc[0].ProgressElement(state); v != "my value1" { 266 | t.Errorf("Unexpected result: %s", v) 267 | } 268 | } 269 | 270 | func TestRegisterElement(t *testing.T) { 271 | var testEl ElementFunc = func(state *State, args ...string) string { 272 | return strings.Repeat("*", state.AdaptiveElWidth()) 273 | } 274 | RegisterElement("testEl", testEl, true) 275 | result := ProgressBarTemplate(`{{testEl . }}`).New(0).SetWidth(5).String() 276 | if result != "*****" { 277 | t.Errorf("Unexpected result: '%v'", result) 278 | } 279 | } 280 | 281 | func BenchmarkBar(b *testing.B) { 282 | var formats = map[string][]string{ 283 | "simple": []string{".", ".", ".", ".", "."}, 284 | "unicode": []string{"⚑", "⚒", "⚟", "⟞", "⚐"}, 285 | "color": []string{color.RedString("%s", "."), color.RedString("%s", "."), color.RedString("%s", "."), color.RedString("%s", "."), color.RedString("%s", ".")}, 286 | "long": []string{"..", "..", "..", "..", ".."}, 287 | "longunicode": []string{"⚑⚑", "⚒⚒", "⚟⚟", "⟞⟞", "⚐⚐"}, 288 | } 289 | for name, args := range formats { 290 | state := testState(100, 50, 100, false, true) 291 | b.Run(name, func(b *testing.B) { 292 | b.ReportAllocs() 293 | for i := 0; i < b.N; i++ { 294 | ElementBar(state, args...) 295 | } 296 | }) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /v3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cheggaaa/pb/v3 2 | 3 | require ( 4 | github.com/VividCortex/ewma v1.2.0 5 | github.com/fatih/color v1.18.0 6 | github.com/mattn/go-colorable v0.1.14 7 | github.com/mattn/go-isatty v0.0.20 8 | github.com/mattn/go-runewidth v0.0.16 9 | golang.org/x/sys v0.30.0 10 | ) 11 | 12 | require github.com/rivo/uniseg v0.4.7 // indirect 13 | 14 | go 1.18 15 | -------------------------------------------------------------------------------- /v3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 2 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 3 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 4 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 5 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 6 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 7 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 8 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 9 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 10 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 11 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 12 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 13 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 14 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 16 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | -------------------------------------------------------------------------------- /v3/io.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Reader it's a wrapper for given reader, but with progress handle 8 | type Reader struct { 9 | io.Reader 10 | bar *ProgressBar 11 | } 12 | 13 | // Read reads bytes from wrapped reader and add amount of bytes to progress bar 14 | func (r *Reader) Read(p []byte) (n int, err error) { 15 | n, err = r.Reader.Read(p) 16 | r.bar.Add(n) 17 | return 18 | } 19 | 20 | // Close the wrapped reader when it implements io.Closer 21 | func (r *Reader) Close() (err error) { 22 | r.bar.Finish() 23 | if closer, ok := r.Reader.(io.Closer); ok { 24 | return closer.Close() 25 | } 26 | return 27 | } 28 | 29 | // Writer it's a wrapper for given writer, but with progress handle 30 | type Writer struct { 31 | io.Writer 32 | bar *ProgressBar 33 | } 34 | 35 | // Write writes bytes to wrapped writer and add amount of bytes to progress bar 36 | func (r *Writer) Write(p []byte) (n int, err error) { 37 | n, err = r.Writer.Write(p) 38 | r.bar.Add(n) 39 | return 40 | } 41 | 42 | // Close the wrapped reader when it implements io.Closer 43 | func (r *Writer) Close() (err error) { 44 | r.bar.Finish() 45 | if closer, ok := r.Writer.(io.Closer); ok { 46 | return closer.Close() 47 | } 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /v3/io_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPBProxyReader(t *testing.T) { 8 | bar := new(ProgressBar) 9 | if bar.GetBool(Bytes) { 10 | t.Errorf("By default bytes must be false") 11 | } 12 | 13 | testReader := new(testReaderWriterCloser) 14 | proxyReader := bar.NewProxyReader(testReader) 15 | 16 | if !bar.GetBool(Bytes) { 17 | t.Errorf("Bytes must be true after call NewProxyReader") 18 | } 19 | 20 | for i := 0; i < 10; i++ { 21 | buf := make([]byte, 10) 22 | n, e := proxyReader.Read(buf) 23 | if e != nil { 24 | t.Errorf("Proxy reader return err: %v", e) 25 | } 26 | if n != len(buf) { 27 | t.Errorf("Proxy reader return unexpected N: %d (wand %d)", n, len(buf)) 28 | } 29 | for _, b := range buf { 30 | if b != 'f' { 31 | t.Errorf("Unexpected read value: %v (want %v)", b, 'f') 32 | } 33 | } 34 | if want := int64((i + 1) * len(buf)); bar.Current() != want { 35 | t.Errorf("Unexpected bar current value: %d (want %d)", bar.Current(), want) 36 | } 37 | } 38 | proxyReader.Close() 39 | if !testReader.closed { 40 | t.Errorf("Reader must be closed after call ProxyReader.Close") 41 | } 42 | proxyReader.Reader = nil 43 | proxyReader.Close() 44 | } 45 | 46 | func TestPBProxyWriter(t *testing.T) { 47 | bar := new(ProgressBar) 48 | if bar.GetBool(Bytes) { 49 | t.Errorf("By default bytes must be false") 50 | } 51 | 52 | testWriter := new(testReaderWriterCloser) 53 | proxyReader := bar.NewProxyWriter(testWriter) 54 | 55 | if !bar.GetBool(Bytes) { 56 | t.Errorf("Bytes must be true after call NewProxyReader") 57 | } 58 | 59 | for i := 0; i < 10; i++ { 60 | buf := make([]byte, 10) 61 | n, e := proxyReader.Write(buf) 62 | if e != nil { 63 | t.Errorf("Proxy reader return err: %v", e) 64 | } 65 | if n != len(buf) { 66 | t.Errorf("Proxy reader return unexpected N: %d (wand %d)", n, len(buf)) 67 | } 68 | if want := int64((i + 1) * len(buf)); bar.Current() != want { 69 | t.Errorf("Unexpected bar current value: %d (want %d)", bar.Current(), want) 70 | } 71 | } 72 | proxyReader.Close() 73 | if !testWriter.closed { 74 | t.Errorf("Reader must be closed after call ProxyReader.Close") 75 | } 76 | proxyReader.Writer = nil 77 | proxyReader.Close() 78 | } 79 | 80 | type testReaderWriterCloser struct { 81 | closed bool 82 | data []byte 83 | } 84 | 85 | func (tr *testReaderWriterCloser) Read(p []byte) (n int, err error) { 86 | for i := range p { 87 | p[i] = 'f' 88 | } 89 | return len(p), nil 90 | } 91 | 92 | func (tr *testReaderWriterCloser) Write(p []byte) (n int, err error) { 93 | tr.data = append(tr.data, p...) 94 | return len(p), nil 95 | } 96 | 97 | func (tr *testReaderWriterCloser) Close() (err error) { 98 | tr.closed = true 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /v3/pb.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "text/template" 13 | "time" 14 | 15 | "github.com/fatih/color" 16 | 17 | "github.com/mattn/go-colorable" 18 | "github.com/mattn/go-isatty" 19 | 20 | "github.com/cheggaaa/pb/v3/termutil" 21 | ) 22 | 23 | // Version of ProgressBar library 24 | const Version = "3.0.8" 25 | 26 | type key int 27 | 28 | const ( 29 | // Bytes means we're working with byte sizes. Numbers will print as Kb, Mb, etc 30 | // bar.Set(pb.Bytes, true) 31 | Bytes key = 1 << iota 32 | 33 | // Use SI bytes prefix names (kB, MB, etc) instead of IEC prefix names (KiB, MiB, etc) 34 | SIBytesPrefix 35 | 36 | // Terminal means we're will print to terminal and can use ascii sequences 37 | // Also we're will try to use terminal width 38 | Terminal 39 | 40 | // Static means progress bar will not update automaticly 41 | Static 42 | 43 | // ReturnSymbol - by default in terminal mode it's '\r' 44 | ReturnSymbol 45 | 46 | // Color by default is true when output is tty, but you can set to false for disabling colors 47 | Color 48 | 49 | // Hide the progress bar when finished, rather than leaving it up. By default it's false. 50 | CleanOnFinish 51 | 52 | // Round elapsed time to this precision. Defaults to time.Second. 53 | TimeRound 54 | ) 55 | 56 | const ( 57 | defaultBarWidth = 100 58 | defaultRefreshRate = time.Millisecond * 200 59 | ) 60 | 61 | // New creates new ProgressBar object 62 | func New(total int) *ProgressBar { 63 | return New64(int64(total)) 64 | } 65 | 66 | // New64 creates new ProgressBar object using int64 as total 67 | func New64(total int64) *ProgressBar { 68 | pb := new(ProgressBar) 69 | return pb.SetTotal(total) 70 | } 71 | 72 | // StartNew starts new ProgressBar with Default template 73 | func StartNew(total int) *ProgressBar { 74 | return New(total).Start() 75 | } 76 | 77 | // Start64 starts new ProgressBar with Default template. Using int64 as total. 78 | func Start64(total int64) *ProgressBar { 79 | return New64(total).Start() 80 | } 81 | 82 | var ( 83 | terminalWidth = termutil.TerminalWidth 84 | isTerminal = isatty.IsTerminal 85 | isCygwinTerminal = isatty.IsCygwinTerminal 86 | ) 87 | 88 | // ProgressBar is the main object of bar 89 | type ProgressBar struct { 90 | current, total int64 91 | width int 92 | maxWidth int 93 | mu sync.RWMutex 94 | rm sync.Mutex 95 | vars map[interface{}]interface{} 96 | elements map[string]Element 97 | output io.Writer 98 | coutput io.Writer 99 | nocoutput io.Writer 100 | startTime time.Time 101 | refreshRate time.Duration 102 | tmpl *template.Template 103 | state *State 104 | buf *bytes.Buffer 105 | ticker *time.Ticker 106 | finish chan struct{} 107 | finished bool 108 | configured bool 109 | err error 110 | } 111 | 112 | func (pb *ProgressBar) configure() { 113 | if pb.configured { 114 | return 115 | } 116 | pb.configured = true 117 | 118 | if pb.vars == nil { 119 | pb.vars = make(map[interface{}]interface{}) 120 | } 121 | if pb.output == nil { 122 | pb.output = os.Stderr 123 | } 124 | 125 | if pb.tmpl == nil { 126 | pb.tmpl, pb.err = getTemplate(string(Default)) 127 | if pb.err != nil { 128 | return 129 | } 130 | } 131 | if pb.vars[Terminal] == nil { 132 | if f, ok := pb.output.(*os.File); ok { 133 | if isTerminal(f.Fd()) || isCygwinTerminal(f.Fd()) { 134 | pb.vars[Terminal] = true 135 | } 136 | } 137 | } 138 | if pb.vars[ReturnSymbol] == nil { 139 | if tm, ok := pb.vars[Terminal].(bool); ok && tm { 140 | pb.vars[ReturnSymbol] = "\r" 141 | } 142 | } 143 | if pb.vars[Color] == nil { 144 | if tm, ok := pb.vars[Terminal].(bool); ok && tm { 145 | pb.vars[Color] = true 146 | } 147 | } 148 | if pb.refreshRate == 0 { 149 | pb.refreshRate = defaultRefreshRate 150 | } 151 | if pb.vars[CleanOnFinish] == nil { 152 | pb.vars[CleanOnFinish] = false 153 | } 154 | if f, ok := pb.output.(*os.File); ok { 155 | pb.coutput = colorable.NewColorable(f) 156 | } else { 157 | pb.coutput = pb.output 158 | } 159 | pb.nocoutput = colorable.NewNonColorable(pb.output) 160 | } 161 | 162 | // Start starts the bar 163 | func (pb *ProgressBar) Start() *ProgressBar { 164 | pb.mu.Lock() 165 | defer pb.mu.Unlock() 166 | if pb.finish != nil { 167 | return pb 168 | } 169 | pb.configure() 170 | pb.finished = false 171 | pb.state = nil 172 | pb.startTime = time.Now() 173 | if st, ok := pb.vars[Static].(bool); ok && st { 174 | return pb 175 | } 176 | pb.finish = make(chan struct{}) 177 | pb.ticker = time.NewTicker(pb.refreshRate) 178 | go pb.writer(pb.finish) 179 | return pb 180 | } 181 | 182 | func (pb *ProgressBar) writer(finish chan struct{}) { 183 | for { 184 | select { 185 | case <-pb.ticker.C: 186 | pb.write(false) 187 | case <-finish: 188 | pb.ticker.Stop() 189 | pb.write(true) 190 | finish <- struct{}{} 191 | return 192 | } 193 | } 194 | } 195 | 196 | // Write performs write to the output 197 | func (pb *ProgressBar) Write() *ProgressBar { 198 | pb.mu.RLock() 199 | finished := pb.finished 200 | pb.mu.RUnlock() 201 | pb.write(finished) 202 | return pb 203 | } 204 | 205 | func (pb *ProgressBar) write(finish bool) { 206 | result, width := pb.render() 207 | if pb.Err() != nil { 208 | return 209 | } 210 | if pb.GetBool(Terminal) { 211 | if r := (width - CellCount(result)); r > 0 { 212 | result += strings.Repeat(" ", r) 213 | } 214 | } 215 | if ret, ok := pb.Get(ReturnSymbol).(string); ok { 216 | result = ret + result 217 | if finish && ret == "\r" { 218 | if pb.GetBool(CleanOnFinish) { 219 | // "Wipe out" progress bar by overwriting one line with blanks 220 | result = "\r" + color.New(color.Reset).Sprint(strings.Repeat(" ", width)) + "\r" 221 | } else { 222 | result += "\n" 223 | } 224 | } 225 | } 226 | if pb.GetBool(Color) { 227 | pb.coutput.Write([]byte(result)) 228 | } else { 229 | pb.nocoutput.Write([]byte(result)) 230 | } 231 | } 232 | 233 | // Total return current total bar value 234 | func (pb *ProgressBar) Total() int64 { 235 | return atomic.LoadInt64(&pb.total) 236 | } 237 | 238 | // SetTotal sets the total bar value 239 | func (pb *ProgressBar) SetTotal(value int64) *ProgressBar { 240 | atomic.StoreInt64(&pb.total, value) 241 | return pb 242 | } 243 | 244 | // AddTotal adds to the total bar value 245 | func (pb *ProgressBar) AddTotal(value int64) *ProgressBar { 246 | atomic.AddInt64(&pb.total, value) 247 | return pb 248 | } 249 | 250 | // SetCurrent sets the current bar value 251 | func (pb *ProgressBar) SetCurrent(value int64) *ProgressBar { 252 | atomic.StoreInt64(&pb.current, value) 253 | return pb 254 | } 255 | 256 | // Current return current bar value 257 | func (pb *ProgressBar) Current() int64 { 258 | return atomic.LoadInt64(&pb.current) 259 | } 260 | 261 | // Add adding given int64 value to bar value 262 | func (pb *ProgressBar) Add64(value int64) *ProgressBar { 263 | atomic.AddInt64(&pb.current, value) 264 | return pb 265 | } 266 | 267 | // Add adding given int value to bar value 268 | func (pb *ProgressBar) Add(value int) *ProgressBar { 269 | return pb.Add64(int64(value)) 270 | } 271 | 272 | // Increment atomically increments the progress 273 | func (pb *ProgressBar) Increment() *ProgressBar { 274 | return pb.Add64(1) 275 | } 276 | 277 | // Set sets any value by any key 278 | func (pb *ProgressBar) Set(key, value interface{}) *ProgressBar { 279 | pb.mu.Lock() 280 | defer pb.mu.Unlock() 281 | if pb.vars == nil { 282 | pb.vars = make(map[interface{}]interface{}) 283 | } 284 | pb.vars[key] = value 285 | return pb 286 | } 287 | 288 | // Get return value by key 289 | func (pb *ProgressBar) Get(key interface{}) interface{} { 290 | pb.mu.RLock() 291 | defer pb.mu.RUnlock() 292 | if pb.vars == nil { 293 | return nil 294 | } 295 | return pb.vars[key] 296 | } 297 | 298 | // GetBool return value by key and try to convert there to boolean 299 | // If value doesn't set or not boolean - return false 300 | func (pb *ProgressBar) GetBool(key interface{}) bool { 301 | if v, ok := pb.Get(key).(bool); ok { 302 | return v 303 | } 304 | return false 305 | } 306 | 307 | // SetWidth sets the bar width 308 | // When given value <= 0 would be using the terminal width (if possible) or default value. 309 | func (pb *ProgressBar) SetWidth(width int) *ProgressBar { 310 | pb.mu.Lock() 311 | pb.width = width 312 | pb.mu.Unlock() 313 | return pb 314 | } 315 | 316 | // SetMaxWidth sets the bar maximum width 317 | // When given value <= 0 would be using the terminal width (if possible) or default value. 318 | func (pb *ProgressBar) SetMaxWidth(maxWidth int) *ProgressBar { 319 | pb.mu.Lock() 320 | pb.maxWidth = maxWidth 321 | pb.mu.Unlock() 322 | return pb 323 | } 324 | 325 | // Width return the bar width 326 | // It's current terminal width or settled over 'SetWidth' value. 327 | func (pb *ProgressBar) Width() (width int) { 328 | defer func() { 329 | if r := recover(); r != nil { 330 | width = defaultBarWidth 331 | } 332 | }() 333 | pb.mu.RLock() 334 | width = pb.width 335 | maxWidth := pb.maxWidth 336 | pb.mu.RUnlock() 337 | if width <= 0 { 338 | var err error 339 | if width, err = terminalWidth(); err != nil { 340 | return defaultBarWidth 341 | } 342 | } 343 | if maxWidth > 0 && width > maxWidth { 344 | width = maxWidth 345 | } 346 | return 347 | } 348 | 349 | func (pb *ProgressBar) SetRefreshRate(dur time.Duration) *ProgressBar { 350 | pb.mu.Lock() 351 | if dur > 0 { 352 | pb.refreshRate = dur 353 | } 354 | pb.mu.Unlock() 355 | return pb 356 | } 357 | 358 | // SetWriter sets the io.Writer. Bar will write in this writer 359 | // By default this is os.Stderr 360 | func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar { 361 | pb.mu.Lock() 362 | pb.output = w 363 | pb.configured = false 364 | pb.configure() 365 | pb.mu.Unlock() 366 | return pb 367 | } 368 | 369 | // StartTime return the time when bar started 370 | func (pb *ProgressBar) StartTime() time.Time { 371 | pb.mu.RLock() 372 | defer pb.mu.RUnlock() 373 | return pb.startTime 374 | } 375 | 376 | // Format convert int64 to string according to the current settings 377 | func (pb *ProgressBar) Format(v int64) string { 378 | if pb.GetBool(Bytes) { 379 | return formatBytes(v, pb.GetBool(SIBytesPrefix)) 380 | } 381 | return strconv.FormatInt(v, 10) 382 | } 383 | 384 | // Finish stops the bar 385 | func (pb *ProgressBar) Finish() *ProgressBar { 386 | pb.mu.Lock() 387 | if pb.finished { 388 | pb.mu.Unlock() 389 | return pb 390 | } 391 | finishChan := pb.finish 392 | pb.finished = true 393 | pb.mu.Unlock() 394 | if finishChan != nil { 395 | finishChan <- struct{}{} 396 | <-finishChan 397 | pb.mu.Lock() 398 | pb.finish = nil 399 | pb.mu.Unlock() 400 | } 401 | return pb 402 | } 403 | 404 | // IsStarted indicates progress bar state 405 | func (pb *ProgressBar) IsStarted() bool { 406 | pb.mu.RLock() 407 | defer pb.mu.RUnlock() 408 | return pb.finish != nil 409 | } 410 | 411 | // IsFinished indicates progress bar is finished 412 | func (pb *ProgressBar) IsFinished() bool { 413 | pb.mu.RLock() 414 | defer pb.mu.RUnlock() 415 | return pb.finished 416 | } 417 | 418 | // SetTemplateString sets ProgressBar tempate string and parse it 419 | func (pb *ProgressBar) SetTemplateString(tmpl string) *ProgressBar { 420 | pb.mu.Lock() 421 | defer pb.mu.Unlock() 422 | pb.tmpl, pb.err = getTemplate(tmpl) 423 | return pb 424 | } 425 | 426 | // SetTemplateString sets ProgressBarTempate and parse it 427 | func (pb *ProgressBar) SetTemplate(tmpl ProgressBarTemplate) *ProgressBar { 428 | return pb.SetTemplateString(string(tmpl)) 429 | } 430 | 431 | // NewProxyReader creates a wrapper for given reader, but with progress handle 432 | // Takes io.Reader or io.ReadCloser 433 | // Also, it automatically switches progress bar to handle units as bytes 434 | func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader { 435 | pb.Set(Bytes, true) 436 | return &Reader{r, pb} 437 | } 438 | 439 | // NewProxyWriter creates a wrapper for given writer, but with progress handle 440 | // Takes io.Writer or io.WriteCloser 441 | // Also, it automatically switches progress bar to handle units as bytes 442 | func (pb *ProgressBar) NewProxyWriter(r io.Writer) *Writer { 443 | pb.Set(Bytes, true) 444 | return &Writer{r, pb} 445 | } 446 | 447 | func (pb *ProgressBar) render() (result string, width int) { 448 | defer func() { 449 | if r := recover(); r != nil { 450 | pb.SetErr(fmt.Errorf("render panic: %v", r)) 451 | } 452 | }() 453 | pb.rm.Lock() 454 | defer pb.rm.Unlock() 455 | pb.mu.Lock() 456 | pb.configure() 457 | if pb.state == nil { 458 | pb.state = &State{ProgressBar: pb} 459 | pb.buf = bytes.NewBuffer(nil) 460 | } 461 | if pb.startTime.IsZero() { 462 | pb.startTime = time.Now() 463 | } 464 | pb.state.id++ 465 | pb.state.finished = pb.finished 466 | pb.state.time = time.Now() 467 | pb.mu.Unlock() 468 | 469 | pb.state.width = pb.Width() 470 | width = pb.state.width 471 | pb.state.total = pb.Total() 472 | pb.state.current = pb.Current() 473 | pb.buf.Reset() 474 | 475 | if e := pb.tmpl.Execute(pb.buf, pb.state); e != nil { 476 | pb.SetErr(e) 477 | return "", 0 478 | } 479 | 480 | result = pb.buf.String() 481 | 482 | aec := len(pb.state.recalc) 483 | if aec == 0 { 484 | // no adaptive elements 485 | return 486 | } 487 | 488 | staticWidth := CellCount(result) - (aec * adElPlaceholderLen) 489 | 490 | if pb.state.Width()-staticWidth <= 0 { 491 | result = strings.Replace(result, adElPlaceholder, "", -1) 492 | result = StripString(result, pb.state.Width()) 493 | } else { 494 | pb.state.adaptiveElWidth = (width - staticWidth) / aec 495 | for _, el := range pb.state.recalc { 496 | result = strings.Replace(result, adElPlaceholder, el.ProgressElement(pb.state), 1) 497 | } 498 | } 499 | pb.state.recalc = pb.state.recalc[:0] 500 | return 501 | } 502 | 503 | // SetErr sets error to the ProgressBar 504 | // Error will be available over Err() 505 | func (pb *ProgressBar) SetErr(err error) *ProgressBar { 506 | pb.mu.Lock() 507 | pb.err = err 508 | pb.mu.Unlock() 509 | return pb 510 | } 511 | 512 | // Err return possible error 513 | // When all ok - will be nil 514 | // May contain template.Execute errors 515 | func (pb *ProgressBar) Err() error { 516 | pb.mu.RLock() 517 | defer pb.mu.RUnlock() 518 | return pb.err 519 | } 520 | 521 | // String return currrent string representation of ProgressBar 522 | func (pb *ProgressBar) String() string { 523 | res, _ := pb.render() 524 | return res 525 | } 526 | 527 | // ProgressElement implements Element interface 528 | func (pb *ProgressBar) ProgressElement(s *State, args ...string) string { 529 | if s.IsAdaptiveWidth() { 530 | pb.SetWidth(s.AdaptiveElWidth()) 531 | } 532 | return pb.String() 533 | } 534 | 535 | // State represents the current state of bar 536 | // Need for bar elements 537 | type State struct { 538 | *ProgressBar 539 | 540 | id uint64 541 | total, current int64 542 | width, adaptiveElWidth int 543 | finished, adaptive bool 544 | time time.Time 545 | 546 | recalc []Element 547 | } 548 | 549 | // Id it's the current state identifier 550 | // - incremental 551 | // - starts with 1 552 | // - resets after finish/start 553 | func (s *State) Id() uint64 { 554 | return s.id 555 | } 556 | 557 | // Total it's bar int64 total 558 | func (s *State) Total() int64 { 559 | return s.total 560 | } 561 | 562 | // Value it's current value 563 | func (s *State) Value() int64 { 564 | return s.current 565 | } 566 | 567 | // Width of bar 568 | func (s *State) Width() int { 569 | return s.width 570 | } 571 | 572 | // AdaptiveElWidth - adaptive elements must return string with given cell count (when AdaptiveElWidth > 0) 573 | func (s *State) AdaptiveElWidth() int { 574 | return s.adaptiveElWidth 575 | } 576 | 577 | // IsAdaptiveWidth returns true when element must be shown as adaptive 578 | func (s *State) IsAdaptiveWidth() bool { 579 | return s.adaptive 580 | } 581 | 582 | // IsFinished return true when bar is finished 583 | func (s *State) IsFinished() bool { 584 | return s.finished 585 | } 586 | 587 | // IsFirst return true only in first render 588 | func (s *State) IsFirst() bool { 589 | return s.id == 1 590 | } 591 | 592 | // Time when state was created 593 | func (s *State) Time() time.Time { 594 | return s.time 595 | } 596 | -------------------------------------------------------------------------------- /v3/pb_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | func TestPBBasic(t *testing.T) { 15 | bar := new(ProgressBar) 16 | var a, e int64 17 | if a, e = bar.Total(), 0; a != e { 18 | t.Errorf("Unexpected total: actual: %v; expected: %v", a, e) 19 | } 20 | if a, e = bar.Current(), 0; a != e { 21 | t.Errorf("Unexpected current: actual: %v; expected: %v", a, e) 22 | } 23 | bar.SetCurrent(10).SetTotal(20) 24 | if a, e = bar.Total(), 20; a != e { 25 | t.Errorf("Unexpected total: actual: %v; expected: %v", a, e) 26 | } 27 | if a, e = bar.Current(), 10; a != e { 28 | t.Errorf("Unexpected current: actual: %v; expected: %v", a, e) 29 | } 30 | bar.Add(5) 31 | if a, e = bar.Current(), 15; a != e { 32 | t.Errorf("Unexpected current: actual: %v; expected: %v", a, e) 33 | } 34 | bar.Increment() 35 | if a, e = bar.Current(), 16; a != e { 36 | t.Errorf("Unexpected current: actual: %v; expected: %v", a, e) 37 | } 38 | } 39 | 40 | func TestPBWidth(t *testing.T) { 41 | terminalWidth = func() (int, error) { 42 | return 50, nil 43 | } 44 | // terminal width 45 | bar := new(ProgressBar) 46 | if a, e := bar.Width(), 50; a != e { 47 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 48 | } 49 | // terminal width error 50 | terminalWidth = func() (int, error) { 51 | return 0, errors.New("test error") 52 | } 53 | if a, e := bar.Width(), defaultBarWidth; a != e { 54 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 55 | } 56 | // terminal width panic 57 | terminalWidth = func() (int, error) { 58 | panic("test") 59 | return 0, nil 60 | } 61 | if a, e := bar.Width(), defaultBarWidth; a != e { 62 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 63 | } 64 | // set negative terminal width 65 | bar.SetWidth(-42) 66 | if a, e := bar.Width(), defaultBarWidth; a != e { 67 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 68 | } 69 | // set terminal width 70 | bar.SetWidth(42) 71 | if a, e := bar.Width(), 42; a != e { 72 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 73 | } 74 | } 75 | 76 | func TestPBMaxWidth(t *testing.T) { 77 | terminalWidth = func() (int, error) { 78 | return 50, nil 79 | } 80 | // terminal width 81 | bar := new(ProgressBar) 82 | if a, e := bar.Width(), 50; a != e { 83 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 84 | } 85 | 86 | bar.SetMaxWidth(55) 87 | if a, e := bar.Width(), 50; a != e { 88 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 89 | } 90 | 91 | bar.SetMaxWidth(38) 92 | if a, e := bar.Width(), 38; a != e { 93 | t.Errorf("Unexpected width: actual: %v; expected: %v", a, e) 94 | } 95 | } 96 | 97 | func TestAddTotal(t *testing.T) { 98 | bar := new(ProgressBar) 99 | bar.SetTotal(0) 100 | bar.AddTotal(50) 101 | got := bar.Total() 102 | if got != 50 { 103 | t.Errorf("bar.Total() = %v, want %v", got, 50) 104 | } 105 | bar.AddTotal(-10) 106 | got = bar.Total() 107 | if got != 40 { 108 | t.Errorf("bar.Total() = %v, want %v", got, 40) 109 | } 110 | } 111 | 112 | func TestPBTemplate(t *testing.T) { 113 | bar := new(ProgressBar) 114 | result := bar.SetTotal(100).SetCurrent(50).SetWidth(40).String() 115 | expected := "50 / 100 [------->________] 50.00% ? p/s" 116 | if result != expected { 117 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 118 | } 119 | 120 | // check strip 121 | result = bar.SetWidth(8).String() 122 | expected = "50 / 100" 123 | if result != expected { 124 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 125 | } 126 | 127 | // invalid template 128 | for _, invalidTemplate := range []string{ 129 | `{{invalid template`, `{{speed}}`, 130 | } { 131 | bar.SetTemplateString(invalidTemplate) 132 | result = bar.String() 133 | expected = "" 134 | if result != expected { 135 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 136 | } 137 | if err := bar.Err(); err == nil { 138 | t.Errorf("Must be error") 139 | } 140 | } 141 | 142 | // simple template without adaptive elemnts 143 | bar.SetTemplateString(`{{counters . }}`) 144 | result = bar.String() 145 | expected = "50 / 100" 146 | if result != expected { 147 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 148 | } 149 | } 150 | 151 | func TestPBStartFinish(t *testing.T) { 152 | bar := ProgressBarTemplate(`{{counters . }}`).New(0) 153 | for i := int64(0); i < 2; i++ { 154 | if bar.IsStarted() { 155 | t.Error("Must be false") 156 | } 157 | var buf = bytes.NewBuffer(nil) 158 | bar.SetTotal(100). 159 | SetCurrent(int64(i)). 160 | SetWidth(7). 161 | Set(Terminal, true). 162 | SetWriter(buf). 163 | SetRefreshRate(time.Millisecond * 20). 164 | Start() 165 | if !bar.IsStarted() { 166 | t.Error("Must be true") 167 | } 168 | time.Sleep(time.Millisecond * 100) 169 | bar.Finish() 170 | if buf.Len() == 0 { 171 | t.Error("no writes") 172 | } 173 | var resultsString = strings.TrimPrefix(buf.String(), "\r") 174 | if !strings.HasSuffix(resultsString, "\n") { 175 | t.Error("No end \\n symb") 176 | } else { 177 | resultsString = resultsString[:len(resultsString)-1] 178 | } 179 | var results = strings.Split(resultsString, "\r") 180 | if len(results) < 3 { 181 | t.Errorf("Unexpected writes count: %v", len(results)) 182 | } 183 | exp := fmt.Sprintf("%d / 100", i) 184 | for i, res := range results { 185 | if res != exp { 186 | t.Errorf("Unexpected result[%d]: '%v'", i, res) 187 | } 188 | } 189 | // test second finish call 190 | bar.Finish() 191 | } 192 | } 193 | 194 | func TestPBFlags(t *testing.T) { 195 | // Static 196 | color.NoColor = false 197 | buf := bytes.NewBuffer(nil) 198 | bar := ProgressBarTemplate(`{{counters . | red}}`).New(100) 199 | bar.Set(Static, true).SetCurrent(50).SetWidth(10).SetWriter(buf).Start() 200 | if bar.IsStarted() { 201 | t.Error("Must be false") 202 | } 203 | bar.Write() 204 | result := buf.String() 205 | expected := "50 / 100" 206 | if result != expected { 207 | t.Errorf("Unexpected result: (actual/expected)\n'%s'\n'%s'", result, expected) 208 | } 209 | if !bar.state.IsFirst() { 210 | t.Error("must be true") 211 | } 212 | // Color 213 | bar.Set(Color, true) 214 | buf.Reset() 215 | bar.Write() 216 | result = buf.String() 217 | expected = color.RedString("50 / 100") 218 | if result != expected { 219 | t.Errorf("Unexpected result: (actual/expected)\n'%s'\n'%s'", result, expected) 220 | } 221 | if bar.state.IsFirst() { 222 | t.Error("must be false") 223 | } 224 | // Terminal 225 | bar.Set(Terminal, true).SetWriter(buf) 226 | buf.Reset() 227 | bar.Write() 228 | result = buf.String() 229 | expected = "\r" + color.RedString("50 / 100") + " " 230 | if result != expected { 231 | t.Errorf("Unexpected result: (actual/expected)\n'%s'\n'%s'", result, expected) 232 | } 233 | } 234 | 235 | func BenchmarkRender(b *testing.B) { 236 | var formats = []string{ 237 | string(Simple), 238 | string(Default), 239 | string(Full), 240 | `{{string . "prefix" | red}}{{counters . | green}} {{bar . | yellow}} {{percent . | cyan}} {{speed . | cyan}}{{string . "suffix" | cyan}}`, 241 | } 242 | var names = []string{ 243 | "Simple", "Default", "Full", "Color", 244 | } 245 | for i, tmpl := range formats { 246 | bar := new(ProgressBar) 247 | bar.SetTemplateString(tmpl).SetWidth(100) 248 | b.Run(names[i], func(b *testing.B) { 249 | b.ReportAllocs() 250 | for i := 0; i < b.N; i++ { 251 | bar.String() 252 | } 253 | }) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /v3/pool.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly windows plan9 aix 2 | 3 | package pb 4 | 5 | import ( 6 | "io" 7 | "sync" 8 | "time" 9 | 10 | "github.com/cheggaaa/pb/v3/termutil" 11 | ) 12 | 13 | // Create and start new pool with given bars 14 | // You need call pool.Stop() after work 15 | func StartPool(pbs ...*ProgressBar) (pool *Pool, err error) { 16 | pool = new(Pool) 17 | if err = pool.Start(); err != nil { 18 | return 19 | } 20 | pool.Add(pbs...) 21 | return 22 | } 23 | 24 | // NewPool initialises a pool with progress bars, but 25 | // doesn't start it. You need to call Start manually 26 | func NewPool(pbs ...*ProgressBar) (pool *Pool) { 27 | pool = new(Pool) 28 | pool.Add(pbs...) 29 | return 30 | } 31 | 32 | type Pool struct { 33 | Output io.Writer 34 | RefreshRate time.Duration 35 | bars []*ProgressBar 36 | lastBarsCount int 37 | shutdownCh chan struct{} 38 | workerCh chan struct{} 39 | m sync.Mutex 40 | finishOnce sync.Once 41 | } 42 | 43 | // Add progress bars. 44 | func (p *Pool) Add(pbs ...*ProgressBar) { 45 | p.m.Lock() 46 | defer p.m.Unlock() 47 | for _, bar := range pbs { 48 | bar.Set(Static, true) 49 | bar.Start() 50 | p.bars = append(p.bars, bar) 51 | } 52 | } 53 | 54 | func (p *Pool) Start() (err error) { 55 | p.RefreshRate = defaultRefreshRate 56 | p.shutdownCh, err = termutil.RawModeOn() 57 | if err != nil { 58 | return 59 | } 60 | p.workerCh = make(chan struct{}) 61 | go p.writer() 62 | return 63 | } 64 | 65 | func (p *Pool) writer() { 66 | var first = true 67 | defer func() { 68 | if first == false { 69 | p.print(false) 70 | } else { 71 | p.print(true) 72 | p.print(false) 73 | } 74 | close(p.workerCh) 75 | }() 76 | 77 | for { 78 | select { 79 | case <-time.After(p.RefreshRate): 80 | if p.print(first) { 81 | p.print(false) 82 | return 83 | } 84 | first = false 85 | case <-p.shutdownCh: 86 | return 87 | } 88 | } 89 | } 90 | 91 | // Restore terminal state and close pool 92 | func (p *Pool) Stop() error { 93 | p.finishOnce.Do(func() { 94 | if p.shutdownCh != nil { 95 | close(p.shutdownCh) 96 | } 97 | }) 98 | 99 | // Wait for the worker to complete 100 | select { 101 | case <-p.workerCh: 102 | } 103 | 104 | return termutil.RawModeOff() 105 | } 106 | -------------------------------------------------------------------------------- /v3/pool_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package pb 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/cheggaaa/pb/v3/termutil" 12 | ) 13 | 14 | func (p *Pool) print(first bool) bool { 15 | p.m.Lock() 16 | defer p.m.Unlock() 17 | var out string 18 | if !first { 19 | coords, err := termutil.GetCursorPos() 20 | if err != nil { 21 | log.Panic(err) 22 | } 23 | coords.Y -= int16(p.lastBarsCount) 24 | if coords.Y < 0 { 25 | coords.Y = 0 26 | } 27 | coords.X = 0 28 | 29 | err = termutil.SetCursorPos(coords) 30 | if err != nil { 31 | log.Panic(err) 32 | } 33 | } 34 | cols, err := termutil.TerminalWidth() 35 | if err != nil { 36 | cols = defaultBarWidth 37 | } 38 | isFinished := true 39 | for _, bar := range p.bars { 40 | if !bar.IsFinished() { 41 | isFinished = false 42 | } 43 | result := bar.String() 44 | if r := cols - CellCount(result); r > 0 { 45 | result += strings.Repeat(" ", r) 46 | } 47 | out += fmt.Sprintf("\r%s\n", result) 48 | } 49 | if p.Output != nil { 50 | fmt.Fprint(p.Output, out) 51 | } else { 52 | fmt.Print(out) 53 | } 54 | p.lastBarsCount = len(p.bars) 55 | return isFinished 56 | } 57 | -------------------------------------------------------------------------------- /v3/pool_x.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || freebsd || netbsd || openbsd || solaris || dragonfly || plan9 || aix 2 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly plan9 aix 3 | 4 | package pb 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/cheggaaa/pb/v3/termutil" 12 | ) 13 | 14 | func (p *Pool) print(first bool) bool { 15 | p.m.Lock() 16 | defer p.m.Unlock() 17 | var out string 18 | if !first { 19 | out = fmt.Sprintf("\033[%dA", p.lastBarsCount) 20 | } 21 | isFinished := true 22 | bars := p.bars 23 | rows, cols, err := termutil.TerminalSize() 24 | if err != nil { 25 | cols = defaultBarWidth 26 | } 27 | if rows > 0 && len(bars) > rows { 28 | // we need to hide bars that overflow terminal height 29 | bars = bars[len(bars)-rows:] 30 | } 31 | for _, bar := range bars { 32 | if !bar.IsFinished() { 33 | isFinished = false 34 | } 35 | bar.SetWidth(cols) 36 | result := bar.String() 37 | if r := cols - CellCount(result); r > 0 { 38 | result += strings.Repeat(" ", r) 39 | } 40 | out += fmt.Sprintf("\r%s\n", result) 41 | } 42 | if p.Output != nil { 43 | fmt.Fprint(p.Output, out) 44 | } else { 45 | fmt.Fprint(os.Stderr, out) 46 | } 47 | p.lastBarsCount = len(bars) 48 | return isFinished 49 | } 50 | -------------------------------------------------------------------------------- /v3/preset.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | var ( 4 | // Full - preset with all default available elements 5 | // Example: 'Prefix 20/100 [-->______] 20% 1 p/s ETA 1m Suffix' 6 | Full ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }} {{speed . }} {{rtime . "ETA %s"}}{{with string . "suffix"}} {{.}}{{end}}` 7 | 8 | // Default - preset like Full but without elapsed time 9 | // Example: 'Prefix 20/100 [-->______] 20% 1 p/s Suffix' 10 | Default ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }} {{speed . }}{{with string . "suffix"}} {{.}}{{end}}` 11 | 12 | // Simple - preset without speed and any timers. Only counters, bar and percents 13 | // Example: 'Prefix 20/100 [-->______] 20% Suffix' 14 | Simple ProgressBarTemplate = `{{with string . "prefix"}}{{.}} {{end}}{{counters . }} {{bar . }} {{percent . }}{{with string . "suffix"}} {{.}}{{end}}` 15 | ) 16 | -------------------------------------------------------------------------------- /v3/preset_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestPreset(t *testing.T) { 9 | prefix := "Prefix" 10 | suffix := "Suffix" 11 | 12 | for _, preset := range []ProgressBarTemplate{Full, Default, Simple} { 13 | bar := preset.New(100). 14 | SetCurrent(20). 15 | Set("prefix", prefix). 16 | Set("suffix", suffix). 17 | SetWidth(50) 18 | 19 | // initialize the internal state 20 | _, _ = bar.render() 21 | s := bar.String() 22 | if !strings.HasPrefix(s, prefix+" ") { 23 | t.Error("prefix not found:", s) 24 | } 25 | if !strings.HasSuffix(s, " "+suffix) { 26 | t.Error("suffix not found:", s) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /v3/speed.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/VividCortex/ewma" 9 | ) 10 | 11 | var speedAddLimit = time.Second / 2 12 | 13 | type speed struct { 14 | ewma ewma.MovingAverage 15 | lastStateId uint64 16 | prevValue, startValue int64 17 | prevTime, startTime time.Time 18 | } 19 | 20 | func (s *speed) value(state *State) float64 { 21 | if s.ewma == nil { 22 | s.ewma = ewma.NewMovingAverage() 23 | } 24 | if state.IsFirst() || state.Id() < s.lastStateId { 25 | s.reset(state) 26 | return 0 27 | } 28 | if state.Id() == s.lastStateId { 29 | return s.ewma.Value() 30 | } 31 | if state.IsFinished() { 32 | return s.absValue(state) 33 | } 34 | dur := state.Time().Sub(s.prevTime) 35 | if dur < speedAddLimit { 36 | return s.ewma.Value() 37 | } 38 | diff := math.Abs(float64(state.Value() - s.prevValue)) 39 | lastSpeed := diff / dur.Seconds() 40 | s.prevTime = state.Time() 41 | s.prevValue = state.Value() 42 | s.lastStateId = state.Id() 43 | s.ewma.Add(lastSpeed) 44 | return s.ewma.Value() 45 | } 46 | 47 | func (s *speed) reset(state *State) { 48 | s.lastStateId = state.Id() 49 | s.startTime = state.Time() 50 | s.prevTime = state.Time() 51 | s.startValue = state.Value() 52 | s.prevValue = state.Value() 53 | s.ewma = ewma.NewMovingAverage() 54 | } 55 | 56 | func (s *speed) absValue(state *State) float64 { 57 | if dur := state.Time().Sub(s.startTime); dur > 0 { 58 | return float64(state.Value()) / dur.Seconds() 59 | } 60 | return 0 61 | } 62 | 63 | func getSpeedObj(state *State) (s *speed) { 64 | if sObj, ok := state.Get(speedObj).(*speed); ok { 65 | return sObj 66 | } 67 | s = new(speed) 68 | state.Set(speedObj, s) 69 | return 70 | } 71 | 72 | // ElementSpeed calculates current speed by EWMA 73 | // Optionally can take one or two string arguments. 74 | // First string will be used as value for format speed, default is "%s p/s". 75 | // Second string will be used when speed not available, default is "? p/s" 76 | // In template use as follows: {{speed .}} or {{speed . "%s per second"}} or {{speed . "%s ps" "..."} 77 | var ElementSpeed ElementFunc = func(state *State, args ...string) string { 78 | sp := getSpeedObj(state).value(state) 79 | if sp == 0 { 80 | return argsHelper(args).getNotEmptyOr(1, "? p/s") 81 | } 82 | return fmt.Sprintf(argsHelper(args).getNotEmptyOr(0, "%s p/s"), state.Format(int64(round(sp)))) 83 | } 84 | -------------------------------------------------------------------------------- /v3/template.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "text/template" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // ProgressBarTemplate that template string 12 | type ProgressBarTemplate string 13 | 14 | // New creates new bar from template 15 | func (pbt ProgressBarTemplate) New(total int) *ProgressBar { 16 | return New(total).SetTemplate(pbt) 17 | } 18 | 19 | // Start64 create and start new bar with given int64 total value 20 | func (pbt ProgressBarTemplate) Start64(total int64) *ProgressBar { 21 | return New64(total).SetTemplate(pbt).Start() 22 | } 23 | 24 | // Start create and start new bar with given int total value 25 | func (pbt ProgressBarTemplate) Start(total int) *ProgressBar { 26 | return pbt.Start64(int64(total)) 27 | } 28 | 29 | var templateCacheMu sync.Mutex 30 | var templateCache = make(map[string]*template.Template) 31 | 32 | var defaultTemplateFuncs = template.FuncMap{ 33 | // colors 34 | "black": color.New(color.FgBlack).SprintFunc(), 35 | "red": color.New(color.FgRed).SprintFunc(), 36 | "green": color.New(color.FgGreen).SprintFunc(), 37 | "yellow": color.New(color.FgYellow).SprintFunc(), 38 | "blue": color.New(color.FgBlue).SprintFunc(), 39 | "magenta": color.New(color.FgMagenta).SprintFunc(), 40 | "cyan": color.New(color.FgCyan).SprintFunc(), 41 | "white": color.New(color.FgWhite).SprintFunc(), 42 | "resetcolor": color.New(color.Reset).SprintFunc(), 43 | "rndcolor": rndcolor, 44 | "rnd": rnd, 45 | } 46 | 47 | func getTemplate(tmpl string) (t *template.Template, err error) { 48 | templateCacheMu.Lock() 49 | defer templateCacheMu.Unlock() 50 | t = templateCache[tmpl] 51 | if t != nil { 52 | // found in cache 53 | return 54 | } 55 | t = template.New("") 56 | fillTemplateFuncs(t) 57 | _, err = t.Parse(tmpl) 58 | if err != nil { 59 | t = nil 60 | return 61 | } 62 | templateCache[tmpl] = t 63 | return 64 | } 65 | 66 | func fillTemplateFuncs(t *template.Template) { 67 | t.Funcs(defaultTemplateFuncs) 68 | emf := make(template.FuncMap) 69 | elementsM.Lock() 70 | for k, v := range elements { 71 | element := v 72 | emf[k] = func(state *State, args ...string) string { return element.ProgressElement(state, args...) } 73 | } 74 | elementsM.Unlock() 75 | t.Funcs(emf) 76 | return 77 | } 78 | 79 | func rndcolor(s string) string { 80 | c := rand.Intn(int(color.FgWhite-color.FgBlack)) + int(color.FgBlack) 81 | return color.New(color.Attribute(c)).Sprint(s) 82 | } 83 | 84 | func rnd(args ...string) string { 85 | if len(args) == 0 { 86 | return "" 87 | } 88 | return args[rand.Intn(len(args))] 89 | } 90 | -------------------------------------------------------------------------------- /v3/template_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestProgressBarTemplate(t *testing.T) { 9 | // test New 10 | bar := ProgressBarTemplate(`{{counters . }}`).New(0) 11 | result := bar.String() 12 | expected := "0" 13 | if result != expected { 14 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 15 | } 16 | if bar.IsStarted() { 17 | t.Error("Must be false") 18 | } 19 | 20 | // test Start 21 | bar = ProgressBarTemplate(`{{counters . }}`).Start(42).SetWriter(bytes.NewBuffer(nil)) 22 | result = bar.String() 23 | expected = "0 / 42" 24 | if result != expected { 25 | t.Errorf("Unexpected result: (actual/expected)\n%s\n%s", result, expected) 26 | } 27 | if !bar.IsStarted() { 28 | t.Error("Must be true") 29 | } 30 | } 31 | 32 | func TestTemplateFuncs(t *testing.T) { 33 | var results = make(map[string]int) 34 | for i := 0; i < 100; i++ { 35 | r := rndcolor("s") 36 | results[r] = results[r] + 1 37 | } 38 | if len(results) < 6 { 39 | t.Errorf("Unexpected rndcolor results count: %v", len(results)) 40 | } 41 | 42 | results = make(map[string]int) 43 | for i := 0; i < 100; i++ { 44 | r := rnd("1", "2", "3") 45 | results[r] = results[r] + 1 46 | } 47 | if len(results) != 3 { 48 | t.Errorf("Unexpected rnd results count: %v", len(results)) 49 | } 50 | if r := rnd(); r != "" { 51 | t.Errorf("Unexpected rnd result: '%v'", r) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /v3/termutil/term.go: -------------------------------------------------------------------------------- 1 | package termutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | ) 9 | 10 | var echoLocked bool 11 | var echoLockMutex sync.Mutex 12 | var errLocked = errors.New("terminal locked") 13 | var autoTerminate = true 14 | 15 | // AutoTerminate enables or disables automatic terminate signal catching. 16 | // It's needed to restore the terminal state after the pool was used. 17 | // By default, it's enabled. 18 | func AutoTerminate(enable bool) { 19 | echoLockMutex.Lock() 20 | defer echoLockMutex.Unlock() 21 | autoTerminate = enable 22 | } 23 | 24 | // RawModeOn switches terminal to raw mode 25 | func RawModeOn() (quit chan struct{}, err error) { 26 | echoLockMutex.Lock() 27 | defer echoLockMutex.Unlock() 28 | if echoLocked { 29 | err = errLocked 30 | return 31 | } 32 | if err = lockEcho(); err != nil { 33 | return 34 | } 35 | echoLocked = true 36 | quit = make(chan struct{}, 1) 37 | go catchTerminate(quit) 38 | return 39 | } 40 | 41 | // RawModeOff restore previous terminal state 42 | func RawModeOff() (err error) { 43 | echoLockMutex.Lock() 44 | defer echoLockMutex.Unlock() 45 | if !echoLocked { 46 | return 47 | } 48 | if err = unlockEcho(); err != nil { 49 | return 50 | } 51 | echoLocked = false 52 | return 53 | } 54 | 55 | // listen exit signals and restore terminal state 56 | func catchTerminate(quit chan struct{}) { 57 | sig := make(chan os.Signal, 1) 58 | if autoTerminate { 59 | signal.Notify(sig, unlockSignals...) 60 | defer signal.Stop(sig) 61 | } 62 | select { 63 | case <-quit: 64 | RawModeOff() 65 | case <-sig: 66 | RawModeOff() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /v3/termutil/term_aix.go: -------------------------------------------------------------------------------- 1 | //go:build aix 2 | 3 | package termutil 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | var ( 13 | tty *os.File 14 | 15 | unlockSignals = []os.Signal{ 16 | os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL, 17 | } 18 | ) 19 | 20 | func init() { 21 | var err error 22 | tty, err = os.Open("/dev/tty") 23 | if err != nil { 24 | tty = os.Stdin 25 | } 26 | } 27 | 28 | // TerminalWidth returns width of the terminal. 29 | func TerminalWidth() (int, error) { 30 | _, width, err := TerminalSize() 31 | return width, err 32 | } 33 | 34 | // TerminalSize returns size of the terminal. 35 | func TerminalSize() (int, int, error) { 36 | w, err := unix.IoctlGetWinsize(int(tty.Fd()), syscall.TIOCGWINSZ) 37 | if err != nil { 38 | return 0, 0, err 39 | } 40 | return int(w.Row), int(w.Col), nil 41 | } 42 | 43 | var oldState unix.Termios 44 | 45 | func lockEcho() error { 46 | fd := int(tty.Fd()) 47 | currentState, err := unix.IoctlGetTermios(fd, unix.TCGETS) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | oldState = *currentState 53 | newState := oldState 54 | newState.Lflag &^= syscall.ECHO 55 | newState.Lflag |= syscall.ICANON | syscall.ISIG 56 | newState.Iflag |= syscall.ICRNL 57 | if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newState); err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | func unlockEcho() (err error) { 64 | fd := int(tty.Fd()) 65 | if err := unix.IoctlSetTermios(fd, unix.TCSETS, &oldState); err != nil { 66 | return err 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /v3/termutil/term_appengine.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | // +build appengine 3 | 4 | package termutil 5 | 6 | import "errors" 7 | 8 | // terminalWidth returns width of the terminal, which is not supported 9 | // and should always failed on appengine classic which is a sandboxed PaaS. 10 | func TerminalWidth() (int, error) { 11 | return 0, errors.New("Not supported") 12 | } 13 | -------------------------------------------------------------------------------- /v3/termutil/term_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build (darwin || freebsd || netbsd || openbsd || dragonfly) && !appengine 2 | // +build darwin freebsd netbsd openbsd dragonfly 3 | // +build !appengine 4 | 5 | package termutil 6 | 7 | import "syscall" 8 | 9 | const ioctlReadTermios = syscall.TIOCGETA 10 | const ioctlWriteTermios = syscall.TIOCSETA 11 | -------------------------------------------------------------------------------- /v3/termutil/term_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !appengine 2 | // +build linux,!appengine 3 | 4 | package termutil 5 | 6 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 7 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 8 | -------------------------------------------------------------------------------- /v3/termutil/term_nix.go: -------------------------------------------------------------------------------- 1 | //go:build (linux || darwin || freebsd || netbsd || openbsd || dragonfly) && !appengine 2 | // +build linux darwin freebsd netbsd openbsd dragonfly 3 | // +build !appengine 4 | 5 | package termutil 6 | 7 | import "syscall" 8 | 9 | const sysIoctl = syscall.SYS_IOCTL 10 | -------------------------------------------------------------------------------- /v3/termutil/term_plan9.go: -------------------------------------------------------------------------------- 1 | package termutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "syscall" 7 | ) 8 | 9 | var ( 10 | consctl *os.File 11 | 12 | // Plan 9 doesn't have syscall.SIGQUIT 13 | unlockSignals = []os.Signal{ 14 | os.Interrupt, syscall.SIGTERM, syscall.SIGKILL, 15 | } 16 | ) 17 | 18 | // TerminalWidth returns width of the terminal. 19 | func TerminalWidth() (int, error) { 20 | return 0, errors.New("Not supported") 21 | } 22 | 23 | func lockEcho() error { 24 | if consctl != nil { 25 | return errors.New("consctl already open") 26 | } 27 | var err error 28 | consctl, err = os.OpenFile("/dev/consctl", os.O_WRONLY, 0) 29 | if err != nil { 30 | return err 31 | } 32 | _, err = consctl.WriteString("rawon") 33 | if err != nil { 34 | consctl.Close() 35 | consctl = nil 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func unlockEcho() error { 42 | if consctl == nil { 43 | return nil 44 | } 45 | if err := consctl.Close(); err != nil { 46 | return err 47 | } 48 | consctl = nil 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /v3/termutil/term_solaris.go: -------------------------------------------------------------------------------- 1 | //go:build solaris && !appengine 2 | // +build solaris,!appengine 3 | 4 | package termutil 5 | 6 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 7 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 8 | const sysIoctl = 54 9 | -------------------------------------------------------------------------------- /v3/termutil/term_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package termutil 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | var ( 16 | tty = os.Stdin 17 | 18 | unlockSignals = []os.Signal{ 19 | os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL, 20 | } 21 | ) 22 | 23 | var ( 24 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 25 | 26 | // GetConsoleScreenBufferInfo retrieves information about the 27 | // specified console screen buffer. 28 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 29 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 30 | 31 | // GetConsoleMode retrieves the current input mode of a console's 32 | // input buffer or the current output mode of a console screen buffer. 33 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx 34 | getConsoleMode = kernel32.NewProc("GetConsoleMode") 35 | 36 | // SetConsoleMode sets the input mode of a console's input buffer 37 | // or the output mode of a console screen buffer. 38 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx 39 | setConsoleMode = kernel32.NewProc("SetConsoleMode") 40 | 41 | // SetConsoleCursorPosition sets the cursor position in the 42 | // specified console screen buffer. 43 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx 44 | setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 45 | 46 | mingw = isMingw() 47 | ) 48 | 49 | type ( 50 | // Defines the coordinates of the upper left and lower right corners 51 | // of a rectangle. 52 | // See 53 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx 54 | smallRect struct { 55 | Left, Top, Right, Bottom int16 56 | } 57 | 58 | // Defines the coordinates of a character cell in a console screen 59 | // buffer. The origin of the coordinate system (0,0) is at the top, left cell 60 | // of the buffer. 61 | // See 62 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx 63 | coordinates struct { 64 | X, Y int16 65 | } 66 | 67 | word int16 68 | 69 | // Contains information about a console screen buffer. 70 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx 71 | consoleScreenBufferInfo struct { 72 | dwSize coordinates 73 | dwCursorPosition coordinates 74 | wAttributes word 75 | srWindow smallRect 76 | dwMaximumWindowSize coordinates 77 | } 78 | ) 79 | 80 | // TerminalWidth returns width of the terminal. 81 | func TerminalWidth() (width int, err error) { 82 | if mingw { 83 | return termWidthTPut() 84 | } 85 | return termWidthCmd() 86 | } 87 | 88 | func termWidthCmd() (width int, err error) { 89 | var info consoleScreenBufferInfo 90 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 91 | if e != 0 { 92 | return 0, error(e) 93 | } 94 | return int(info.dwSize.X) - 1, nil 95 | } 96 | 97 | func isMingw() bool { 98 | return os.Getenv("MINGW_PREFIX") != "" || os.Getenv("MSYSTEM") == "MINGW64" 99 | } 100 | 101 | func termWidthTPut() (width int, err error) { 102 | // TODO: maybe anybody knows a better way to get it on mintty... 103 | var res []byte 104 | cmd := exec.Command("tput", "cols") 105 | cmd.Stdin = os.Stdin 106 | if res, err = cmd.CombinedOutput(); err != nil { 107 | return 0, fmt.Errorf("%s: %v", string(res), err) 108 | } 109 | if len(res) > 1 { 110 | res = res[:len(res)-1] 111 | } 112 | return strconv.Atoi(string(res)) 113 | } 114 | 115 | func GetCursorPos() (pos coordinates, err error) { 116 | var info consoleScreenBufferInfo 117 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 118 | if e != 0 { 119 | return info.dwCursorPosition, error(e) 120 | } 121 | return info.dwCursorPosition, nil 122 | } 123 | 124 | func SetCursorPos(pos coordinates) error { 125 | _, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0) 126 | if e != 0 { 127 | return error(e) 128 | } 129 | return nil 130 | } 131 | 132 | var oldState word 133 | 134 | func lockEcho() (err error) { 135 | if _, _, e := syscall.Syscall(getConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&oldState)), 0); e != 0 { 136 | err = fmt.Errorf("Can't get terminal settings: %v", e) 137 | return 138 | } 139 | 140 | newState := oldState 141 | const ENABLE_LINE_INPUT = 0x0002 142 | newState = newState & (^ENABLE_LINE_INPUT) 143 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(newState), 0); e != 0 { 144 | err = fmt.Errorf("Can't set terminal settings: %v", e) 145 | return 146 | } 147 | return 148 | } 149 | 150 | func unlockEcho() (err error) { 151 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(oldState), 0); e != 0 { 152 | err = fmt.Errorf("Can't set terminal settings") 153 | } 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /v3/termutil/term_x.go: -------------------------------------------------------------------------------- 1 | //go:build (linux || darwin || freebsd || netbsd || openbsd || solaris || dragonfly) && !appengine 2 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly 3 | // +build !appengine 4 | 5 | package termutil 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | var ( 15 | tty *os.File 16 | 17 | unlockSignals = []os.Signal{ 18 | os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL, 19 | } 20 | oldState syscall.Termios 21 | ) 22 | 23 | type window struct { 24 | Row uint16 25 | Col uint16 26 | Xpixel uint16 27 | Ypixel uint16 28 | } 29 | 30 | func init() { 31 | var err error 32 | tty, err = os.Open("/dev/tty") 33 | if err != nil { 34 | tty = os.Stdin 35 | } 36 | } 37 | 38 | // TerminalWidth returns width of the terminal. 39 | func TerminalWidth() (int, error) { 40 | _, c, err := TerminalSize() 41 | return c, err 42 | } 43 | 44 | // TerminalSize returns size of the terminal. 45 | func TerminalSize() (rows, cols int, err error) { 46 | w := new(window) 47 | res, _, err := syscall.Syscall(sysIoctl, 48 | tty.Fd(), 49 | uintptr(syscall.TIOCGWINSZ), 50 | uintptr(unsafe.Pointer(w)), 51 | ) 52 | if int(res) == -1 { 53 | return 0, 0, err 54 | } 55 | return int(w.Row), int(w.Col), nil 56 | } 57 | 58 | func lockEcho() error { 59 | fd := tty.Fd() 60 | 61 | if _, _, err := syscall.Syscall(sysIoctl, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState))); err != 0 { 62 | return fmt.Errorf("error when puts the terminal connected to the given file descriptor: %w", err) 63 | } 64 | 65 | newState := oldState 66 | newState.Lflag &^= syscall.ECHO 67 | newState.Lflag |= syscall.ICANON | syscall.ISIG 68 | newState.Iflag |= syscall.ICRNL 69 | if _, _, e := syscall.Syscall(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState))); e != 0 { 70 | return fmt.Errorf("error update terminal settings: %w", e) 71 | } 72 | return nil 73 | } 74 | 75 | func unlockEcho() error { 76 | fd := tty.Fd() 77 | if _, _, err := syscall.Syscall(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState))); err != 0 { 78 | return fmt.Errorf("error restores the terminal connected to the given file descriptor: %w", err) 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /v3/util.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/mattn/go-runewidth" 7 | "math" 8 | "regexp" 9 | //"unicode/utf8" 10 | ) 11 | 12 | const ( 13 | _KiB = 1024 14 | _MiB = 1048576 15 | _GiB = 1073741824 16 | _TiB = 1099511627776 17 | 18 | _kB = 1e3 19 | _MB = 1e6 20 | _GB = 1e9 21 | _TB = 1e12 22 | ) 23 | 24 | var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9;]+\x6d") 25 | 26 | func CellCount(s string) int { 27 | n := runewidth.StringWidth(s) 28 | for _, sm := range ctrlFinder.FindAllString(s, -1) { 29 | n -= runewidth.StringWidth(sm) 30 | } 31 | return n 32 | } 33 | 34 | func StripString(s string, w int) string { 35 | l := CellCount(s) 36 | if l <= w { 37 | return s 38 | } 39 | var buf = bytes.NewBuffer(make([]byte, 0, len(s))) 40 | StripStringToBuffer(s, w, buf) 41 | return buf.String() 42 | } 43 | 44 | func StripStringToBuffer(s string, w int, buf *bytes.Buffer) { 45 | var seqs = ctrlFinder.FindAllStringIndex(s, -1) 46 | var maxWidthReached bool 47 | mainloop: 48 | for i, r := range s { 49 | for _, seq := range seqs { 50 | if i >= seq[0] && i < seq[1] { 51 | buf.WriteRune(r) 52 | continue mainloop 53 | } 54 | } 55 | if rw := CellCount(string(r)); rw <= w && !maxWidthReached { 56 | w -= rw 57 | buf.WriteRune(r) 58 | } else { 59 | maxWidthReached = true 60 | } 61 | } 62 | for w > 0 { 63 | buf.WriteByte(' ') 64 | w-- 65 | } 66 | return 67 | } 68 | 69 | func round(val float64) (newVal float64) { 70 | roundOn := 0.5 71 | places := 0 72 | var round float64 73 | pow := math.Pow(10, float64(places)) 74 | digit := pow * val 75 | _, div := math.Modf(digit) 76 | if div >= roundOn { 77 | round = math.Ceil(digit) 78 | } else { 79 | round = math.Floor(digit) 80 | } 81 | newVal = round / pow 82 | return 83 | } 84 | 85 | // Convert bytes to human readable string. Like a 2 MiB, 64.2 KiB, or 2 MB, 64.2 kB 86 | // if useSIPrefix is set to true 87 | func formatBytes(i int64, useSIPrefix bool) (result string) { 88 | if !useSIPrefix { 89 | switch { 90 | case i >= _TiB: 91 | result = fmt.Sprintf("%.02f TiB", float64(i)/_TiB) 92 | case i >= _GiB: 93 | result = fmt.Sprintf("%.02f GiB", float64(i)/_GiB) 94 | case i >= _MiB: 95 | result = fmt.Sprintf("%.02f MiB", float64(i)/_MiB) 96 | case i >= _KiB: 97 | result = fmt.Sprintf("%.02f KiB", float64(i)/_KiB) 98 | default: 99 | result = fmt.Sprintf("%d B", i) 100 | } 101 | } else { 102 | switch { 103 | case i >= _TB: 104 | result = fmt.Sprintf("%.02f TB", float64(i)/_TB) 105 | case i >= _GB: 106 | result = fmt.Sprintf("%.02f GB", float64(i)/_GB) 107 | case i >= _MB: 108 | result = fmt.Sprintf("%.02f MB", float64(i)/_MB) 109 | case i >= _kB: 110 | result = fmt.Sprintf("%.02f kB", float64(i)/_kB) 111 | default: 112 | result = fmt.Sprintf("%d B", i) 113 | } 114 | } 115 | return 116 | } 117 | -------------------------------------------------------------------------------- /v3/util_test.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | "testing" 6 | ) 7 | 8 | var testColorString = color.RedString("red") + 9 | color.GreenString("hello") + 10 | "simple" + 11 | color.WhiteString("進捗") 12 | 13 | func TestUtilCellCount(t *testing.T) { 14 | if e, l := 18, CellCount(testColorString); l != e { 15 | t.Errorf("Invalid length %d, expected %d", l, e) 16 | } 17 | } 18 | 19 | func TestUtilStripString(t *testing.T) { 20 | if r, e := StripString("12345", 4), "1234"; r != e { 21 | t.Errorf("Invalid result '%s', expected '%s'", r, e) 22 | } 23 | 24 | if r, e := StripString("12345", 5), "12345"; r != e { 25 | t.Errorf("Invalid result '%s', expected '%s'", r, e) 26 | } 27 | if r, e := StripString("12345", 10), "12345"; r != e { 28 | t.Errorf("Invalid result '%s', expected '%s'", r, e) 29 | } 30 | 31 | s := color.RedString("1") + "23" 32 | e := color.RedString("1") + "2" 33 | if r := StripString(s, 2); r != e { 34 | t.Errorf("Invalid result '%s', expected '%s'", r, e) 35 | } 36 | return 37 | } 38 | 39 | func TestUtilRound(t *testing.T) { 40 | if v := round(4.4); v != 4 { 41 | t.Errorf("Unexpected result: %v", v) 42 | } 43 | if v := round(4.501); v != 5 { 44 | t.Errorf("Unexpected result: %v", v) 45 | } 46 | } 47 | 48 | func TestUtilFormatBytes(t *testing.T) { 49 | inputs := []struct { 50 | v int64 51 | s bool 52 | e string 53 | }{ 54 | {v: 1000, s: false, e: "1000 B"}, 55 | {v: 1024, s: false, e: "1.00 KiB"}, 56 | {v: 3*_MiB + 140*_KiB, s: false, e: "3.14 MiB"}, 57 | {v: 2 * _GiB, s: false, e: "2.00 GiB"}, 58 | {v: 2048 * _GiB, s: false, e: "2.00 TiB"}, 59 | 60 | {v: 999, s: true, e: "999 B"}, 61 | {v: 1024, s: true, e: "1.02 kB"}, 62 | {v: 3*_MB + 140*_kB, s: true, e: "3.14 MB"}, 63 | {v: 2 * _GB, s: true, e: "2.00 GB"}, 64 | {v: 2048 * _GB, s: true, e: "2.05 TB"}, 65 | } 66 | 67 | for _, input := range inputs { 68 | actual := formatBytes(input.v, input.s) 69 | if actual != input.e { 70 | t.Errorf("Expected {%s} was {%s}", input.e, actual) 71 | } 72 | } 73 | } 74 | 75 | func BenchmarkUtilsCellCount(b *testing.B) { 76 | b.ReportAllocs() 77 | for i := 0; i < b.N; i++ { 78 | CellCount(testColorString) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // It's proxy Writer, implement io.Writer 8 | type Writer struct { 9 | io.Writer 10 | bar *ProgressBar 11 | } 12 | 13 | func (r *Writer) Write(p []byte) (n int, err error) { 14 | n, err = r.Writer.Write(p) 15 | r.bar.Add(n) 16 | return 17 | } 18 | 19 | // Close the reader when it implements io.Closer 20 | func (r *Writer) Close() (err error) { 21 | r.bar.Finish() 22 | if closer, ok := r.Writer.(io.Closer); ok { 23 | return closer.Close() 24 | } 25 | return 26 | } 27 | --------------------------------------------------------------------------------