|
|
@@ -2,14 +2,11 @@ package progress
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
- "fmt"
|
|
|
- "io"
|
|
|
- "strings"
|
|
|
"sync"
|
|
|
"time"
|
|
|
|
|
|
- "github.com/buger/goterm"
|
|
|
- "github.com/morikuni/aec"
|
|
|
+ "github.com/containerd/console"
|
|
|
+ "github.com/moby/term"
|
|
|
)
|
|
|
|
|
|
// EventStatus indicates the status of an action
|
|
|
@@ -49,16 +46,6 @@ type Writer interface {
|
|
|
Event(Event)
|
|
|
}
|
|
|
|
|
|
-type writer struct {
|
|
|
- out io.Writer
|
|
|
- events map[string]Event
|
|
|
- eventIDs []string
|
|
|
- repeated bool
|
|
|
- numLines int
|
|
|
- done chan bool
|
|
|
- mtx *sync.RWMutex
|
|
|
-}
|
|
|
-
|
|
|
type writerKey struct{}
|
|
|
|
|
|
// WithContextWriter adds the writer to the context
|
|
|
@@ -73,155 +60,27 @@ func ContextWriter(ctx context.Context) Writer {
|
|
|
}
|
|
|
|
|
|
// NewWriter returns a new multi-progress writer
|
|
|
-func NewWriter(out io.Writer) Writer {
|
|
|
- return &writer{
|
|
|
- out: out,
|
|
|
- eventIDs: []string{},
|
|
|
- events: map[string]Event{},
|
|
|
- repeated: false,
|
|
|
- done: make(chan bool),
|
|
|
- mtx: &sync.RWMutex{},
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (w *writer) Start(ctx context.Context) error {
|
|
|
- ticker := time.NewTicker(100 * time.Millisecond)
|
|
|
-
|
|
|
- for {
|
|
|
- select {
|
|
|
- case <-ctx.Done():
|
|
|
- return ctx.Err()
|
|
|
- case <-w.done:
|
|
|
- w.print()
|
|
|
- return nil
|
|
|
- case <-ticker.C:
|
|
|
- w.print()
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func (w *writer) Stop() {
|
|
|
- w.done <- true
|
|
|
-}
|
|
|
+func NewWriter(out console.File) (Writer, error) {
|
|
|
+ _, isTerminal := term.GetFdInfo(out)
|
|
|
|
|
|
-func (w *writer) Event(e Event) {
|
|
|
- w.mtx.Lock()
|
|
|
- defer w.mtx.Unlock()
|
|
|
- if !contains(w.eventIDs, e.ID) {
|
|
|
- w.eventIDs = append(w.eventIDs, e.ID)
|
|
|
- }
|
|
|
- if _, ok := w.events[e.ID]; ok {
|
|
|
- event := w.events[e.ID]
|
|
|
- if event.Status != Done && e.Status == Done {
|
|
|
- event.stop()
|
|
|
+ if isTerminal {
|
|
|
+ con, err := console.ConsoleFromFile(out)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
}
|
|
|
- event.Status = e.Status
|
|
|
- event.Text = e.Text
|
|
|
- event.StatusText = e.StatusText
|
|
|
- w.events[e.ID] = event
|
|
|
- } else {
|
|
|
- e.startTime = time.Now()
|
|
|
- e.spinner = newSpinner()
|
|
|
- w.events[e.ID] = e
|
|
|
- }
|
|
|
-}
|
|
|
|
|
|
-func (w *writer) print() {
|
|
|
- w.mtx.Lock()
|
|
|
- defer w.mtx.Unlock()
|
|
|
- terminalWidth := goterm.Width()
|
|
|
- b := aec.EmptyBuilder
|
|
|
- for i := 0; i <= w.numLines; i++ {
|
|
|
- b = b.Up(1)
|
|
|
+ return &ttyWriter{
|
|
|
+ out: con,
|
|
|
+ eventIDs: []string{},
|
|
|
+ events: map[string]Event{},
|
|
|
+ repeated: false,
|
|
|
+ done: make(chan bool),
|
|
|
+ mtx: &sync.RWMutex{},
|
|
|
+ }, nil
|
|
|
}
|
|
|
- if !w.repeated {
|
|
|
- b = b.Down(1)
|
|
|
- }
|
|
|
- w.repeated = true
|
|
|
- fmt.Fprint(w.out, b.Column(0).ANSI)
|
|
|
-
|
|
|
- // Hide the cursor while we are printing
|
|
|
- fmt.Fprint(w.out, aec.Hide)
|
|
|
- defer fmt.Fprint(w.out, aec.Show)
|
|
|
|
|
|
- firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
|
|
|
- if w.numLines != 0 && numDone(w.events) == w.numLines {
|
|
|
- firstLine = aec.Apply(firstLine, aec.BlueF)
|
|
|
- }
|
|
|
- fmt.Fprintln(w.out, firstLine)
|
|
|
-
|
|
|
- var statusPadding int
|
|
|
- for _, v := range w.eventIDs {
|
|
|
- l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
|
|
|
- if statusPadding < l {
|
|
|
- statusPadding = l
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- numLines := 0
|
|
|
- for _, v := range w.eventIDs {
|
|
|
- line := lineText(w.events[v], terminalWidth, statusPadding)
|
|
|
- // nolint: errcheck
|
|
|
- fmt.Fprint(w.out, line)
|
|
|
- numLines++
|
|
|
- }
|
|
|
-
|
|
|
- w.numLines = numLines
|
|
|
-}
|
|
|
-
|
|
|
-func lineText(event Event, terminalWidth, statusPadding int) string {
|
|
|
- endTime := time.Now()
|
|
|
- if event.Status != Working {
|
|
|
- endTime = event.endTime
|
|
|
- }
|
|
|
-
|
|
|
- elapsed := endTime.Sub(event.startTime).Seconds()
|
|
|
-
|
|
|
- textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
|
|
|
- padding := statusPadding - textLen
|
|
|
- if padding < 0 {
|
|
|
- padding = 0
|
|
|
- }
|
|
|
- text := fmt.Sprintf(" %s %s %s%s %s",
|
|
|
- event.spinner.String(),
|
|
|
- event.ID,
|
|
|
- event.Text,
|
|
|
- strings.Repeat(" ", padding),
|
|
|
- event.StatusText,
|
|
|
- )
|
|
|
- timer := fmt.Sprintf("%.1fs\n", elapsed)
|
|
|
- o := align(text, timer, terminalWidth)
|
|
|
-
|
|
|
- color := aec.WhiteF
|
|
|
- if event.Status == Done {
|
|
|
- color = aec.BlueF
|
|
|
- }
|
|
|
- if event.Status == Error {
|
|
|
- color = aec.RedF
|
|
|
- }
|
|
|
-
|
|
|
- return aec.Apply(o, color)
|
|
|
-}
|
|
|
-
|
|
|
-func numDone(events map[string]Event) int {
|
|
|
- i := 0
|
|
|
- for _, e := range events {
|
|
|
- if e.Status == Done {
|
|
|
- i++
|
|
|
- }
|
|
|
- }
|
|
|
- return i
|
|
|
-}
|
|
|
-
|
|
|
-func align(l, r string, w int) string {
|
|
|
- return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
|
|
|
-}
|
|
|
-
|
|
|
-func contains(ar []string, needle string) bool {
|
|
|
- for _, v := range ar {
|
|
|
- if needle == v {
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
- return false
|
|
|
+ return &plainWriter{
|
|
|
+ out: out,
|
|
|
+ done: make(chan bool),
|
|
|
+ }, nil
|
|
|
}
|