github.phpd.cn/thought-machine/please@v12.2.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  	"cli"
    13  	"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  		printf("${ERASE_AFTER}\n")
    79  	}
    80  	for i := 0; i < len(buildingTargets) && i < maxLines; i++ {
    81  		buildingTargets[i].Lock()
    82  		// Take a local copy of the structure, which isn't *that* big, so we don't need to retain the lock
    83  		// while we do potentially blocking things like printing.
    84  		target := buildingTargets[i].buildingTargetData
    85  		buildingTargets[i].Unlock()
    86  		label := target.Label.Parent()
    87  		duration := now.Sub(target.Started).Seconds()
    88  		if target.Active && target.Target != nil && target.Target.ShowProgress && target.Target.Progress > 0.0 {
    89  			if target.Target.Progress > 1.0 && target.Target.Progress < 100.0 && target.Target.Progress != target.LastProgress {
    90  				proportionDone := target.Target.Progress / 100.0
    91  				perPercent := float32(duration) / proportionDone
    92  				buildingTargets[i].Eta = time.Duration(perPercent * (1.0 - proportionDone) * float32(time.Second)).Truncate(time.Second)
    93  				buildingTargets[i].LastProgress = target.Target.Progress
    94  			}
    95  			if target.Eta > 0 {
    96  				lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${RESET} (%.1f%%%%, est %s remaining)${ERASE_AFTER}\n",
    97  					duration, target.Colour, label, target.Description, target.Target.Progress, target.Eta)
    98  			} else {
    99  				lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${RESET} (%.1f%%%% complete)${ERASE_AFTER}\n",
   100  					duration, target.Colour, label, target.Description, target.Target.Progress)
   101  			}
   102  		} else if target.Active {
   103  			lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_WHITE}%s${ERASE_AFTER}\n",
   104  				duration, target.Colour, label, target.Description)
   105  		} else if time.Since(target.Finished).Seconds() < 0.5 {
   106  			// Only display finished targets for half a second after they're done.
   107  			duration := target.Finished.Sub(target.Started).Seconds()
   108  			if target.Failed {
   109  				lprintf(cols, "${BOLD_RED}=> [%4.1fs] ${RESET}%s%s ${BOLD_RED}Failed${ERASE_AFTER}\n",
   110  					duration, target.Colour, label)
   111  			} else if target.Cached {
   112  				lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${BOLD_GREY}%s${ERASE_AFTER}\n",
   113  					duration, target.Colour, label, target.Description)
   114  			} else {
   115  				lprintf(cols, "${BOLD_WHITE}=> [%4.1fs] ${RESET}%s%s ${WHITE}%s${ERASE_AFTER}\n",
   116  					duration, target.Colour, label, target.Description)
   117  			}
   118  		} else {
   119  			printf("${BOLD_GREY}=|${ERASE_AFTER}\n")
   120  		}
   121  	}
   122  	printf("${RESET}")
   123  }
   124  
   125  // printStat prints a single statistic with appropriate colours.
   126  func printStat(caption string, stat float64, multiplier int) {
   127  	colour := "${BOLD_GREEN}"
   128  	if stat > 80.0*float64(multiplier) {
   129  		colour = "${BOLD_RED}"
   130  	} else if stat > 60.0*float64(multiplier) {
   131  		colour = "${BOLD_YELLOW}"
   132  	}
   133  	printf("  ${BOLD_WHITE}%s:${RESET} %s%5.1f%%${RESET}", caption, colour, stat)
   134  }
   135  
   136  func recalcWindowSize(backend *cli.LogBackend) {
   137  	rows, cols, _ := cli.WindowSize()
   138  	backend.Lock()
   139  	defer backend.Unlock()
   140  	backend.Rows = rows - 4 // Give a little space at the edge for any off-by-ones
   141  	backend.Cols = cols
   142  	backend.RecalcLines()
   143  }
   144  
   145  // Limited-length printf that respects current window width.
   146  // Output is truncated at the middle to fit within 'cols'.
   147  func lprintf(cols int, format string, args ...interface{}) {
   148  	printf(lprintfPrepare(cols, format, args...))
   149  }
   150  
   151  func lprintfPrepare(cols int, format string, args ...interface{}) string {
   152  	s := fmt.Sprintf(format, args...)
   153  	if len(s) < cols {
   154  		return s // it's short enough, nice and simple
   155  	}
   156  	// Okay, it's too long. Tricky thing: ANSI escape codes don't count for width
   157  	// so we need to count without those. Bonus: make an effort to be unicode-aware.
   158  	var b bytes.Buffer
   159  	written := 0
   160  	inAnsiCode := false
   161  	for _, rune := range s {
   162  		if inAnsiCode {
   163  			b.WriteRune(rune)
   164  			if rune == 'm' {
   165  				inAnsiCode = false
   166  			}
   167  		} else if rune == '\x1b' {
   168  			b.WriteRune(rune)
   169  			inAnsiCode = true
   170  		} else if rune == '\n' {
   171  			b.WriteRune(rune)
   172  		} else if written == cols-3 {
   173  			b.WriteString("...")
   174  			written += 3
   175  		} else if written < cols-3 {
   176  			b.WriteRune(rune)
   177  			written++
   178  		}
   179  	}
   180  	return b.String()
   181  }
   182  
   183  // setWindowTitle sets the title of the current shell window based on the current build state.
   184  func setWindowTitle(state *core.BuildState, running bool) {
   185  	if !state.Config.Display.UpdateTitle {
   186  		return
   187  	}
   188  	if running {
   189  		SetWindowTitle("plz: finishing up")
   190  	} else {
   191  		SetWindowTitle(fmt.Sprintf("plz: %d / %d tasks, %3.1fs", state.NumDone(), state.NumActive(), time.Since(state.StartTime).Seconds()))
   192  	}
   193  }
   194  
   195  // SetWindowTitle sets the title of the current shell window.
   196  func SetWindowTitle(title string) {
   197  	if cli.StdErrIsATerminal && terminalClaimsToBeXterm {
   198  		os.Stderr.Write([]byte(fmt.Sprintf("\033]0;%s\007", title)))
   199  	}
   200  }