tty.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  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/docker/compose/v2/pkg/api"
  23. "github.com/docker/compose/v2/pkg/utils"
  24. "github.com/buger/goterm"
  25. "github.com/morikuni/aec"
  26. )
  27. type ttyWriter struct {
  28. out io.Writer
  29. events map[string]Event
  30. eventIDs []string
  31. repeated bool
  32. numLines int
  33. done chan bool
  34. mtx *sync.Mutex
  35. tailEvents []string
  36. dryRun bool
  37. }
  38. func (w *ttyWriter) Start(ctx context.Context) error {
  39. ticker := time.NewTicker(100 * time.Millisecond)
  40. defer ticker.Stop()
  41. for {
  42. select {
  43. case <-ctx.Done():
  44. w.print()
  45. w.printTailEvents()
  46. return ctx.Err()
  47. case <-w.done:
  48. w.print()
  49. w.printTailEvents()
  50. return nil
  51. case <-ticker.C:
  52. w.print()
  53. }
  54. }
  55. }
  56. func (w *ttyWriter) Stop() {
  57. w.done <- true
  58. }
  59. func (w *ttyWriter) Event(e Event) {
  60. w.mtx.Lock()
  61. defer w.mtx.Unlock()
  62. if !utils.StringContains(w.eventIDs, e.ID) {
  63. w.eventIDs = append(w.eventIDs, e.ID)
  64. }
  65. if _, ok := w.events[e.ID]; ok {
  66. last := w.events[e.ID]
  67. switch e.Status {
  68. case Done, Error, Warning:
  69. if last.Status != e.Status {
  70. last.stop()
  71. }
  72. }
  73. last.Status = e.Status
  74. last.Text = e.Text
  75. last.StatusText = e.StatusText
  76. // allow set/unset of parent, but not swapping otherwise prompt is flickering
  77. if last.ParentID == "" || e.ParentID == "" {
  78. last.ParentID = e.ParentID
  79. }
  80. w.events[e.ID] = last
  81. } else {
  82. e.startTime = time.Now()
  83. e.spinner = newSpinner()
  84. if e.Status == Done || e.Status == Error {
  85. e.stop()
  86. }
  87. w.events[e.ID] = e
  88. }
  89. }
  90. func (w *ttyWriter) Events(events []Event) {
  91. for _, e := range events {
  92. w.Event(e)
  93. }
  94. }
  95. func (w *ttyWriter) TailMsgf(msg string, args ...interface{}) {
  96. w.mtx.Lock()
  97. defer w.mtx.Unlock()
  98. msgWithPrefix := msg
  99. if w.dryRun {
  100. msgWithPrefix = strings.TrimSpace(api.DRYRUN_PREFIX + msg)
  101. }
  102. w.tailEvents = append(w.tailEvents, fmt.Sprintf(msgWithPrefix, args...))
  103. }
  104. func (w *ttyWriter) printTailEvents() {
  105. w.mtx.Lock()
  106. defer w.mtx.Unlock()
  107. for _, msg := range w.tailEvents {
  108. fmt.Fprintln(w.out, msg)
  109. }
  110. }
  111. func (w *ttyWriter) print() { //nolint:gocyclo
  112. w.mtx.Lock()
  113. defer w.mtx.Unlock()
  114. if len(w.eventIDs) == 0 {
  115. return
  116. }
  117. terminalWidth := goterm.Width()
  118. b := aec.EmptyBuilder
  119. for i := 0; i <= w.numLines; i++ {
  120. b = b.Up(1)
  121. }
  122. if !w.repeated {
  123. b = b.Down(1)
  124. }
  125. w.repeated = true
  126. fmt.Fprint(w.out, b.Column(0).ANSI)
  127. // Hide the cursor while we are printing
  128. fmt.Fprint(w.out, aec.Hide)
  129. defer fmt.Fprint(w.out, aec.Show)
  130. firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
  131. if w.numLines != 0 && numDone(w.events) == w.numLines {
  132. firstLine = aec.Apply(firstLine, aec.BlueF)
  133. }
  134. fmt.Fprintln(w.out, firstLine)
  135. var statusPadding int
  136. for _, v := range w.eventIDs {
  137. event := w.events[v]
  138. l := len(fmt.Sprintf("%s %s", event.ID, event.Text))
  139. if statusPadding < l {
  140. statusPadding = l
  141. }
  142. if event.ParentID != "" {
  143. statusPadding -= 2
  144. }
  145. }
  146. skipChildEvents := false
  147. if len(w.eventIDs) > goterm.Height()-2 {
  148. skipChildEvents = true
  149. }
  150. numLines := 0
  151. for _, v := range w.eventIDs {
  152. event := w.events[v]
  153. if event.ParentID != "" {
  154. continue
  155. }
  156. line := lineText(event, "", terminalWidth, statusPadding, runtime.GOOS != "windows", w.dryRun)
  157. fmt.Fprint(w.out, line)
  158. numLines++
  159. for _, v := range w.eventIDs {
  160. ev := w.events[v]
  161. if ev.ParentID == event.ID {
  162. if skipChildEvents {
  163. continue
  164. }
  165. line := lineText(ev, " ", terminalWidth, statusPadding, runtime.GOOS != "windows", w.dryRun)
  166. fmt.Fprint(w.out, line)
  167. numLines++
  168. }
  169. }
  170. }
  171. for i := numLines; i < w.numLines; i++ {
  172. if numLines < goterm.Height()-2 {
  173. fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
  174. numLines++
  175. }
  176. }
  177. w.numLines = numLines
  178. }
  179. func lineText(event Event, pad string, terminalWidth, statusPadding int, color bool, dryRun bool) string {
  180. endTime := time.Now()
  181. if event.Status != Working {
  182. endTime = event.startTime
  183. if (event.endTime != time.Time{}) {
  184. endTime = event.endTime
  185. }
  186. }
  187. prefix := ""
  188. if dryRun {
  189. prefix = api.DRYRUN_PREFIX
  190. }
  191. elapsed := endTime.Sub(event.startTime).Seconds()
  192. textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
  193. padding := statusPadding - textLen
  194. if padding < 0 {
  195. padding = 0
  196. }
  197. // calculate the max length for the status text, on errors it
  198. // is 2-3 lines long and breaks the line formatting
  199. maxStatusLen := terminalWidth - textLen - statusPadding - 15
  200. status := event.StatusText
  201. // in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
  202. if maxStatusLen > 0 && len(status) > maxStatusLen {
  203. status = status[:maxStatusLen] + "..."
  204. }
  205. text := fmt.Sprintf("%s %s%s %s %s%s %s",
  206. pad,
  207. event.spinner.String(),
  208. prefix,
  209. event.ID,
  210. event.Text,
  211. strings.Repeat(" ", padding),
  212. status,
  213. )
  214. timer := fmt.Sprintf("%.1fs\n", elapsed)
  215. o := align(text, timer, terminalWidth)
  216. if color {
  217. color := aec.WhiteF
  218. if event.Status == Done {
  219. color = aec.BlueF
  220. }
  221. if event.Status == Error {
  222. color = aec.RedF
  223. }
  224. if event.Status == Warning {
  225. color = aec.YellowF
  226. }
  227. return aec.Apply(o, color)
  228. }
  229. return o
  230. }
  231. func numDone(events map[string]Event) int {
  232. i := 0
  233. for _, e := range events {
  234. if e.Status == Done {
  235. i++
  236. }
  237. }
  238. return i
  239. }
  240. func align(l, r string, w int) string {
  241. return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
  242. }