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  }