|  | // Copyright 2023 Google LLC | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //   https://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | // Package progressbar provides functions for drawing unicode progress bars to | 
|  | // the terminal | 
|  | package progressbar | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "fmt" | 
|  | "io" | 
|  | "math" | 
|  | "strings" | 
|  | "time" | 
|  | ) | 
|  |  | 
|  | // 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 returns a string with an ANSI-colored progress bar, providing | 
|  | // realtime information about the status of the CTS run. | 
|  | // Note: We'll want to skip this if !isatty or if we're running on windows. | 
|  | type ProgressBar struct { | 
|  | Config | 
|  | out io.Writer | 
|  | c   chan Status | 
|  | } | 
|  |  | 
|  | // New returns a new ProgressBar that streams output to out. | 
|  | // Call ProgressBar.Stop() once finished. | 
|  | func New(out io.Writer, cfg *Config) *ProgressBar { | 
|  | p := &ProgressBar{out: out, c: make(chan Status)} | 
|  | 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 | 
|  | } | 
|  |  | 
|  | // Update updates the ProgressBar with the given status | 
|  | func (p *ProgressBar) Update(s Status) { | 
|  | p.c <- s | 
|  | } | 
|  |  | 
|  | // Stop stops drawing the progress bar. | 
|  | // Once called, the ProgressBar must not be used. | 
|  | func (p *ProgressBar) 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, '%') | 
|  | } |