diff --git a/cmd/cmd.go b/cmd/cmd.go index 008c6b38..e3dd3837 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -30,7 +30,7 @@ import ( "github.com/jmorganca/ollama/api" "github.com/jmorganca/ollama/format" "github.com/jmorganca/ollama/parser" - "github.com/jmorganca/ollama/progressbar" + "github.com/jmorganca/ollama/progress" "github.com/jmorganca/ollama/readline" "github.com/jmorganca/ollama/server" "github.com/jmorganca/ollama/version" @@ -48,14 +48,20 @@ func CreateHandler(cmd *cobra.Command, args []string) error { return err } + p := progress.NewProgress(os.Stderr) + defer p.Stop() + + bars := make(map[string]*progress.Bar) + + status := fmt.Sprintf("creating %s", args[0]) + spinner := progress.NewSpinner(status) + p.Add(status, spinner) + modelfile, err := os.ReadFile(filename) if err != nil { return err } - spinner := NewSpinner("transferring context") - go spinner.Spin(100 * time.Millisecond) - commands, err := parser.Parse(bytes.NewReader(modelfile)) if err != nil { return err @@ -66,6 +72,12 @@ func CreateHandler(cmd *cobra.Command, args []string) error { return err } + spinner.Stop() + + status = "transferring context" + spinner = progress.NewSpinner(status) + p.Add(status, spinner) + for _, c := range commands { switch c.Name { case "model", "adapter": @@ -99,41 +111,34 @@ func CreateHandler(cmd *cobra.Command, args []string) error { } } - var currentDigest string - var bar *progressbar.ProgressBar - - request := api.CreateRequest{Name: args[0], Path: filename, Modelfile: string(modelfile)} fn := func(resp api.ProgressResponse) error { - if resp.Digest != currentDigest && resp.Digest != "" { + if resp.Digest != "" { spinner.Stop() - currentDigest = resp.Digest - // pulling - bar = progressbar.DefaultBytes( - resp.Total, - resp.Status, - ) - bar.Set64(resp.Completed) - } else if resp.Digest == currentDigest && resp.Digest != "" { - bar.Set64(resp.Completed) - } else { - currentDigest = "" + + bar, ok := bars[resp.Digest] + if !ok { + bar = progress.NewBar(resp.Status, resp.Total, resp.Completed) + bars[resp.Digest] = bar + p.Add(resp.Digest, bar) + } + + bar.Set(resp.Completed) + } else if status != resp.Status { spinner.Stop() - spinner = NewSpinner(resp.Status) - go spinner.Spin(100 * time.Millisecond) + + status = resp.Status + spinner = progress.NewSpinner(status) + p.Add(status, spinner) } return nil } + request := api.CreateRequest{Name: args[0], Path: filename, Modelfile: string(modelfile)} if err := client.Create(context.Background(), &request, fn); err != nil { return err } - spinner.Stop() - if spinner.description != "success" { - return errors.New("unexpected end to create model") - } - return nil } @@ -170,36 +175,44 @@ func PushHandler(cmd *cobra.Command, args []string) error { return err } - var currentDigest string - var bar *progressbar.ProgressBar + p := progress.NewProgress(os.Stderr) + defer p.Stop() + + bars := make(map[string]*progress.Bar) + + status := fmt.Sprintf("pushing %s", args[0]) + spinner := progress.NewSpinner(status) + p.Add(status, spinner) - request := api.PushRequest{Name: args[0], Insecure: insecure} fn := func(resp api.ProgressResponse) error { - if resp.Digest != currentDigest && resp.Digest != "" { - currentDigest = resp.Digest - bar = progressbar.DefaultBytes( - resp.Total, - fmt.Sprintf("pushing %s...", resp.Digest[7:19]), - ) + if resp.Digest != "" { + spinner.Stop() - bar.Set64(resp.Completed) - } else if resp.Digest == currentDigest && resp.Digest != "" { - bar.Set64(resp.Completed) - } else { - currentDigest = "" - fmt.Println(resp.Status) + bar, ok := bars[resp.Digest] + if !ok { + bar = progress.NewBar(resp.Status, resp.Total, resp.Completed) + bars[resp.Digest] = bar + p.Add(resp.Digest, bar) + } + + bar.Set(resp.Completed) + } else if status != resp.Status { + spinner.Stop() + + status = resp.Status + spinner = progress.NewSpinner(status) + p.Add(status, spinner) } + return nil } + request := api.PushRequest{Name: args[0], Insecure: insecure} if err := client.Push(context.Background(), &request, fn); err != nil { return err } - if bar != nil && !bar.IsFinished() { - return errors.New("unexpected end to push model") - } - + spinner.Stop() return nil } @@ -350,46 +363,48 @@ func PullHandler(cmd *cobra.Command, args []string) error { return err } - return pull(args[0], insecure) -} - -func pull(model string, insecure bool) error { client, err := api.ClientFromEnvironment() if err != nil { return err } - var currentDigest string - var bar *progressbar.ProgressBar + p := progress.NewProgress(os.Stderr) + defer p.Stop() + + bars := make(map[string]*progress.Bar) + + status := fmt.Sprintf("pulling %s", args[0]) + spinner := progress.NewSpinner(status) + p.Add(status, spinner) - request := api.PullRequest{Name: model, Insecure: insecure} fn := func(resp api.ProgressResponse) error { - if resp.Digest != currentDigest && resp.Digest != "" { - currentDigest = resp.Digest - bar = progressbar.DefaultBytes( - resp.Total, - fmt.Sprintf("pulling %s...", resp.Digest[7:19]), - ) + if resp.Digest != "" { + spinner.Stop() - bar.Set64(resp.Completed) - } else if resp.Digest == currentDigest && resp.Digest != "" { - bar.Set64(resp.Completed) - } else { - currentDigest = "" - fmt.Println(resp.Status) + bar, ok := bars[resp.Digest] + if !ok { + bar = progress.NewBar(resp.Status, resp.Total, resp.Completed) + bars[resp.Digest] = bar + p.Add(resp.Digest, bar) + } + + bar.Set(resp.Completed) + } else if status != resp.Status { + spinner.Stop() + + status = resp.Status + spinner = progress.NewSpinner(status) + p.Add(status, spinner) } return nil } + request := api.PullRequest{Name: args[0], Insecure: insecure} if err := client.Pull(context.Background(), &request, fn); err != nil { return err } - if bar != nil && !bar.IsFinished() { - return errors.New("unexpected end to pull model") - } - return nil } @@ -442,8 +457,11 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st return err } - spinner := NewSpinner("") - go spinner.Spin(60 * time.Millisecond) + p := progress.NewProgress(os.Stderr) + defer p.Stop() + + spinner := progress.NewSpinner("") + p.Add("", spinner) var latest api.GenerateResponse @@ -475,9 +493,8 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st request := api.GenerateRequest{Model: model, Prompt: prompt, Context: generateContext, Format: format} fn := func(response api.GenerateResponse) error { - if !spinner.IsFinished() { - spinner.Finish() - } + spinner.Stop() + p.StopAndClear() latest = response @@ -511,7 +528,6 @@ func generate(cmd *cobra.Command, model, prompt string, wordWrap bool, format st if err := client.Generate(cancelCtx, &request, fn); err != nil { if strings.Contains(err.Error(), "context canceled") && abort { - spinner.Finish() return nil } return err diff --git a/cmd/spinner.go b/cmd/spinner.go deleted file mode 100644 index 53751c74..00000000 --- a/cmd/spinner.go +++ /dev/null @@ -1,44 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "time" - - "github.com/jmorganca/ollama/progressbar" -) - -type Spinner struct { - description string - *progressbar.ProgressBar -} - -func NewSpinner(description string) *Spinner { - return &Spinner{ - description: description, - ProgressBar: progressbar.NewOptions(-1, - progressbar.OptionSetWriter(os.Stderr), - progressbar.OptionThrottle(60*time.Millisecond), - progressbar.OptionSpinnerType(14), - progressbar.OptionSetRenderBlankState(true), - progressbar.OptionSetElapsedTime(false), - progressbar.OptionClearOnFinish(), - progressbar.OptionSetDescription(description), - ), - } -} - -func (s *Spinner) Spin(tick time.Duration) { - for range time.Tick(tick) { - if s.IsFinished() { - break - } - - s.Add(1) - } -} - -func (s *Spinner) Stop() { - s.Finish() - fmt.Println(s.description) -} diff --git a/format/bytes.go b/format/bytes.go index 1cf5a762..471bdf49 100644 --- a/format/bytes.go +++ b/format/bytes.go @@ -7,10 +7,13 @@ const ( KiloByte = Byte * 1000 MegaByte = KiloByte * 1000 GigaByte = MegaByte * 1000 + TeraByte = GigaByte * 1000 ) func HumanBytes(b int64) string { switch { + case b > TeraByte: + return fmt.Sprintf("%.1f TB", float64(b)/TeraByte) case b > GigaByte: return fmt.Sprintf("%.1f GB", float64(b)/GigaByte) case b > MegaByte: diff --git a/progress/bar.go b/progress/bar.go new file mode 100644 index 00000000..f69fb87b --- /dev/null +++ b/progress/bar.go @@ -0,0 +1,123 @@ +package progress + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/jmorganca/ollama/format" + "golang.org/x/term" +) + +type Bar struct { + message string + messageWidth int + + maxValue int64 + initialValue int64 + currentValue int64 + + started time.Time + stopped time.Time +} + +func NewBar(message string, maxValue, initialValue int64) *Bar { + return &Bar{ + message: message, + messageWidth: -1, + maxValue: maxValue, + initialValue: initialValue, + currentValue: initialValue, + started: time.Now(), + } +} + +func (b *Bar) String() string { + termWidth, _, err := term.GetSize(int(os.Stderr.Fd())) + if err != nil { + panic(err) + } + + var pre, mid, suf strings.Builder + + if b.message != "" { + message := strings.TrimSpace(b.message) + if b.messageWidth > 0 && len(message) > b.messageWidth { + message = message[:b.messageWidth] + } + + fmt.Fprintf(&pre, "%s", message) + if b.messageWidth-pre.Len() >= 0 { + pre.WriteString(strings.Repeat(" ", b.messageWidth-pre.Len())) + } + + pre.WriteString(" ") + } + + fmt.Fprintf(&pre, "%.1f%% ", b.percent()) + + fmt.Fprintf(&suf, "(%s/%s, %s/s, %s)", + format.HumanBytes(b.currentValue), + format.HumanBytes(b.maxValue), + format.HumanBytes(int64(b.rate())), + b.elapsed()) + + mid.WriteString("[") + + // pad 3 for last = or > and "] " + f := termWidth - pre.Len() - mid.Len() - suf.Len() - 3 + n := int(float64(f) * b.percent() / 100) + if n > 0 { + mid.WriteString(strings.Repeat("=", n)) + } + + if b.currentValue >= b.maxValue { + mid.WriteString("=") + } else { + mid.WriteString(">") + } + + if f-n > 0 { + mid.WriteString(strings.Repeat(" ", f-n)) + } + + mid.WriteString("] ") + + return pre.String() + mid.String() + suf.String() +} + +func (b *Bar) Set(value int64) { + if value >= b.maxValue { + value = b.maxValue + b.stopped = time.Now() + } + + b.currentValue = value +} + +func (b *Bar) percent() float64 { + if b.maxValue > 0 { + return float64(b.currentValue) / float64(b.maxValue) * 100 + } + + return 0 +} + +func (b *Bar) rate() float64 { + elapsed := b.elapsed() + if elapsed.Seconds() > 0 { + return (float64(b.currentValue) - float64(b.initialValue)) / elapsed.Seconds() + } + + return 0 +} + +func (b *Bar) elapsed() time.Duration { + stopped := b.stopped + if stopped.IsZero() { + stopped = time.Now() + } + + return stopped.Sub(b.started).Round(time.Second) +} diff --git a/progress/progress.go b/progress/progress.go new file mode 100644 index 00000000..8c195be9 --- /dev/null +++ b/progress/progress.go @@ -0,0 +1,98 @@ +package progress + +import ( + "fmt" + "io" + "sync" + "time" +) + +type State interface { + String() string +} + +type Progress struct { + mu sync.Mutex + pos int + w io.Writer + + ticker *time.Ticker + states []State +} + +func NewProgress(w io.Writer) *Progress { + p := &Progress{w: w} + go p.start() + return p +} + +func (p *Progress) Stop() bool { + for _, state := range p.states { + if spinner, ok := state.(*Spinner); ok { + spinner.Stop() + } + } + + if p.ticker != nil { + p.ticker.Stop() + p.ticker = nil + p.render() + return true + } + + return false +} + +func (p *Progress) StopAndClear() bool { + fmt.Fprint(p.w, "\033[?25l") + defer fmt.Fprint(p.w, "\033[?25h") + + stopped := p.Stop() + if stopped { + // clear the progress bar by: + // 1. for each line in the progress: + // a. move the cursor up one line + // b. clear the line + for i := 0; i < p.pos; i++ { + fmt.Fprint(p.w, "\033[A\033[2K") + } + } + + return stopped +} + +func (p *Progress) Add(key string, state State) { + p.mu.Lock() + defer p.mu.Unlock() + + p.states = append(p.states, state) +} + +func (p *Progress) render() error { + p.mu.Lock() + defer p.mu.Unlock() + + fmt.Fprint(p.w, "\033[?25l") + defer fmt.Fprint(p.w, "\033[?25h") + + if p.pos > 0 { + fmt.Fprintf(p.w, "\033[%dA", p.pos) + } + + for _, state := range p.states { + fmt.Fprintln(p.w, state.String()) + } + + if len(p.states) > 0 { + p.pos = len(p.states) + } + + return nil +} + +func (p *Progress) start() { + p.ticker = time.NewTicker(100 * time.Millisecond) + for range p.ticker.C { + p.render() + } +} diff --git a/progress/spinner.go b/progress/spinner.go new file mode 100644 index 00000000..bc46bc02 --- /dev/null +++ b/progress/spinner.go @@ -0,0 +1,102 @@ +package progress + +import ( + "fmt" + "os" + "strings" + "time" + + "golang.org/x/term" +) + +type Spinner struct { + message string + messageWidth int + + parts []string + + value int + + ticker *time.Ticker + started time.Time + stopped time.Time +} + +func NewSpinner(message string) *Spinner { + s := &Spinner{ + message: message, + parts: []string{ + "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", + }, + started: time.Now(), + } + go s.start() + return s +} + +func (s *Spinner) String() string { + termWidth, _, err := term.GetSize(int(os.Stderr.Fd())) + if err != nil { + panic(err) + } + + var pre strings.Builder + if len(s.message) > 0 { + message := strings.TrimSpace(s.message) + if s.messageWidth > 0 && len(message) > s.messageWidth { + message = message[:s.messageWidth] + } + + fmt.Fprintf(&pre, "%s", message) + if s.messageWidth-pre.Len() >= 0 { + pre.WriteString(strings.Repeat(" ", s.messageWidth-pre.Len())) + } + + pre.WriteString(" ") + } + + var pad int + if s.stopped.IsZero() { + // spinner has a string length of 3 but a rune length of 1 + // in order to align correctly, we need to pad with (3 - 1) = 2 spaces + spinner := s.parts[s.value] + pre.WriteString(spinner) + pad = len(spinner) - len([]rune(spinner)) + } + + var suf strings.Builder + fmt.Fprintf(&suf, "(%s)", s.elapsed()) + + var mid strings.Builder + f := termWidth - pre.Len() - mid.Len() - suf.Len() + pad + if f > 0 { + mid.WriteString(strings.Repeat(" ", f)) + } + + return pre.String() + mid.String() + suf.String() +} + +func (s *Spinner) start() { + s.ticker = time.NewTicker(100 * time.Millisecond) + for range s.ticker.C { + s.value = (s.value + 1) % len(s.parts) + if !s.stopped.IsZero() { + return + } + } +} + +func (s *Spinner) Stop() { + if s.stopped.IsZero() { + s.stopped = time.Now() + } +} + +func (s *Spinner) elapsed() time.Duration { + stopped := s.stopped + if stopped.IsZero() { + stopped = time.Now() + } + + return stopped.Sub(s.started).Round(time.Second) +} diff --git a/progressbar/LICENSE b/progressbar/LICENSE deleted file mode 100644 index 0ca97652..00000000 --- a/progressbar/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Zack - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/progressbar/README.md b/progressbar/README.md deleted file mode 100644 index b9096dbd..00000000 --- a/progressbar/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# progressbar - -[![CI](https://github.com/schollz/progressbar/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/schollz/progressbar/actions/workflows/ci.yml) -[![go report card](https://goreportcard.com/badge/github.com/schollz/progressbar)](https://goreportcard.com/report/github.com/schollz/progressbar) -[![coverage](https://img.shields.io/badge/coverage-84%25-brightgreen.svg)](https://gocover.io/github.com/schollz/progressbar) -[![godocs](https://godoc.org/github.com/schollz/progressbar?status.svg)](https://godoc.org/github.com/schollz/progressbar/v3) - -A very simple thread-safe progress bar which should work on every OS without problems. I needed a progressbar for [croc](https://github.com/schollz/croc) and everything I tried had problems, so I made another one. In order to be OS agnostic I do not plan to support [multi-line outputs](https://github.com/schollz/progressbar/issues/6). - - -## Install - -``` -go get -u github.com/schollz/progressbar/v3 -``` - -## Usage - -### Basic usage - -```golang -bar := progressbar.Default(100) -for i := 0; i < 100; i++ { - bar.Add(1) - time.Sleep(40 * time.Millisecond) -} -``` - -which looks like: - -![Example of basic bar](examples/basic/basic.gif) - - -### I/O operations - -The `progressbar` implements an `io.Writer` so it can automatically detect the number of bytes written to a stream, so you can use it as a progressbar for an `io.Reader`. - -```golang -req, _ := http.NewRequest("GET", "https://dl.google.com/go/go1.14.2.src.tar.gz", nil) -resp, _ := http.DefaultClient.Do(req) -defer resp.Body.Close() - -f, _ := os.OpenFile("go1.14.2.src.tar.gz", os.O_CREATE|os.O_WRONLY, 0644) -defer f.Close() - -bar := progressbar.DefaultBytes( - resp.ContentLength, - "downloading", -) -io.Copy(io.MultiWriter(f, bar), resp.Body) -``` - -which looks like: - -![Example of download bar](examples/download/download.gif) - - -### Progress bar with unknown length - -A progressbar with unknown length is a spinner. Any bar with -1 length will automatically convert it to a spinner with a customizable spinner type. For example, the above code can be run and set the `resp.ContentLength` to `-1`. - -which looks like: - -![Example of download bar with unknown length](examples/download-unknown/download-unknown.gif) - - -### Customization - -There is a lot of customization that you can do - change the writer, the color, the width, description, theme, etc. See [all the options](https://pkg.go.dev/github.com/schollz/progressbar/v3?tab=doc#Option). - -```golang -bar := progressbar.NewOptions(1000, - progressbar.OptionSetWriter(ansi.NewAnsiStdout()), - progressbar.OptionEnableColorCodes(true), - progressbar.OptionShowBytes(true), - progressbar.OptionSetWidth(15), - progressbar.OptionSetDescription("[cyan][1/3][reset] Writing moshable file..."), - progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "[green]=[reset]", - SaucerHead: "[green]>[reset]", - SaucerPadding: " ", - BarStart: "[", - BarEnd: "]", - })) -for i := 0; i < 1000; i++ { - bar.Add(1) - time.Sleep(5 * time.Millisecond) -} -``` - -which looks like: - -![Example of customized bar](examples/customization/customization.gif) - - -## Contributing - -Pull requests are welcome. Feel free to... - -- Revise documentation -- Add new features -- Fix bugs -- Suggest improvements - -## Thanks - -Thanks [@Dynom](https://github.com/dynom) for massive improvements in version 2.0! - -Thanks [@CrushedPixel](https://github.com/CrushedPixel) for adding descriptions and color code support! - -Thanks [@MrMe42](https://github.com/MrMe42) for adding some minor features! - -Thanks [@tehstun](https://github.com/tehstun) for some great PRs! - -Thanks [@Benzammour](https://github.com/Benzammour) and [@haseth](https://github.com/haseth) for helping create v3! - -Thanks [@briandowns](https://github.com/briandowns) for compiling the list of spinners. - -## License - -MIT diff --git a/progressbar/progressbar.go b/progressbar/progressbar.go deleted file mode 100644 index c1a36c43..00000000 --- a/progressbar/progressbar.go +++ /dev/null @@ -1,1098 +0,0 @@ -package progressbar - -import ( - "errors" - "fmt" - "io" - "math" - "os" - "regexp" - "strings" - "sync" - "time" - - "github.com/mattn/go-runewidth" - "github.com/mitchellh/colorstring" - "golang.org/x/term" -) - -// ProgressBar is a thread-safe, simple -// progress bar -type ProgressBar struct { - state state - config config - lock sync.Mutex -} - -// State is the basic properties of the bar -type State struct { - CurrentPercent float64 - CurrentBytes float64 - SecondsSince float64 - SecondsLeft float64 - KBsPerSecond float64 -} - -type state struct { - currentNum int64 - currentPercent int - lastPercent int - currentSaucerSize int - isAltSaucerHead bool - - lastShown time.Time - startTime time.Time - - counterTime time.Time - counterNumSinceLast int64 - counterLastTenRates []float64 - - maxLineWidth int - currentBytes float64 - finished bool - exit bool // Progress bar exit halfway - - rendered string -} - -type config struct { - max int64 // max number of the counter - maxHumanized string - maxHumanizedSuffix string - width int - writer io.Writer - theme Theme - renderWithBlankState bool - description string - iterationString string - ignoreLength bool // ignoreLength if max bytes not known - - // whether the output is expected to contain color codes - colorCodes bool - - // show rate of change in kB/sec or MB/sec - showBytes bool - // show the iterations per second - showIterationsPerSecond bool - showIterationsCount bool - - // whether the progress bar should show elapsed time. - // always enabled if predictTime is true. - elapsedTime bool - - showElapsedTimeOnFinish bool - - // whether the progress bar should attempt to predict the finishing - // time of the progress based on the start time and the average - // number of seconds between increments. - predictTime bool - - // minimum time to wait in between updates - throttleDuration time.Duration - - // clear bar once finished - clearOnFinish bool - - // spinnerType should be a number between 0-75 - spinnerType int - - // spinnerTypeOptionUsed remembers if the spinnerType was changed manually - spinnerTypeOptionUsed bool - - // spinner represents the spinner as a slice of string - spinner []string - - // fullWidth specifies whether to measure and set the bar to a specific width - fullWidth bool - - // invisible doesn't render the bar at all, useful for debugging - invisible bool - - onCompletion func() - - // whether the render function should make use of ANSI codes to reduce console I/O - useANSICodes bool - - // showDescriptionAtLineEnd specifies whether description should be written at line end instead of line start - showDescriptionAtLineEnd bool -} - -// Theme defines the elements of the bar -type Theme struct { - Saucer string - AltSaucerHead string - SaucerHead string - SaucerPadding string - BarStart string - BarEnd string -} - -// Option is the type all options need to adhere to -type Option func(p *ProgressBar) - -// OptionSetWidth sets the width of the bar -func OptionSetWidth(s int) Option { - return func(p *ProgressBar) { - p.config.width = s - } -} - -// OptionSpinnerType sets the type of spinner used for indeterminate bars -func OptionSpinnerType(spinnerType int) Option { - return func(p *ProgressBar) { - p.config.spinnerTypeOptionUsed = true - p.config.spinnerType = spinnerType - } -} - -// OptionSpinnerCustom sets the spinner used for indeterminate bars to the passed -// slice of string -func OptionSpinnerCustom(spinner []string) Option { - return func(p *ProgressBar) { - p.config.spinner = spinner - } -} - -// OptionSetTheme sets the elements the bar is constructed of -func OptionSetTheme(t Theme) Option { - return func(p *ProgressBar) { - p.config.theme = t - } -} - -// OptionSetVisibility sets the visibility -func OptionSetVisibility(visibility bool) Option { - return func(p *ProgressBar) { - p.config.invisible = !visibility - } -} - -// OptionFullWidth sets the bar to be full width -func OptionFullWidth() Option { - return func(p *ProgressBar) { - p.config.fullWidth = true - } -} - -// OptionSetWriter sets the output writer (defaults to os.StdOut) -func OptionSetWriter(w io.Writer) Option { - return func(p *ProgressBar) { - p.config.writer = w - } -} - -// OptionSetRenderBlankState sets whether or not to render a 0% bar on construction -func OptionSetRenderBlankState(r bool) Option { - return func(p *ProgressBar) { - p.config.renderWithBlankState = r - } -} - -// OptionSetDescription sets the description of the bar to render in front of it -func OptionSetDescription(description string) Option { - return func(p *ProgressBar) { - p.config.description = description - } -} - -// OptionEnableColorCodes enables or disables support for color codes -// using mitchellh/colorstring -func OptionEnableColorCodes(colorCodes bool) Option { - return func(p *ProgressBar) { - p.config.colorCodes = colorCodes - } -} - -// OptionSetElapsedTime will enable elapsed time. Always enabled if OptionSetPredictTime is true. -func OptionSetElapsedTime(elapsedTime bool) Option { - return func(p *ProgressBar) { - p.config.elapsedTime = elapsedTime - } -} - -// OptionSetPredictTime will also attempt to predict the time remaining. -func OptionSetPredictTime(predictTime bool) Option { - return func(p *ProgressBar) { - p.config.predictTime = predictTime - } -} - -// OptionShowCount will also print current count out of total -func OptionShowCount() Option { - return func(p *ProgressBar) { - p.config.showIterationsCount = true - } -} - -// OptionShowIts will also print the iterations/second -func OptionShowIts() Option { - return func(p *ProgressBar) { - p.config.showIterationsPerSecond = true - } -} - -// OptionShowElapsedOnFinish will keep the display of elapsed time on finish -func OptionShowElapsedTimeOnFinish() Option { - return func(p *ProgressBar) { - p.config.showElapsedTimeOnFinish = true - } -} - -// OptionSetItsString sets what's displayed for iterations a second. The default is "it" which would display: "it/s" -func OptionSetItsString(iterationString string) Option { - return func(p *ProgressBar) { - p.config.iterationString = iterationString - } -} - -// OptionThrottle will wait the specified duration before updating again. The default -// duration is 0 seconds. -func OptionThrottle(duration time.Duration) Option { - return func(p *ProgressBar) { - p.config.throttleDuration = duration - } -} - -// OptionClearOnFinish will clear the bar once its finished -func OptionClearOnFinish() Option { - return func(p *ProgressBar) { - p.config.clearOnFinish = true - } -} - -// OptionOnCompletion will invoke cmpl function once its finished -func OptionOnCompletion(cmpl func()) Option { - return func(p *ProgressBar) { - p.config.onCompletion = cmpl - } -} - -// OptionShowBytes will update the progress bar -// configuration settings to display/hide kBytes/Sec -func OptionShowBytes(val bool) Option { - return func(p *ProgressBar) { - p.config.showBytes = val - } -} - -// OptionUseANSICodes will use more optimized terminal i/o. -// -// Only useful in environments with support for ANSI escape sequences. -func OptionUseANSICodes(val bool) Option { - return func(p *ProgressBar) { - p.config.useANSICodes = val - } -} - -// OptionShowDescriptionAtLineEnd defines whether description should be written at line end instead of line start -func OptionShowDescriptionAtLineEnd() Option { - return func(p *ProgressBar) { - p.config.showDescriptionAtLineEnd = true - } -} - -var defaultTheme = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "▕", BarEnd: "▏"} - -// NewOptions constructs a new instance of ProgressBar, with any options you specify -func NewOptions(max int, options ...Option) *ProgressBar { - return NewOptions64(int64(max), options...) -} - -// NewOptions64 constructs a new instance of ProgressBar, with any options you specify -func NewOptions64(max int64, options ...Option) *ProgressBar { - b := ProgressBar{ - state: getBasicState(), - config: config{ - writer: os.Stdout, - theme: defaultTheme, - iterationString: "it", - width: 40, - max: max, - throttleDuration: 0 * time.Nanosecond, - elapsedTime: true, - predictTime: true, - spinnerType: 9, - invisible: false, - }, - } - - for _, o := range options { - o(&b) - } - - if b.config.spinnerType < 0 || b.config.spinnerType > 75 { - panic("invalid spinner type, must be between 0 and 75") - } - - // ignoreLength if max bytes not known - if b.config.max == -1 { - b.config.ignoreLength = true - b.config.max = int64(b.config.width) - b.config.predictTime = false - } - - b.config.maxHumanized, b.config.maxHumanizedSuffix = humanizeBytes(float64(b.config.max)) - - if b.config.renderWithBlankState { - b.RenderBlank() - } - - return &b -} - -func getBasicState() state { - now := time.Now() - return state{ - startTime: now, - lastShown: now, - counterTime: now, - } -} - -// New returns a new ProgressBar -// with the specified maximum -func New(max int) *ProgressBar { - return NewOptions(max) -} - -// DefaultBytes provides a progressbar to measure byte -// throughput with recommended defaults. -// Set maxBytes to -1 to use as a spinner. -func DefaultBytes(maxBytes int64, description ...string) *ProgressBar { - desc := "" - if len(description) > 0 { - desc = description[0] - } - return NewOptions64( - maxBytes, - OptionSetDescription(desc), - OptionSetWriter(os.Stderr), - OptionShowBytes(true), - OptionSetWidth(10), - OptionThrottle(65*time.Millisecond), - OptionShowCount(), - OptionOnCompletion(func() { - fmt.Fprint(os.Stderr, "\n") - }), - OptionSpinnerType(14), - OptionFullWidth(), - OptionSetRenderBlankState(true), - ) -} - -// DefaultBytesSilent is the same as DefaultBytes, but does not output anywhere. -// String() can be used to get the output instead. -func DefaultBytesSilent(maxBytes int64, description ...string) *ProgressBar { - // Mostly the same bar as DefaultBytes - - desc := "" - if len(description) > 0 { - desc = description[0] - } - return NewOptions64( - maxBytes, - OptionSetDescription(desc), - OptionSetWriter(io.Discard), - OptionShowBytes(true), - OptionSetWidth(10), - OptionThrottle(65*time.Millisecond), - OptionShowCount(), - OptionSpinnerType(14), - OptionFullWidth(), - ) -} - -// Default provides a progressbar with recommended defaults. -// Set max to -1 to use as a spinner. -func Default(max int64, description ...string) *ProgressBar { - desc := "" - if len(description) > 0 { - desc = description[0] - } - return NewOptions64( - max, - OptionSetDescription(desc), - OptionSetWriter(os.Stderr), - OptionSetWidth(10), - OptionThrottle(65*time.Millisecond), - OptionShowCount(), - OptionShowIts(), - OptionOnCompletion(func() { - fmt.Fprint(os.Stderr, "\n") - }), - OptionSpinnerType(14), - OptionFullWidth(), - OptionSetRenderBlankState(true), - ) -} - -// DefaultSilent is the same as Default, but does not output anywhere. -// String() can be used to get the output instead. -func DefaultSilent(max int64, description ...string) *ProgressBar { - // Mostly the same bar as Default - - desc := "" - if len(description) > 0 { - desc = description[0] - } - return NewOptions64( - max, - OptionSetDescription(desc), - OptionSetWriter(io.Discard), - OptionSetWidth(10), - OptionThrottle(65*time.Millisecond), - OptionShowCount(), - OptionShowIts(), - OptionSpinnerType(14), - OptionFullWidth(), - ) -} - -// String returns the current rendered version of the progress bar. -// It will never return an empty string while the progress bar is running. -func (p *ProgressBar) String() string { - return p.state.rendered -} - -// RenderBlank renders the current bar state, you can use this to render a 0% state -func (p *ProgressBar) RenderBlank() error { - if p.config.invisible { - return nil - } - if p.state.currentNum == 0 { - p.state.lastShown = time.Time{} - } - return p.render() -} - -// Reset will reset the clock that is used -// to calculate current time and the time left. -func (p *ProgressBar) Reset() { - p.lock.Lock() - defer p.lock.Unlock() - - p.state = getBasicState() -} - -// Finish will fill the bar to full -func (p *ProgressBar) Finish() error { - p.lock.Lock() - p.state.currentNum = p.config.max - p.lock.Unlock() - return p.Add(0) -} - -// Exit will exit the bar to keep current state -func (p *ProgressBar) Exit() error { - p.lock.Lock() - defer p.lock.Unlock() - - p.state.exit = true - if p.config.onCompletion != nil { - p.config.onCompletion() - } - return nil -} - -// Add will add the specified amount to the progressbar -func (p *ProgressBar) Add(num int) error { - return p.Add64(int64(num)) -} - -// Set will set the bar to a current number -func (p *ProgressBar) Set(num int) error { - return p.Set64(int64(num)) -} - -// Set64 will set the bar to a current number -func (p *ProgressBar) Set64(num int64) error { - p.lock.Lock() - toAdd := num - int64(p.state.currentBytes) - p.lock.Unlock() - return p.Add64(toAdd) -} - -// Add64 will add the specified amount to the progressbar -func (p *ProgressBar) Add64(num int64) error { - if p.config.invisible { - return nil - } - p.lock.Lock() - defer p.lock.Unlock() - - if p.state.exit { - return nil - } - - // error out since OptionSpinnerCustom will always override a manually set spinnerType - if p.config.spinnerTypeOptionUsed && len(p.config.spinner) > 0 { - return errors.New("OptionSpinnerType and OptionSpinnerCustom cannot be used together") - } - - if p.config.max == 0 { - return errors.New("max must be greater than 0") - } - - if p.state.currentNum < p.config.max { - if p.config.ignoreLength { - p.state.currentNum = (p.state.currentNum + num) % p.config.max - } else { - p.state.currentNum += num - } - } - - p.state.currentBytes += float64(num) - - // reset the countdown timer every second to take rolling average - p.state.counterNumSinceLast += num - if time.Since(p.state.counterTime).Seconds() > 0.5 { - p.state.counterLastTenRates = append(p.state.counterLastTenRates, float64(p.state.counterNumSinceLast)/time.Since(p.state.counterTime).Seconds()) - if len(p.state.counterLastTenRates) > 10 { - p.state.counterLastTenRates = p.state.counterLastTenRates[1:] - } - p.state.counterTime = time.Now() - p.state.counterNumSinceLast = 0 - } - - percent := float64(p.state.currentNum) / float64(p.config.max) - p.state.currentSaucerSize = int(percent * float64(p.config.width)) - p.state.currentPercent = int(percent * 100) - updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0 - - p.state.lastPercent = p.state.currentPercent - if p.state.currentNum > p.config.max { - return errors.New("current number exceeds max") - } - - // always update if show bytes/second or its/second - if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount { - return p.render() - } - - return nil -} - -// Clear erases the progress bar from the current line -func (p *ProgressBar) Clear() error { - return clearProgressBar(p.config, p.state) -} - -// Describe will change the description shown before the progress, which -// can be changed on the fly (as for a slow running process). -func (p *ProgressBar) Describe(description string) { - p.lock.Lock() - defer p.lock.Unlock() - p.config.description = description - if p.config.invisible { - return - } - p.render() -} - -// New64 returns a new ProgressBar -// with the specified maximum -func New64(max int64) *ProgressBar { - return NewOptions64(max) -} - -// GetMax returns the max of a bar -func (p *ProgressBar) GetMax() int { - return int(p.config.max) -} - -// GetMax64 returns the current max -func (p *ProgressBar) GetMax64() int64 { - return p.config.max -} - -// ChangeMax takes in a int -// and changes the max value -// of the progress bar -func (p *ProgressBar) ChangeMax(newMax int) { - p.ChangeMax64(int64(newMax)) -} - -// ChangeMax64 is basically -// the same as ChangeMax, -// but takes in a int64 -// to avoid casting -func (p *ProgressBar) ChangeMax64(newMax int64) { - p.config.max = newMax - - if p.config.showBytes { - p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max)) - } - - p.Add(0) // re-render -} - -// IsFinished returns true if progress bar is completed -func (p *ProgressBar) IsFinished() bool { - return p.state.finished -} - -// render renders the progress bar, updating the maximum -// rendered line width. this function is not thread-safe, -// so it must be called with an acquired lock. -func (p *ProgressBar) render() error { - // make sure that the rendering is not happening too quickly - // but always show if the currentNum reaches the max - if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() && - p.state.currentNum < p.config.max { - return nil - } - - if !p.config.useANSICodes { - // first, clear the existing progress bar - err := clearProgressBar(p.config, p.state) - if err != nil { - return err - } - } - - // check if the progress bar is finished - if !p.state.finished && p.state.currentNum >= p.config.max { - p.state.finished = true - if !p.config.clearOnFinish { - renderProgressBar(p.config, &p.state) - } - if p.config.onCompletion != nil { - p.config.onCompletion() - } - } - if p.state.finished { - // when using ANSI codes we don't pre-clean the current line - if p.config.useANSICodes && p.config.clearOnFinish { - err := clearProgressBar(p.config, p.state) - if err != nil { - return err - } - } - return nil - } - - // then, re-render the current progress bar - w, err := renderProgressBar(p.config, &p.state) - if err != nil { - return err - } - - if w > p.state.maxLineWidth { - p.state.maxLineWidth = w - } - - p.state.lastShown = time.Now() - - return nil -} - -// State returns the current state -func (p *ProgressBar) State() State { - p.lock.Lock() - defer p.lock.Unlock() - s := State{} - s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max) - s.CurrentBytes = p.state.currentBytes - s.SecondsSince = time.Since(p.state.startTime).Seconds() - if p.state.currentNum > 0 { - s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum)) - } - s.KBsPerSecond = float64(p.state.currentBytes) / 1000.0 / s.SecondsSince - return s -} - -// regex matching ansi escape codes -var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) - -func getStringWidth(c config, str string, colorize bool) int { - if c.colorCodes { - // convert any color codes in the progress bar into the respective ANSI codes - str = colorstring.Color(str) - } - - // the width of the string, if printed to the console - // does not include the carriage return character - cleanString := strings.Replace(str, "\r", "", -1) - - if c.colorCodes { - // the ANSI codes for the colors do not take up space in the console output, - // so they do not count towards the output string width - cleanString = ansiRegex.ReplaceAllString(cleanString, "") - } - - // get the amount of runes in the string instead of the - // character count of the string, as some runes span multiple characters. - // see https://stackoverflow.com/a/12668840/2733724 - stringWidth := runewidth.StringWidth(cleanString) - return stringWidth -} - -func renderProgressBar(c config, s *state) (int, error) { - var sb strings.Builder - - averageRate := average(s.counterLastTenRates) - if len(s.counterLastTenRates) == 0 || s.finished { - // if no average samples, or if finished, - // then average rate should be the total rate - if t := time.Since(s.startTime).Seconds(); t > 0 { - averageRate = s.currentBytes / t - } else { - averageRate = 0 - } - } - - // show iteration count in "current/total" iterations format - if c.showIterationsCount { - if sb.Len() == 0 { - sb.WriteString("(") - } else { - sb.WriteString(", ") - } - if !c.ignoreLength { - if c.showBytes { - currentHumanize, currentSuffix := humanizeBytes(s.currentBytes) - if currentSuffix == c.maxHumanizedSuffix { - sb.WriteString(fmt.Sprintf("%s/%s%s", - currentHumanize, c.maxHumanized, c.maxHumanizedSuffix)) - } else { - sb.WriteString(fmt.Sprintf("%s%s/%s%s", - currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix)) - } - } else { - sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max)) - } - } else { - if c.showBytes { - currentHumanize, currentSuffix := humanizeBytes(s.currentBytes) - sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix)) - } else { - sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-")) - } - } - } - - // show rolling average rate - if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) { - if sb.Len() == 0 { - sb.WriteString("(") - } else { - sb.WriteString(", ") - } - currentHumanize, currentSuffix := humanizeBytes(averageRate) - sb.WriteString(fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix)) - } - - // show iterations rate - if c.showIterationsPerSecond { - if sb.Len() == 0 { - sb.WriteString("(") - } else { - sb.WriteString(", ") - } - if averageRate > 1 { - sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString)) - } else if averageRate*60 > 1 { - sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString)) - } else { - sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString)) - } - } - if sb.Len() > 0 { - sb.WriteString(")") - } - - leftBrac, rightBrac, saucer, saucerHead := "", "", "", "" - - // show time prediction in "current/total" seconds format - switch { - case c.predictTime: - rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second) - if rightBracNum.Seconds() < 0 { - rightBracNum = 0 * time.Second - } - rightBrac = rightBracNum.String() - fallthrough - case c.elapsedTime: - leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String() - } - - if c.fullWidth && !c.ignoreLength { - width, err := termWidth() - if err != nil { - width = 80 - } - - amend := 1 // an extra space at eol - switch { - case leftBrac != "" && rightBrac != "": - amend = 4 // space, square brackets and colon - case leftBrac != "" && rightBrac == "": - amend = 4 // space and square brackets and another space - case leftBrac == "" && rightBrac != "": - amend = 3 // space and square brackets - } - if c.showDescriptionAtLineEnd { - amend += 1 // another space - } - - c.width = width - getStringWidth(c, c.description, true) - 10 - amend - sb.Len() - len(leftBrac) - len(rightBrac) - s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width)) - } - if s.currentSaucerSize > 0 { - if c.ignoreLength { - saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1) - } else { - saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1) - } - - // Check if an alternate saucer head is set for animation - if c.theme.AltSaucerHead != "" && s.isAltSaucerHead { - saucerHead = c.theme.AltSaucerHead - s.isAltSaucerHead = false - } else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width { - // use the saucer for the saucer head if it hasn't been set - // to preserve backwards compatibility - saucerHead = c.theme.Saucer - } else { - saucerHead = c.theme.SaucerHead - s.isAltSaucerHead = true - } - } - - /* - Progress Bar format - Description % |------ | (kb/s) (iteration count) (iteration rate) (predict time) - - or if showDescriptionAtLineEnd is enabled - % |------ | (kb/s) (iteration count) (iteration rate) (predict time) Description - */ - - repeatAmount := c.width - s.currentSaucerSize - if repeatAmount < 0 { - repeatAmount = 0 - } - - str := "" - - if c.ignoreLength { - selectedSpinner := spinners[c.spinnerType] - if len(c.spinner) > 0 { - selectedSpinner = c.spinner - } - spinner := selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(selectedSpinner)))))] - if c.elapsedTime { - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s [%s] %s ", - spinner, - sb.String(), - leftBrac, - c.description) - } else { - str = fmt.Sprintf("\r%s %s %s [%s] ", - spinner, - c.description, - sb.String(), - leftBrac) - } - } else { - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s %s ", - spinner, - sb.String(), - c.description) - } else { - str = fmt.Sprintf("\r%s %s %s ", - spinner, - c.description, - sb.String()) - } - } - } else if rightBrac == "" { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", - s.currentPercent, - c.theme.BarStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, - sb.String()) - - if s.currentPercent == 100 && c.showElapsedTimeOnFinish { - str = fmt.Sprintf("%s [%s]", str, leftBrac) - } - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s ", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s ", c.description, str) - } - } else { - if s.currentPercent == 100 { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", - s.currentPercent, - c.theme.BarStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, - sb.String()) - - if c.showElapsedTimeOnFinish { - str = fmt.Sprintf("%s [%s]", str, leftBrac) - } - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s", c.description, str) - } - } else { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]", - s.currentPercent, - c.theme.BarStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - c.theme.BarEnd, - sb.String(), - leftBrac, - rightBrac) - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s", c.description, str) - } - } - } - - if c.colorCodes { - // convert any color codes in the progress bar into the respective ANSI codes - str = colorstring.Color(str) - } - - s.rendered = str - - return getStringWidth(c, str, false), writeString(c, str) -} - -func clearProgressBar(c config, s state) error { - if s.maxLineWidth == 0 { - return nil - } - if c.useANSICodes { - // write the "clear current line" ANSI escape sequence - return writeString(c, "\033[2K\r") - } - // fill the empty content - // to overwrite the progress bar and jump - // back to the beginning of the line - str := fmt.Sprintf("\r%s\r", strings.Repeat(" ", s.maxLineWidth)) - return writeString(c, str) - // the following does not show correctly if the previous line is longer than subsequent line - // return writeString(c, "\r") -} - -func writeString(c config, str string) error { - if _, err := io.WriteString(c.writer, str); err != nil { - return err - } - - if f, ok := c.writer.(*os.File); ok { - // ignore any errors in Sync(), as stdout - // can't be synced on some operating systems - // like Debian 9 (Stretch) - f.Sync() - } - - return nil -} - -// Reader is the progressbar io.Reader struct -type Reader struct { - io.Reader - bar *ProgressBar -} - -// NewReader return a new Reader with a given progress bar. -func NewReader(r io.Reader, bar *ProgressBar) Reader { - return Reader{ - Reader: r, - bar: bar, - } -} - -// Read will read the data and add the number of bytes to the progressbar -func (r *Reader) Read(p []byte) (n int, err error) { - n, err = r.Reader.Read(p) - r.bar.Add(n) - return -} - -// Close the reader when it implements io.Closer -func (r *Reader) Close() (err error) { - if closer, ok := r.Reader.(io.Closer); ok { - return closer.Close() - } - r.bar.Finish() - return -} - -// Write implement io.Writer -func (p *ProgressBar) Write(b []byte) (n int, err error) { - n = len(b) - p.Add(n) - return -} - -// Read implement io.Reader -func (p *ProgressBar) Read(b []byte) (n int, err error) { - n = len(b) - p.Add(n) - return -} - -func (p *ProgressBar) Close() (err error) { - p.Finish() - return -} - -func average(xs []float64) float64 { - total := 0.0 - for _, v := range xs { - total += v - } - return total / float64(len(xs)) -} - -func humanizeBytes(s float64) (string, string) { - sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"} - base := 1000.0 - if s < 10 { - return fmt.Sprintf("%2.0f", s), sizes[0] - } - e := math.Floor(logn(float64(s), base)) - suffix := sizes[int(e)] - val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 - f := "%.0f" - if val < 10 { - f = "%.1f" - } - - return fmt.Sprintf(f, val), suffix -} - -func logn(n, b float64) float64 { - return math.Log(n) / math.Log(b) -} - -// termWidth function returns the visible width of the current terminal -// and can be redefined for testing -var termWidth = func() (width int, err error) { - width, _, err = term.GetSize(int(os.Stdout.Fd())) - if err == nil { - return width, nil - } - - return 0, err -} diff --git a/progressbar/spinners.go b/progressbar/spinners.go deleted file mode 100644 index c3ccd01f..00000000 --- a/progressbar/spinners.go +++ /dev/null @@ -1,80 +0,0 @@ -package progressbar - -var spinners = map[int][]string{ - 0: {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"}, - 1: {"▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"}, - 2: {"▖", "▘", "▝", "▗"}, - 3: {"┤", "┘", "┴", "└", "├", "┌", "┬", "┐"}, - 4: {"◢", "◣", "◤", "◥"}, - 5: {"◰", "◳", "◲", "◱"}, - 6: {"◴", "◷", "◶", "◵"}, - 7: {"◐", "◓", "◑", "◒"}, - 8: {".", "o", "O", "@", "*"}, - 9: {"|", "/", "-", "\\"}, - 10: {"◡◡", "⊙⊙", "◠◠"}, - 11: {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}, - 12: {">))'>", " >))'>", " >))'>", " >))'>", " >))'>", " <'((<", " <'((<", " <'((<"}, - 13: {"⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"}, - 14: {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, - 15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}, - 16: {"▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"}, - 17: {"■", "□", "▪", "▫"}, - 18: {"←", "↑", "→", "↓"}, - 19: {"╫", "╪"}, - 20: {"⇐", "⇖", "⇑", "⇗", "⇒", "⇘", "⇓", "⇙"}, - 21: {"⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈"}, - 22: {"⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈"}, - 23: {"⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁"}, - 24: {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"}, - 25: {"ヲ", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ン"}, - 26: {".", "..", "..."}, - 27: {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"}, - 28: {".", "o", "O", "°", "O", "o", "."}, - 29: {"+", "x"}, - 30: {"v", "<", "^", ">"}, - 31: {">>--->", " >>--->", " >>--->", " >>--->", " >>--->", " <---<<", " <---<<", " <---<<", " <---<<", "<---<<"}, - 32: {"|", "||", "|||", "||||", "|||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"}, - 33: {"[ ]", "[= ]", "[== ]", "[=== ]", "[==== ]", "[===== ]", "[====== ]", "[======= ]", "[======== ]", "[========= ]", "[==========]"}, - 34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"}, - 35: {"█▒▒▒▒▒▒▒▒▒", "███▒▒▒▒▒▒▒", "█████▒▒▒▒▒", "███████▒▒▒", "██████████"}, - 36: {"[ ]", "[=> ]", "[===> ]", "[=====> ]", "[======> ]", "[========> ]", "[==========> ]", "[============> ]", "[==============> ]", "[================> ]", "[==================> ]", "[===================>]"}, - 37: {"ဝ", "၀"}, - 38: {"▌", "▀", "▐▄"}, - 39: {"🌍", "🌎", "🌏"}, - 40: {"◜", "◝", "◞", "◟"}, - 41: {"⬒", "⬔", "⬓", "⬕"}, - 42: {"⬖", "⬘", "⬗", "⬙"}, - 43: {"[>>> >]", "[]>>>> []", "[] >>>> []", "[] >>>> []", "[] >>>> []", "[] >>>>[]", "[>> >>]"}, - 44: {"♠", "♣", "♥", "♦"}, - 45: {"➞", "➟", "➠", "➡", "➠", "➟"}, - 46: {" | ", ` \ `, "_ ", ` \ `, " | ", " / ", " _", " / "}, - 47: {" . . . .", ". . . .", ". . . .", ". . . .", ". . . . ", ". . . . ."}, - 48: {" | ", " / ", " _ ", ` \ `, " | ", ` \ `, " _ ", " / "}, - 49: {"⎺", "⎻", "⎼", "⎽", "⎼", "⎻"}, - 50: {"▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"}, - 51: {"[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"}, - 52: {"( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )"}, - 53: {"✶", "✸", "✹", "✺", "✹", "✷"}, - 54: {"▐|\\____________▌", "▐_|\\___________▌", "▐__|\\__________▌", "▐___|\\_________▌", "▐____|\\________▌", "▐_____|\\_______▌", "▐______|\\______▌", "▐_______|\\_____▌", "▐________|\\____▌", "▐_________|\\___▌", "▐__________|\\__▌", "▐___________|\\_▌", "▐____________|\\▌", "▐____________/|▌", "▐___________/|_▌", "▐__________/|__▌", "▐_________/|___▌", "▐________/|____▌", "▐_______/|_____▌", "▐______/|______▌", "▐_____/|_______▌", "▐____/|________▌", "▐___/|_________▌", "▐__/|__________▌", "▐_/|___________▌", "▐/|____________▌"}, - 55: {"▐⠂ ▌", "▐⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂▌", "▐ ⠠▌", "▐ ⡀▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐⠠ ▌"}, - 56: {"¿", "?"}, - 57: {"⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"}, - 58: {"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"}, - 59: {". ", ".. ", "...", " ..", " .", " "}, - 60: {".", "o", "O", "°", "O", "o", "."}, - 61: {"▓", "▒", "░"}, - 62: {"▌", "▀", "▐", "▄"}, - 63: {"⊶", "⊷"}, - 64: {"▪", "▫"}, - 65: {"□", "■"}, - 66: {"▮", "▯"}, - 67: {"-", "=", "≡"}, - 68: {"d", "q", "p", "b"}, - 69: {"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}, - 70: {"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "}, - 71: {"☗", "☖"}, - 72: {"⧇", "⧆"}, - 73: {"◉", "◎"}, - 74: {"㊂", "㊀", "㊁"}, - 75: {"⦾", "⦿"}, -} diff --git a/server/download.go b/server/download.go index 13e8ee18..64ee5623 100644 --- a/server/download.go +++ b/server/download.go @@ -285,7 +285,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse)) } fn(api.ProgressResponse{ - Status: fmt.Sprintf("downloading %s", b.Digest), + Status: fmt.Sprintf("downloading %s", b.Digest[7:19]), Digest: b.Digest, Total: b.Total, Completed: b.Completed.Load(), @@ -322,7 +322,7 @@ func downloadBlob(ctx context.Context, opts downloadOpts) error { return err default: opts.fn(api.ProgressResponse{ - Status: fmt.Sprintf("downloading %s", opts.digest), + Status: fmt.Sprintf("downloading %s", opts.digest[7:19]), Digest: opts.digest, Total: fi.Size(), Completed: fi.Size(), diff --git a/server/upload.go b/server/upload.go index 306b6e63..19b0af91 100644 --- a/server/upload.go +++ b/server/upload.go @@ -301,7 +301,7 @@ func (b *blobUpload) Wait(ctx context.Context, fn func(api.ProgressResponse)) er } fn(api.ProgressResponse{ - Status: fmt.Sprintf("uploading %s", b.Digest), + Status: fmt.Sprintf("uploading %s", b.Digest[7:19]), Digest: b.Digest, Total: b.Total, Completed: b.Completed.Load(), @@ -352,7 +352,7 @@ func uploadBlob(ctx context.Context, mp ModelPath, layer *Layer, opts *RegistryO default: defer resp.Body.Close() fn(api.ProgressResponse{ - Status: fmt.Sprintf("uploading %s", layer.Digest), + Status: fmt.Sprintf("uploading %s", layer.Digest[7:19]), Digest: layer.Digest, Total: layer.Size, Completed: layer.Size,