tty.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package progress
  14. import (
  15. "context"
  16. "fmt"
  17. "io"
  18. "runtime"
  19. "strings"
  20. "sync"
  21. "time"
  22. "github.com/buger/goterm"
  23. "github.com/morikuni/aec"
  24. )
  25. type ttyWriter struct {
  26. out io.Writer
  27. events map[string]Event
  28. eventIDs []string
  29. repeated bool
  30. numLines int
  31. done chan bool
  32. mtx *sync.RWMutex
  33. }
  34. func (w *ttyWriter) Start(ctx context.Context) error {
  35. ticker := time.NewTicker(100 * time.Millisecond)
  36. for {
  37. select {
  38. case <-ctx.Done():
  39. w.print()
  40. return ctx.Err()
  41. case <-w.done:
  42. w.print()
  43. return nil
  44. case <-ticker.C:
  45. w.print()
  46. }
  47. }
  48. }
  49. func (w *ttyWriter) Stop() {
  50. w.done <- true
  51. }
  52. func (w *ttyWriter) Event(e Event) {
  53. w.mtx.Lock()
  54. defer w.mtx.Unlock()
  55. if !contains(w.eventIDs, e.ID) {
  56. w.eventIDs = append(w.eventIDs, e.ID)
  57. }
  58. if _, ok := w.events[e.ID]; ok {
  59. last := w.events[e.ID]
  60. switch e.Status {
  61. case Done, Error:
  62. if last.Status != e.Status {
  63. last.stop()
  64. }
  65. }
  66. last.Status = e.Status
  67. last.Text = e.Text
  68. last.StatusText = e.StatusText
  69. w.events[e.ID] = last
  70. } else {
  71. e.startTime = time.Now()
  72. e.spinner = newSpinner()
  73. w.events[e.ID] = e
  74. }
  75. }
  76. func (w *ttyWriter) print() {
  77. w.mtx.Lock()
  78. defer w.mtx.Unlock()
  79. if len(w.eventIDs) == 0 {
  80. return
  81. }
  82. terminalWidth := goterm.Width()
  83. b := aec.EmptyBuilder
  84. for i := 0; i <= w.numLines; i++ {
  85. b = b.Up(1)
  86. }
  87. if !w.repeated {
  88. b = b.Down(1)
  89. }
  90. w.repeated = true
  91. fmt.Fprint(w.out, b.Column(0).ANSI)
  92. // Hide the cursor while we are printing
  93. fmt.Fprint(w.out, aec.Hide)
  94. defer fmt.Fprint(w.out, aec.Show)
  95. firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
  96. if w.numLines != 0 && numDone(w.events) == w.numLines {
  97. firstLine = aec.Apply(firstLine, aec.BlueF)
  98. }
  99. fmt.Fprintln(w.out, firstLine)
  100. var statusPadding int
  101. for _, v := range w.eventIDs {
  102. l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
  103. if statusPadding < l {
  104. statusPadding = l
  105. }
  106. }
  107. numLines := 0
  108. for _, v := range w.eventIDs {
  109. line := lineText(w.events[v], terminalWidth, statusPadding, runtime.GOOS != "windows")
  110. // nolint: errcheck
  111. fmt.Fprint(w.out, line)
  112. numLines++
  113. }
  114. w.numLines = numLines
  115. }
  116. func lineText(event Event, terminalWidth, statusPadding int, color bool) string {
  117. endTime := time.Now()
  118. if event.Status != Working {
  119. endTime = event.endTime
  120. }
  121. elapsed := endTime.Sub(event.startTime).Seconds()
  122. textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
  123. padding := statusPadding - textLen
  124. if padding < 0 {
  125. padding = 0
  126. }
  127. text := fmt.Sprintf(" %s %s %s%s %s",
  128. event.spinner.String(),
  129. event.ID,
  130. event.Text,
  131. strings.Repeat(" ", padding),
  132. event.StatusText,
  133. )
  134. timer := fmt.Sprintf("%.1fs\n", elapsed)
  135. o := align(text, timer, terminalWidth)
  136. if color {
  137. color := aec.WhiteF
  138. if event.Status == Done {
  139. color = aec.BlueF
  140. }
  141. if event.Status == Error {
  142. color = aec.RedF
  143. }
  144. return aec.Apply(o, color)
  145. }
  146. return o
  147. }
  148. func numDone(events map[string]Event) int {
  149. i := 0
  150. for _, e := range events {
  151. if e.Status == Done {
  152. i++
  153. }
  154. }
  155. return i
  156. }
  157. func align(l, r string, w int) string {
  158. return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
  159. }
  160. func contains(ar []string, needle string) bool {
  161. for _, v := range ar {
  162. if needle == v {
  163. return true
  164. }
  165. }
  166. return false
  167. }