| // Copyright 2023 Google LLC | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. 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. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // 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. | |
| // Package progressbar provides functions for drawing Unicode progress bars to | |
| // the terminal | |
| package progressbar | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "math" | |
| "strings" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/term" | |
| ) | |
| // Defaults for the Config | |
| const ( | |
| DefaultRefreshRate = time.Millisecond * 100 | |
| DefaultWidth = 50 | |
| DefaultANSIColors = true | |
| ) | |
| // Config holds configuration options for a ProgressBar | |
| type Config struct { | |
| RefreshRate time.Duration | |
| Width int | |
| ANSIColors bool | |
| } | |
| // Color is an enumerator of colors | |
| type Color int | |
| // Color enumerators | |
| const ( | |
| White Color = iota | |
| Red | |
| Green | |
| Yellow | |
| Blue | |
| Magenta | |
| Cyan | |
| ) | |
| // Segment describes a single segment of the ProgressBar | |
| type Segment struct { | |
| Count int | |
| Color Color | |
| Transparent bool | |
| Bold bool | |
| } | |
| // Status holds the updated data of the ProgressBar | |
| type Status struct { | |
| Total int | |
| Segments []Segment | |
| } | |
| // ProgressBar is an interface for progress bars. | |
| type ProgressBar interface { | |
| // Update updates the ProgressBar with the given status | |
| Update(s Status) | |
| // Stop stops drawing the progress bar. | |
| // Once called, the ProgressBar must not be used. | |
| Stop() | |
| } | |
| type Build func(out io.Writer, cfg *Config) ProgressBar | |
| // New returns a new ProgressBar that streams output to out. | |
| // Selects between realProgressBar and StubProgressBar automatically depending | |
| // on if the output will be displayed in the terminal correctly. | |
| // Call ProgressBar.Stop() once finished. | |
| func New(out io.Writer, cfg *Config) ProgressBar { | |
| if !term.CanUseAnsiEscapeSequences() { | |
| return NewStubProgressBar(out, cfg) | |
| } | |
| return newRealProgressBar(out, cfg) | |
| } | |
| // NewRealProgressBar constructs and returns a realProgressBar. | |
| // Does not check if the terminal supports the needed escape characters, which | |
| // is why it is not exposed outside the package, use New() instead. | |
| func newRealProgressBar(out io.Writer, cfg *Config) ProgressBar { | |
| p := &realProgressBar{out: out, c: make(chan Status, 64)} | |
| if cfg != nil { | |
| p.Config = *cfg | |
| } else { | |
| p.ANSIColors = DefaultANSIColors | |
| } | |
| if p.RefreshRate == 0 { | |
| p.RefreshRate = DefaultRefreshRate | |
| } | |
| if p.Width == 0 { | |
| p.Width = DefaultWidth | |
| } | |
| go func() { | |
| var status *Status | |
| t := time.NewTicker(p.RefreshRate) | |
| defer t.Stop() | |
| for frame := 0; ; frame++ { | |
| select { | |
| case s, ok := <-p.c: | |
| if !ok { | |
| return | |
| } | |
| status = &s | |
| case <-t.C: | |
| if status != nil { | |
| status.Draw(out, p.Width, p.ANSIColors, frame) | |
| } | |
| } | |
| } | |
| }() | |
| return p | |
| } | |
| // NewStubProgressBar returns a new StubProgressBar, used in tests to guarantee | |
| // the type of ProgressBar returned. | |
| // Note: In production code, use New() instead. | |
| func NewStubProgressBar(_ io.Writer, _ *Config) ProgressBar { | |
| return &StubProgressBar{} | |
| } | |
| // StubProgressBar is a ProgressBar that does nothing, used in environments that | |
| // don't support the control characters needed or when testing to avoid spamming | |
| // output. | |
| type StubProgressBar struct{} | |
| func (p *StubProgressBar) Update(s Status) {} | |
| func (p *StubProgressBar) Stop() {} | |
| // realProgressBar writes out a string with an ANSI-colored progress bar, | |
| // providing realtime information about the status of the CTS run. | |
| // Note: Does not work if !isatty or if running on windows. | |
| type realProgressBar struct { | |
| Config | |
| out io.Writer | |
| c chan Status | |
| } | |
| func (p *realProgressBar) Update(s Status) { | |
| p.c <- s | |
| } | |
| func (p *realProgressBar) Stop() { | |
| close(p.c) | |
| } | |
| // Draw draws the ProgressBar status to out | |
| func (s Status) Draw(out io.Writer, width int, ansiColors bool, animFrame int) { | |
| // ANSI escape sequences | |
| const ( | |
| escape = "\u001B[" | |
| positionLeft = escape + "0G" | |
| ansiReset = escape + "0m" | |
| bold = escape + "1m" | |
| red = escape + "31m" | |
| green = escape + "32m" | |
| yellow = escape + "33m" | |
| blue = escape + "34m" | |
| magenta = escape + "35m" | |
| cyan = escape + "36m" | |
| white = escape + "37m" | |
| ) | |
| animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} | |
| blockSymbols := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'} | |
| numBlocksPrinted := 0 | |
| buf := &bytes.Buffer{} | |
| fmt.Fprint(buf, " ", string(animSymbols[animFrame%len(animSymbols)]), " [") | |
| numFinished := 0 | |
| for _, seg := range s.Segments { | |
| if ansiColors { | |
| switch seg.Color { | |
| case Red: | |
| buf.WriteString(red) | |
| case Green: | |
| buf.WriteString(green) | |
| case Yellow: | |
| buf.WriteString(yellow) | |
| case Blue: | |
| buf.WriteString(blue) | |
| case Magenta: | |
| buf.WriteString(magenta) | |
| case Cyan: | |
| buf.WriteString(cyan) | |
| default: | |
| buf.WriteString(white) | |
| } | |
| if seg.Bold { | |
| buf.WriteString(bold) | |
| } | |
| } | |
| numFinished += seg.Count | |
| statusFrac := float64(seg.Count) / float64(s.Total) | |
| fNumBlocks := float64(width) * statusFrac | |
| numBlocks := int(math.Ceil(fNumBlocks)) | |
| if seg.Transparent { | |
| if numBlocks > 0 { | |
| fmt.Fprint(buf, strings.Repeat(string("░"), numBlocks)) | |
| } | |
| } else { | |
| if numBlocks > 1 { | |
| fmt.Fprint(buf, strings.Repeat(string("▉"), numBlocks-1)) | |
| } | |
| if numBlocks > 0 { | |
| frac := fNumBlocks - float64(numBlocks-1) | |
| symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))] | |
| fmt.Fprint(buf, string(symbol)) | |
| } | |
| } | |
| numBlocksPrinted += numBlocks | |
| } | |
| if width > numBlocksPrinted { | |
| fmt.Fprint(buf, strings.Repeat(string(" "), width-numBlocksPrinted)) | |
| } | |
| fmt.Fprint(buf, ansiReset) | |
| fmt.Fprint(buf, "] ", percentage(numFinished, s.Total)) | |
| if ansiColors { | |
| // move cursor to start of line so the bar is overridden next print | |
| fmt.Fprint(buf, positionLeft) | |
| } else { | |
| // cannot move cursor, so newline | |
| fmt.Fprintln(buf) | |
| } | |
| out.Write(buf.Bytes()) | |
| } | |
| // percentage returns the percentage of n out of total as a string | |
| func percentage(n, total int) string { | |
| if total == 0 { | |
| return "-" | |
| } | |
| f := float64(n) / float64(total) | |
| return fmt.Sprintf("%.1f%c", f*100.0, '%') | |
| } |