├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── images ├── default.gif └── new.gif ├── progress_bar.go └── progress_bar_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 erman imer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # progress_bar 2 | 3 | ![Go](https://github.com/ermanimer/progress_bar/workflows/Go/badge.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/ermanimer/progress_bar)](https://goreportcard.com/report/github.com/ermanimer/progress_bar) 5 | 6 | Go Progress Bar 7 | 8 | ## Features 9 | progress_bar creates a single customizable progress bar for Unix terminal. 10 | 11 | ## Installation 12 | ```bash 13 | go get -u github.com/ermanimer/progress_bar 14 | ``` 15 | 16 | ## Functions 17 | #### DefaultProgressBar(totalValue float64) *ProgressBar 18 | Creates a progress bar with default parameters and given total value. 19 | 20 | |Parameter |Description | 21 | |:---------|:---------------------------| 22 | |totalValue|Total value of progress bar| 23 | 24 | Default parameters: 25 | 26 | |Default Parameter |Value | 27 | |:-----------------------|:--------------------------------------------------------------------------------| 28 | |Default Schema |[{bar}][{percent}][{current}/{total}][Elapsed: {elapsed}s Remaining:{remaining}s]| 29 | |Default Filled Character|# | 30 | |Default Blank Character |. | 31 | |Default Length |50 | 32 | 33 | #### NewProgressBar(output io.Writer, schema string, filledCharacter string, blankCharacter string, length float64, totalValue float64) *ProgressBar 34 | Creates a progress bar with default parameters and given total value. 35 | 36 | |Parameter |Description | 37 | |:--------------|:-------------------------------| 38 | |output |Output of progress bar | 39 | |schema |Schema of progress bar | 40 | |filledCharacter|Filled character of progress bar| 41 | |blankCharacter |Blank character of progress bar | 42 | |length |Length of progress bar | 43 | |totalValue |Total value of progress bar | 44 | 45 | Schema Variables: 46 | 47 | |Schema Variable|Value | 48 | |:--------------|:----------------------------| 49 | |{bar} |Bar of progress bar | 50 | |{percent} |Percentage of progress bar | 51 | |{current} |Current value of progress bar| 52 | |{total} |Total value of progress bar | 53 | |{elapsed} |Elapsed duration | 54 | |{remaining} |Estimated remaining duration | 55 | 56 | ## Methods 57 | #### Start() error 58 | Starts progress bar. 59 | 60 | #### Stop() error 61 | Stops progress bar. 62 | 63 | #### Update(value float64) error 64 | Updates progress bar with given value and stops progress bar is total value is reached. 65 | 66 | |Parameter|Description | 67 | |:--------|:----------------------------| 68 | |value |Current value of progress bar| 69 | 70 | ## Usage 71 | #### Default Progress Bar: 72 | 73 | ```go 74 | package main 75 | 76 | import ( 77 | "fmt" 78 | "time" 79 | 80 | "github.com/ermanimer/progress_bar" 81 | ) 82 | 83 | func main() { 84 | //create new progress bar 85 | pb := progress_bar.DefaultProgressBar(100) 86 | //start 87 | err := pb.Start() 88 | if err != nil { 89 | fmt.Println(err.Error()) 90 | return 91 | } 92 | //update 93 | for value := 1; value <= 100; value++ { 94 | time.Sleep(20 * time.Millisecond) 95 | err := pb.Update(float64(value)) 96 | if err != nil { 97 | fmt.Println(err.Error()) 98 | break 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | Terminal Output: 105 | ![Default Terminal Output](/images/default.gif) 106 | 107 | #### New Progress Bar: 108 | 109 | ```go 110 | package main 111 | 112 | import ( 113 | "fmt" 114 | "os" 115 | "time" 116 | 117 | "github.com/ermanimer/progress_bar" 118 | ) 119 | 120 | func main() { 121 | //create parameters 122 | output := os.Stdout 123 | schema := "({bar}) ({percent}) ({current} of {total} completed)" 124 | filledCharacter := "=" 125 | blankCharacter := "-" 126 | var length float64 = 60 127 | var totalValue float64 = 80 128 | //create new progress bar 129 | pb := progress_bar.NewProgressBar(output, schema, filledCharacter, blankCharacter, length, totalValue) 130 | //start 131 | err := pb.Start() 132 | if err != nil { 133 | fmt.Println(err.Error()) 134 | return 135 | } 136 | //update 137 | for value := 1; value <= 80; value++ { 138 | time.Sleep(20 * time.Millisecond) 139 | err := pb.Update(float64(value)) 140 | if err != nil { 141 | fmt.Println(err.Error()) 142 | break 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | Terminal Output: 149 | ![New Terminal Output](/images/new.gif) 150 | 151 | ## References 152 | - [ANSI Escape Codes, Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code) 153 | - [Build Your Own Command Line With ANSI Escape Codes, Haoyi's Programming Blog](https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html) 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ermanimer/progress_bar 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /images/default.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ermanimer/progress_bar/6b35758c35d85d1db63737584b7cd3bbe4e81b13/images/default.gif -------------------------------------------------------------------------------- /images/new.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ermanimer/progress_bar/6b35758c35d85d1db63737584b7cd3bbe4e81b13/images/new.gif -------------------------------------------------------------------------------- /progress_bar.go: -------------------------------------------------------------------------------- 1 | package progress_bar 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | //default schema 13 | const ( 14 | defaultSchema = "[%s][%s][%s/%s][Elapsed: %ss Remaining: %ss]" 15 | ) 16 | 17 | //schema variables 18 | const ( 19 | svBar = "{bar}" 20 | svPercent = "{percent}" 21 | svCurrentValue = "{current}" 22 | svTotalValue = "{total}" 23 | svElapsedDuration = "{elapsed}" 24 | svRemainingDuration = "{remaining}" 25 | ) 26 | 27 | //default parameters 28 | const ( 29 | defaultFilledCharacter = "#" 30 | defaultBlankCharacter = "." 31 | defaultLength = 50 32 | ) 33 | 34 | //escape codes 35 | const ( 36 | ecClearLine = "\u001b[2K" 37 | ecMoveCursorLeft = "\u001b[%dD" 38 | ) 39 | 40 | type ProgressBar struct { 41 | Output io.Writer 42 | Schema string 43 | FilledCharacter string 44 | BlankCharacter string 45 | Length float64 46 | CurrentValue float64 47 | TotalValue float64 48 | ElapsedDuration float64 49 | RemainingDuration float64 50 | isStarted bool 51 | startingTime time.Time 52 | offset int 53 | } 54 | 55 | func DefaultProgressBar(totalValue float64) *ProgressBar { 56 | return &ProgressBar{ 57 | Output: os.Stdout, 58 | Schema: fmt.Sprintf(defaultSchema, svBar, svPercent, svCurrentValue, svTotalValue, svElapsedDuration, svRemainingDuration), 59 | FilledCharacter: defaultFilledCharacter, 60 | BlankCharacter: defaultBlankCharacter, 61 | Length: defaultLength, 62 | TotalValue: totalValue, 63 | } 64 | } 65 | 66 | func NewProgressBar(output io.Writer, schema string, filledCharacter string, blankCharacter string, length float64, totalValue float64) *ProgressBar { 67 | return &ProgressBar{ 68 | Output: output, 69 | Schema: schema, 70 | FilledCharacter: filledCharacter, 71 | BlankCharacter: blankCharacter, 72 | Length: length, 73 | TotalValue: totalValue, 74 | } 75 | } 76 | 77 | func (pb *ProgressBar) Start() error { 78 | if pb.isStarted { 79 | return errors.New("progress bar is already started") 80 | } 81 | pb.isStarted = true 82 | pb.startingTime = time.Now() 83 | pb.print() 84 | return nil 85 | } 86 | 87 | func (pb *ProgressBar) Stop() error { 88 | if !pb.isStarted { 89 | return errors.New("progress bar is not started") 90 | } 91 | pb.CurrentValue = 0 92 | pb.isStarted = false 93 | pb.offset = 0 94 | fmt.Fprintln(pb.Output, "") 95 | return nil 96 | } 97 | 98 | func (pb *ProgressBar) Update(value float64) error { 99 | if !pb.isStarted { 100 | return errors.New("prograss bar is not started") 101 | } 102 | if value > pb.TotalValue { 103 | return errors.New("value is greater then total value") 104 | } 105 | pb.CurrentValue = value 106 | pb.print() 107 | return nil 108 | } 109 | 110 | func (pb *ProgressBar) print() { 111 | //create bar 112 | filledCharacterCount := int(pb.Length * pb.CurrentValue / pb.TotalValue) 113 | blankCharacterCount := int(pb.Length) - filledCharacterCount 114 | filledCharacters := strings.Repeat(pb.FilledCharacter, filledCharacterCount) 115 | blankCharacters := strings.Repeat(pb.BlankCharacter, blankCharacterCount) 116 | bar := fmt.Sprintf("%s%s", filledCharacters, blankCharacters) 117 | //calculate percentage 118 | percent := pb.CurrentValue / pb.TotalValue * 100 119 | //calculate elapsed and remaining durations 120 | pb.ElapsedDuration = time.Since(pb.startingTime).Seconds() 121 | remainingValue := pb.TotalValue - pb.CurrentValue 122 | pb.RemainingDuration = remainingValue * pb.ElapsedDuration / pb.CurrentValue 123 | //create progress bar 124 | progressBar := strings.Replace(pb.Schema, svBar, bar, 1) 125 | progressBar = strings.Replace(progressBar, svPercent, fmt.Sprintf("%%%.1f", percent), 1) 126 | progressBar = strings.Replace(progressBar, svCurrentValue, fmt.Sprintf("%.1f", pb.CurrentValue), 1) 127 | progressBar = strings.Replace(progressBar, svTotalValue, fmt.Sprintf("%.1f", pb.TotalValue), 1) 128 | progressBar = strings.Replace(progressBar, svElapsedDuration, fmt.Sprintf("%.1f", pb.ElapsedDuration), 1) 129 | progressBar = strings.Replace(progressBar, svRemainingDuration, fmt.Sprintf("%.1f", pb.RemainingDuration), 1) 130 | //clear line and offset cursor 131 | if pb.offset > 0 { 132 | fmt.Fprint(pb.Output, ecClearLine) 133 | fmt.Fprintf(pb.Output, ecMoveCursorLeft, pb.offset) 134 | } 135 | //print progress bar 136 | fmt.Fprint(pb.Output, progressBar) 137 | pb.offset = len(progressBar) 138 | //stop 139 | if pb.CurrentValue == pb.TotalValue { 140 | pb.Stop() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /progress_bar_test.go: -------------------------------------------------------------------------------- 1 | package progress_bar 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDefaultProgressBar(t *testing.T) { 12 | //create output 13 | output := new(bytes.Buffer) 14 | //create default progress bar 15 | pb := DefaultProgressBar(50) 16 | pb.Output = output 17 | //start 18 | expectedOutput := "[..................................................][%0.0][0.0/50.0][Elapsed: 0.0s Remaining: +Infs]" 19 | expectedByteCount := len(expectedOutput) //100 20 | err := start(pb, output, expectedByteCount, expectedOutput) 21 | if err != nil { 22 | t.Error(err.Error()) 23 | } 24 | //update 25 | expectedOutput = "\u001b[2K\u001b[100D[#########################.........................][%50.0][25.0/50.0][Elapsed: 1.0s Remaining: 1.0s]" 26 | expectedByteCount = len(expectedOutput) //111 27 | err = update(pb, 25, output, expectedByteCount, expectedOutput) 28 | if err != nil { 29 | t.Error(err.Error()) 30 | } 31 | //stop 32 | expectedOutput = "\u001b[2K\u001b[101D[##################################################][%100.0][50.0/50.0][Elapsed: 2.0s Remaining: 0.0s]\n" //101: 111 - 10(escape codes) 33 | expectedByteCount = len(expectedOutput) 34 | err = update(pb, 50, output, expectedByteCount, expectedOutput) 35 | if err != nil { 36 | t.Error(err.Error()) 37 | } 38 | } 39 | 40 | func TestNewProgressBar(t *testing.T) { 41 | //create output 42 | output := new(bytes.Buffer) 43 | //create custom parameters 44 | schema := fmt.Sprintf("(%s)(%s)(%s of %s)(E: %ss R: %ss)", svBar, svPercent, svCurrentValue, svTotalValue, svElapsedDuration, svRemainingDuration) 45 | filledCharacter := "=" 46 | blankCharacter := "-" 47 | var length float64 = 60 48 | var value float64 = 80 49 | pb := NewProgressBar(output, schema, filledCharacter, blankCharacter, length, value) 50 | //start 51 | 52 | expectedOutput := "(------------------------------------------------------------)(%0.0)(0.0 of 80.0)(E: 0.0s R: +Infs)" 53 | expectedByteCount := len(expectedOutput) //99 54 | err := start(pb, output, expectedByteCount, expectedOutput) 55 | if err != nil { 56 | t.Error(err.Error()) 57 | } 58 | //update 59 | expectedOutput = "\u001b[2K\u001b[99D(==============================------------------------------)(%50.0)(40.0 of 80.0)(E: 1.0s R: 1.0s)" 60 | expectedByteCount = len(expectedOutput) //109 61 | err = update(pb, 40, output, expectedByteCount, expectedOutput) 62 | if err != nil { 63 | t.Error(err.Error()) 64 | } 65 | //stop 66 | expectedOutput = "\u001b[2K\u001b[100D(============================================================)(%100.0)(80.0 of 80.0)(E: 2.0s R: 0.0s)\n" //100: 109 - 9(escape codes) 67 | expectedByteCount = len(expectedOutput) 68 | err = update(pb, 80, output, expectedByteCount, expectedOutput) 69 | if err != nil { 70 | t.Error(err.Error()) 71 | } 72 | } 73 | 74 | func TestStartError(t *testing.T) { 75 | //create output 76 | output := new(bytes.Buffer) 77 | //create default progress bar 78 | pb := DefaultProgressBar(100) 79 | pb.Output = output 80 | //start 81 | err := pb.Start() 82 | if err != nil { 83 | t.Error("starting progress bar failed") 84 | } 85 | //start again 86 | err = pb.Start() 87 | if err == nil { 88 | t.Error("catching \"progress bar is already started\" error failed") 89 | } 90 | } 91 | 92 | func TestStopError(t *testing.T) { 93 | //create output 94 | output := new(bytes.Buffer) 95 | //create default progress bar 96 | pb := DefaultProgressBar(100) 97 | pb.Output = output 98 | //stop again 99 | err := pb.Stop() 100 | if err == nil { 101 | t.Error("catching \"progress bar is not started\" error failed") 102 | } 103 | } 104 | 105 | func TestUpdateErrors(t *testing.T) { 106 | //create output 107 | output := new(bytes.Buffer) 108 | //create default progress bar 109 | pb := DefaultProgressBar(100) 110 | pb.Output = output 111 | //update 112 | err := pb.Update(50) 113 | if err == nil { 114 | t.Error("catching \"progress bar is noty started\" error failed") 115 | } 116 | //start 117 | err = pb.Start() 118 | if err != nil { 119 | t.Error("starting progress bar failed") 120 | } 121 | //update with a value which is greater then total value 122 | err = pb.Update(101) 123 | if err == nil { 124 | t.Error("catching \"value is greater then total value") 125 | } 126 | } 127 | 128 | func start(pb *ProgressBar, output *bytes.Buffer, expectedByteCount int, expectedOutput string) error { 129 | err := pb.Start() 130 | if err != nil { 131 | return err 132 | } 133 | bs := make([]byte, expectedByteCount) 134 | n, err := output.Read(bs) 135 | if err != nil { 136 | return errors.New("reading output failed") 137 | } 138 | if n != expectedByteCount { 139 | return errors.New("byte count doesn't match") 140 | } 141 | if string(bs) != expectedOutput { 142 | return errors.New("output doesn't match") 143 | } 144 | return nil 145 | } 146 | 147 | func update(pb *ProgressBar, value float64, output *bytes.Buffer, expectedByteCount int, expectedOutput string) error { 148 | time.Sleep(1 * time.Second) 149 | err := pb.Update(value) 150 | if err != nil { 151 | return err 152 | } 153 | bs := make([]byte, expectedByteCount) 154 | n, err := output.Read(bs) 155 | if err != nil { 156 | return errors.New("reading output failed") 157 | } 158 | if n != expectedByteCount { 159 | return errors.New("byte count doesn't match") 160 | } 161 | if string(bs) != expectedOutput { 162 | return errors.New("output doesn't match") 163 | } 164 | return nil 165 | } 166 | --------------------------------------------------------------------------------