tty.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package progress
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "runtime"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/buger/goterm"
  11. "github.com/morikuni/aec"
  12. )
  13. type ttyWriter struct {
  14. out io.Writer
  15. events map[string]Event
  16. eventIDs []string
  17. repeated bool
  18. numLines int
  19. done chan bool
  20. mtx *sync.RWMutex
  21. }
  22. func (w *ttyWriter) Start(ctx context.Context) error {
  23. ticker := time.NewTicker(100 * time.Millisecond)
  24. for {
  25. select {
  26. case <-ctx.Done():
  27. w.print()
  28. return ctx.Err()
  29. case <-w.done:
  30. w.print()
  31. return nil
  32. case <-ticker.C:
  33. w.print()
  34. }
  35. }
  36. }
  37. func (w *ttyWriter) Stop() {
  38. w.done <- true
  39. }
  40. func (w *ttyWriter) Event(e Event) {
  41. w.mtx.Lock()
  42. defer w.mtx.Unlock()
  43. if !StringContains(w.eventIDs, e.ID) {
  44. w.eventIDs = append(w.eventIDs, e.ID)
  45. }
  46. if _, ok := w.events[e.ID]; ok {
  47. last := w.events[e.ID]
  48. switch e.Status {
  49. case Done, Error:
  50. if last.Status != e.Status {
  51. last.stop()
  52. }
  53. }
  54. last.Status = e.Status
  55. last.Text = e.Text
  56. last.StatusText = e.StatusText
  57. w.events[e.ID] = last
  58. } else {
  59. e.startTime = time.Now()
  60. e.spinner = newSpinner()
  61. w.events[e.ID] = e
  62. }
  63. }
  64. func (w *ttyWriter) print() {
  65. w.mtx.Lock()
  66. defer w.mtx.Unlock()
  67. if len(w.eventIDs) == 0 {
  68. return
  69. }
  70. terminalWidth := goterm.Width()
  71. b := aec.EmptyBuilder
  72. for i := 0; i <= w.numLines; i++ {
  73. b = b.Up(1)
  74. }
  75. if !w.repeated {
  76. b = b.Down(1)
  77. }
  78. w.repeated = true
  79. fmt.Fprint(w.out, b.Column(0).ANSI)
  80. // Hide the cursor while we are printing
  81. fmt.Fprint(w.out, aec.Hide)
  82. defer fmt.Fprint(w.out, aec.Show)
  83. firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
  84. if w.numLines != 0 && numDone(w.events) == w.numLines {
  85. firstLine = aec.Apply(firstLine, aec.BlueF)
  86. }
  87. fmt.Fprintln(w.out, firstLine)
  88. var statusPadding int
  89. for _, v := range w.eventIDs {
  90. l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
  91. if statusPadding < l {
  92. statusPadding = l
  93. }
  94. }
  95. numLines := 0
  96. for _, v := range w.eventIDs {
  97. line := lineText(w.events[v], terminalWidth, statusPadding, runtime.GOOS != "windows")
  98. // nolint: errcheck
  99. fmt.Fprint(w.out, line)
  100. numLines++
  101. }
  102. w.numLines = numLines
  103. }
  104. func lineText(event Event, terminalWidth, statusPadding int, color bool) string {
  105. endTime := time.Now()
  106. if event.Status != Working {
  107. endTime = event.endTime
  108. }
  109. elapsed := endTime.Sub(event.startTime).Seconds()
  110. textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
  111. padding := statusPadding - textLen
  112. if padding < 0 {
  113. padding = 0
  114. }
  115. text := fmt.Sprintf(" %s %s %s%s %s",
  116. event.spinner.String(),
  117. event.ID,
  118. event.Text,
  119. strings.Repeat(" ", padding),
  120. event.StatusText,
  121. )
  122. timer := fmt.Sprintf("%.1fs\n", elapsed)
  123. o := align(text, timer, terminalWidth)
  124. if color {
  125. color := aec.WhiteF
  126. if event.Status == Done {
  127. color = aec.BlueF
  128. }
  129. if event.Status == Error {
  130. color = aec.RedF
  131. }
  132. return aec.Apply(o, color)
  133. }
  134. return o
  135. }
  136. func numDone(events map[string]Event) int {
  137. i := 0
  138. for _, e := range events {
  139. if e.Status == Done {
  140. i++
  141. }
  142. }
  143. return i
  144. }
  145. func align(l, r string, w int) string {
  146. return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
  147. }
  148. // StringContains check if an array contains a specific value
  149. func StringContains(array []string, needle string) bool {
  150. for _, val := range array {
  151. if val == needle {
  152. return true
  153. }
  154. }
  155. return false
  156. }