|  | @@ -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
 | 
	
		
			
				|  |  |  }
 |