From 5b31403061aee84871621472bd78704210a5a219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Filast=C3=B2?= Date: Fri, 22 Jun 2018 13:56:42 +0200 Subject: [PATCH] Implement progressbar for web_connectivity test --- internal/log/handlers/cli/cli.go | 37 +++-- .../log/handlers/cli/progress/progress.go | 126 ++++++++++++++++++ 2 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 internal/log/handlers/cli/progress/progress.go diff --git a/internal/log/handlers/cli/cli.go b/internal/log/handlers/cli/cli.go index 1b87655..7985fec 100644 --- a/internal/log/handlers/cli/cli.go +++ b/internal/log/handlers/cli/cli.go @@ -7,10 +7,12 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/apex/log" "github.com/fatih/color" colorable "github.com/mattn/go-colorable" + "github.com/ooni/probe-cli/internal/log/handlers/cli/progress" ) // Default handler outputting to stderr. @@ -71,14 +73,21 @@ func logSectionTitle(w io.Writer, f log.Fields) error { return nil } +var bar *progress.Bar +var lastBarChars int64 + // TypedLog is used for handling special "typed" logs to the CLI func (h *Handler) TypedLog(t string, e *log.Entry) error { switch t { case "progress": - // XXX replace this with something more fancy like https://github.com/tj/go-progress - fmt.Fprintf(h.Writer, "%.1f%% [%s]: %s", e.Fields.Get("percentage").(float64)*100, e.Fields.Get("key"), e.Message) - fmt.Fprintln(h.Writer) - return nil + var err error + if bar == nil { + bar = progress.New(1.0) + } + bar.Value(e.Fields.Get("percentage").(float64)) + bar.Text(e.Message) + lastBarChars, err = bar.WriteTo(h.Writer) + return err case "result_item": return logResultItem(h.Writer, e.Fields) case "result_summary": @@ -96,16 +105,28 @@ func (h *Handler) DefaultLog(e *log.Entry) error { level := Strings[e.Level] names := e.Fields.Names() - color.Fprintf(h.Writer, "%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message) - + s := color.Sprintf("%s %-25s", bold.Sprintf("%*s", h.Padding+1, level), e.Message) for _, name := range names { if name == "source" { continue } - fmt.Fprintf(h.Writer, " %s=%s", color.Sprint(name), e.Fields.Get(name)) + s += fmt.Sprintf(" %s=%s", color.Sprint(name), e.Fields.Get(name)) } - fmt.Fprintln(h.Writer) + if bar != nil { + // We need to move the cursor back to the begging of the line and add some + // padding to the end of the string to delete the previous line written to + // the console. + sChars := int64(utf8.RuneCountInString(s)) + fmt.Fprintf(h.Writer, + fmt.Sprintf("\r%s%s", s, strings.Repeat(" ", int(lastBarChars-sChars))), + ) + fmt.Fprintln(h.Writer) + bar.WriteTo(h.Writer) + } else { + fmt.Fprintf(h.Writer, s) + fmt.Fprintln(h.Writer) + } return nil } diff --git a/internal/log/handlers/cli/progress/progress.go b/internal/log/handlers/cli/progress/progress.go new file mode 100644 index 0000000..50887a5 --- /dev/null +++ b/internal/log/handlers/cli/progress/progress.go @@ -0,0 +1,126 @@ +// Package progress provides a simple terminal progress bar. +package progress + +import ( + "bytes" + "fmt" + "html/template" + "io" + "math" + "strings" +) + +// Bar is a progress bar. +type Bar struct { + StartDelimiter string // StartDelimiter for the bar ("|"). + EndDelimiter string // EndDelimiter for the bar ("|"). + Filled string // Filled section representation ("█"). + Empty string // Empty section representation ("░") + Total float64 // Total value. + Width int // Width of the bar. + + value float64 + tmpl *template.Template + text string +} + +// New returns a new bar with the given total. +func New(total float64) *Bar { + b := &Bar{ + StartDelimiter: "|", + EndDelimiter: "|", + Filled: "█", + Empty: "░", + Total: total, + Width: 60, + } + + b.Template(`{{.Percent | printf "%3.0f"}}% {{.Bar}} {{.Text}}`) + + return b +} + +// NewInt returns a new bar with the given total. +func NewInt(total int) *Bar { + return New(float64(total)) +} + +// Text sets the text value. +func (b *Bar) Text(s string) { + b.text = s +} + +// Value sets the value. +func (b *Bar) Value(n float64) { + if n > b.Total { + panic("Bar update value cannot be greater than the total") + } + b.value = n +} + +// ValueInt sets the value. +func (b *Bar) ValueInt(n int) { + b.Value(float64(n)) +} + +// Percent returns the percentage +func (b *Bar) percent() float64 { + return (b.value / b.Total) * 100 +} + +// Bar returns the progress bar string. +func (b *Bar) bar() string { + p := b.value / b.Total + filled := math.Ceil(float64(b.Width) * p) + empty := math.Floor(float64(b.Width) - filled) + s := b.StartDelimiter + s += strings.Repeat(b.Filled, int(filled)) + s += strings.Repeat(b.Empty, int(empty)) + s += b.EndDelimiter + return s +} + +// String returns the progress bar. +func (b *Bar) String() string { + var buf bytes.Buffer + + data := struct { + Value float64 + Total float64 + Percent float64 + StartDelimiter string + EndDelimiter string + Bar string + Text string + }{ + Value: b.value, + Text: b.text, + StartDelimiter: b.StartDelimiter, + EndDelimiter: b.EndDelimiter, + Percent: b.percent(), + Bar: b.bar(), + } + + if err := b.tmpl.Execute(&buf, data); err != nil { + panic(err) + } + + return buf.String() +} + +// WriteTo writes the progress bar to w. +func (b *Bar) WriteTo(w io.Writer) (int64, error) { + s := fmt.Sprintf("\r %s ", b.String()) + _, err := io.WriteString(w, s) + return int64(len(s)), err +} + +// Template for rendering. This method will panic if the template fails to parse. +func (b *Bar) Template(s string) { + t, err := template.New("").Parse(s) + if err != nil { + panic(err) + } + + b.tmpl = t +}