github.com/tiagovtristao/plz@v13.4.0+incompatible/src/output/interactive_display.go (about) 1 package output 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/signal" 8 "strings" 9 "syscall" 10 "time" 11 12 "github.com/thought-machine/please/src/cli" 13 "github.com/thought-machine/please/src/core" 14 ) 15 16 // We only set the terminal title for terminals that at least claim to be xterm 17 // (note that most terminals do for compatibility; some report as xterm-color, hence HasPrefix) 18 var terminalClaimsToBeXterm = strings.HasPrefix(os.Getenv("TERM"), "xterm") 19 20 func display(state *core.BuildState, buildingTargets *[]buildingTarget, stop <-chan struct{}, done chan<- struct{}) { 21 backend := cli.NewLogBackend(len(*buildingTargets)) 22 go func() { 23 sig := make(chan os.Signal, 10) 24 signal.Notify(sig, syscall.SIGWINCH) 25 for { 26 <-sig 27 recalcWindowSize(backend) 28 } 29 }() 30 recalcWindowSize(backend) 31 backend.SetActive() 32 33 printLines(state, *buildingTargets, backend.MaxInteractiveRows, backend.Cols) 34 outputLines := len(backend.Output) 35 ticker := time.NewTicker(50 * time.Millisecond) 36 loop: 37 for { 38 select { 39 case <-stop: 40 break loop 41 case <-ticker.C: 42 moveToFirstLine(*buildingTargets, outputLines, backend.MaxInteractiveRows, state.Config.Display.SystemStats) 43 printLines(state, *buildingTargets, backend.MaxInteractiveRows, backend.Cols) 44 for _, line := range backend.Output { 45 printf("\x1b[2K%s\n", line) // erase each line as we go 46 } 47 outputLines = len(backend.Output) 48 setWindowTitle(state, true) 49 } 50 } 51 ticker.Stop() 52 setWindowTitle(state, false) 53 // Clear it all out. 54 moveToFirstLine(*buildingTargets, outputLines, backend.MaxInteractiveRows, state.Config.Display.SystemStats) 55 printf("\x1b[0J") // Clear out to end of screen. 56 backend.Deactivate() 57 done <- struct{}{} 58 } 59 60 // moveToFirstLine resets back to the first line. Not as easy as you might think. 61 func moveToFirstLine(buildingTargets []buildingTarget, outputLines, maxInteractiveRows int, showingStats bool) { 62 if maxInteractiveRows > len(buildingTargets) { 63 maxInteractiveRows = len(buildingTargets) 64 } 65 if showingStats { 66 maxInteractiveRows++ 67 } 68 printf("\x1b[%dA", maxInteractiveRows+1+outputLines) 69 } 70 71 func printLines(state *core.BuildState, buildingTargets []buildingTarget, maxLines, cols int) { 72 now := time.Now() 73 printf("Building [%d/%d, %3.1fs]:\n", state.NumDone(), state.NumActive(), time.Since(state.StartTime).Seconds()) 74 if state.Config.Display.SystemStats { 75 printStat("CPU use", state.Stats.CPU.Used, state.Stats.CPU.Count) 76 printStat("I/O", state.Stats.CPU.IOWait, state.Stats.CPU.Count) 77 printStat("Mem use", state.Stats.Memory.UsedPercent, 1) 78 if state.Stats.NumWorkerProcesses > 0 { 79 printf(" ${BOLD_WHITE}Worker processes: %d${RESET}", state.Stats.NumWorkerProcesses) 80 } 81 printf("${ERASE_AFTER}\n") 82 } 83 for i := 0; i < len(buildingTargets) && i < maxLines; i++ { 84 buildingTargets[i].Lock() 85 // Take a local copy of the structure, which isn't *that* big, so we don't need to retain the lock 86 // while we do potentially blocking things like printing. 87 target := buildingTargets[i].buildingTargetData 88 buildingTargets[i].Unlock() 89 label := target.Label.Parent() 90 duration := now.Sub(target.Started).Seconds() 91 if target.Active && target.Target != nil && target.Target.ShowProgress && target.Target.Progress > 0.0 { 92 if target.Target.Progress > 1.0 && target.Target.Progress < 100.0 && target.Target.Progress != target.LastProgress { 93 proportionDone := target.Target.Progress / 100.0 94 perPercent := float32(duration) / proportionDone 95 buildingTargets[i].Eta = time.Duration(perPercent * (1.0 - proportionDone) * float32(time.Second)).Truncate(time.Second) 96 buildingTargets[i].LastProgress = target.Target.Progress 97 } 98 if target.Eta > 0 { 99 lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${RESET} (%.1f%%%%, est %s remaining)${ERASE_AFTER}\n", 100 duration, target.Colour, label, target.Description, target.Target.Progress, target.Eta) 101 } else { 102 lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${RESET} (%.1f%%%% complete)${ERASE_AFTER}\n", 103 duration, target.Colour, label, target.Description, target.Target.Progress) 104 } 105 } else if target.Active { 106 lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${ERASE_AFTER}\n", 107 duration, target.Colour, label, target.Description) 108 } else if time.Since(target.Finished).Seconds() < 0.5 { 109 // Only display finished targets for half a second after they're done. 110 duration := target.Finished.Sub(target.Started).Seconds() 111 if target.Failed { 112 lprintf(cols, "${BOLD_RED}=> [%4.1fs] ${RESET}%s%s ${BOLD_RED}Failed${ERASE_AFTER}\n", 113 duration, target.Colour, label) 114 } else if target.Cached { 115 lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_GREY}%s${ERASE_AFTER}\n", 116 duration, target.Colour, label, target.Description) 117 } else { 118 lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${WHITE}%s${ERASE_AFTER}\n", 119 duration, target.Colour, label, target.Description) 120 } 121 } else { 122 printf("${BOLD_GREY}=|${ERASE_AFTER}\n") 123 } 124 } 125 printf("${RESET}") 126 } 127 128 // printStat prints a single statistic with appropriate colours. 129 func printStat(caption string, stat float64, multiplier int) { 130 colour := "${BOLD_GREEN}" 131 if stat > 80.0*float64(multiplier) { 132 colour = "${BOLD_RED}" 133 } else if stat > 60.0*float64(multiplier) { 134 colour = "${BOLD_YELLOW}" 135 } 136 printf(" ${BOLD_WHITE}%s:${RESET} %s%5.1f%%${RESET}", caption, colour, stat) 137 } 138 139 func recalcWindowSize(backend *cli.LogBackend) { 140 rows, cols, _ := cli.WindowSize() 141 backend.Lock() 142 defer backend.Unlock() 143 backend.Rows = rows - 4 // Give a little space at the edge for any off-by-ones 144 backend.Cols = cols 145 backend.RecalcLines() 146 } 147 148 // Limited-length printf that respects current window width. 149 // Output is truncated at the middle to fit within 'cols'. 150 func lprintf(cols int, format string, args ...interface{}) { 151 printf(lprintfPrepare(cols, format, args...)) 152 } 153 154 func lprintfPrepare(cols int, format string, args ...interface{}) string { 155 s := fmt.Sprintf(format, args...) 156 if len(s) < cols { 157 return s // it's short enough, nice and simple 158 } 159 // Okay, it's too long. Tricky thing: ANSI escape codes don't count for width 160 // so we need to count without those. Bonus: make an effort to be unicode-aware. 161 var b bytes.Buffer 162 written := 0 163 inAnsiCode := false 164 for _, rune := range s { 165 if inAnsiCode { 166 b.WriteRune(rune) 167 if rune == 'm' { 168 inAnsiCode = false 169 } 170 } else if rune == '\x1b' { 171 b.WriteRune(rune) 172 inAnsiCode = true 173 } else if rune == '\n' { 174 b.WriteRune(rune) 175 } else if written == cols-3 { 176 b.WriteString("...") 177 written += 3 178 } else if written < cols-3 { 179 b.WriteRune(rune) 180 written++ 181 } 182 } 183 return b.String() 184 } 185 186 // setWindowTitle sets the title of the current shell window based on the current build state. 187 func setWindowTitle(state *core.BuildState, running bool) { 188 if !state.Config.Display.UpdateTitle { 189 return 190 } 191 if running { 192 SetWindowTitle("plz: finishing up") 193 } else { 194 SetWindowTitle(fmt.Sprintf("plz: %d / %d tasks, %3.1fs", state.NumDone(), state.NumActive(), time.Since(state.StartTime).Seconds())) 195 } 196 } 197 198 // SetWindowTitle sets the title of the current shell window. 199 func SetWindowTitle(title string) { 200 if cli.StdErrIsATerminal && terminalClaimsToBeXterm { 201 os.Stderr.Write([]byte(fmt.Sprintf("\033]0;%s\007", title))) 202 } 203 }