xref: /aosp_15_r20/build/soong/ui/terminal/smart_status.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1*333d2b36SAndroid Build Coastguard Worker// Copyright 2019 Google Inc. All rights reserved.
2*333d2b36SAndroid Build Coastguard Worker//
3*333d2b36SAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*333d2b36SAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*333d2b36SAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*333d2b36SAndroid Build Coastguard Worker//
7*333d2b36SAndroid Build Coastguard Worker//     http://www.apache.org/licenses/LICENSE-2.0
8*333d2b36SAndroid Build Coastguard Worker//
9*333d2b36SAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*333d2b36SAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*333d2b36SAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*333d2b36SAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*333d2b36SAndroid Build Coastguard Worker// limitations under the License.
14*333d2b36SAndroid Build Coastguard Worker
15*333d2b36SAndroid Build Coastguard Workerpackage terminal
16*333d2b36SAndroid Build Coastguard Worker
17*333d2b36SAndroid Build Coastguard Workerimport (
18*333d2b36SAndroid Build Coastguard Worker	"fmt"
19*333d2b36SAndroid Build Coastguard Worker	"io"
20*333d2b36SAndroid Build Coastguard Worker	"os"
21*333d2b36SAndroid Build Coastguard Worker	"os/signal"
22*333d2b36SAndroid Build Coastguard Worker	"strconv"
23*333d2b36SAndroid Build Coastguard Worker	"strings"
24*333d2b36SAndroid Build Coastguard Worker	"sync"
25*333d2b36SAndroid Build Coastguard Worker	"syscall"
26*333d2b36SAndroid Build Coastguard Worker	"time"
27*333d2b36SAndroid Build Coastguard Worker
28*333d2b36SAndroid Build Coastguard Worker	"android/soong/ui/status"
29*333d2b36SAndroid Build Coastguard Worker)
30*333d2b36SAndroid Build Coastguard Worker
31*333d2b36SAndroid Build Coastguard Workerconst tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
32*333d2b36SAndroid Build Coastguard Worker
33*333d2b36SAndroid Build Coastguard Workertype actionTableEntry struct {
34*333d2b36SAndroid Build Coastguard Worker	action    *status.Action
35*333d2b36SAndroid Build Coastguard Worker	startTime time.Time
36*333d2b36SAndroid Build Coastguard Worker}
37*333d2b36SAndroid Build Coastguard Worker
38*333d2b36SAndroid Build Coastguard Workertype smartStatusOutput struct {
39*333d2b36SAndroid Build Coastguard Worker	writer    io.Writer
40*333d2b36SAndroid Build Coastguard Worker	formatter formatter
41*333d2b36SAndroid Build Coastguard Worker
42*333d2b36SAndroid Build Coastguard Worker	lock sync.Mutex
43*333d2b36SAndroid Build Coastguard Worker
44*333d2b36SAndroid Build Coastguard Worker	haveBlankLine bool
45*333d2b36SAndroid Build Coastguard Worker
46*333d2b36SAndroid Build Coastguard Worker	tableMode             bool
47*333d2b36SAndroid Build Coastguard Worker	tableHeight           int
48*333d2b36SAndroid Build Coastguard Worker	requestedTableHeight  int
49*333d2b36SAndroid Build Coastguard Worker	termWidth, termHeight int
50*333d2b36SAndroid Build Coastguard Worker
51*333d2b36SAndroid Build Coastguard Worker	runningActions  []actionTableEntry
52*333d2b36SAndroid Build Coastguard Worker	ticker          *time.Ticker
53*333d2b36SAndroid Build Coastguard Worker	done            chan bool
54*333d2b36SAndroid Build Coastguard Worker	sigwinch        chan os.Signal
55*333d2b36SAndroid Build Coastguard Worker	sigwinchHandled chan bool
56*333d2b36SAndroid Build Coastguard Worker
57*333d2b36SAndroid Build Coastguard Worker	// Once there is a failure, we stop printing command output so the error
58*333d2b36SAndroid Build Coastguard Worker	// is easier to find
59*333d2b36SAndroid Build Coastguard Worker	haveFailures bool
60*333d2b36SAndroid Build Coastguard Worker	// If we are dropping errors, then at the end, we report a message to go
61*333d2b36SAndroid Build Coastguard Worker	// look in the verbose log if you want that command output.
62*333d2b36SAndroid Build Coastguard Worker	postFailureActionCount int
63*333d2b36SAndroid Build Coastguard Worker}
64*333d2b36SAndroid Build Coastguard Worker
65*333d2b36SAndroid Build Coastguard Worker// NewSmartStatusOutput returns a StatusOutput that represents the
66*333d2b36SAndroid Build Coastguard Worker// current build status similarly to Ninja's built-in terminal
67*333d2b36SAndroid Build Coastguard Worker// output.
68*333d2b36SAndroid Build Coastguard Workerfunc NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
69*333d2b36SAndroid Build Coastguard Worker	s := &smartStatusOutput{
70*333d2b36SAndroid Build Coastguard Worker		writer:    w,
71*333d2b36SAndroid Build Coastguard Worker		formatter: formatter,
72*333d2b36SAndroid Build Coastguard Worker
73*333d2b36SAndroid Build Coastguard Worker		haveBlankLine: true,
74*333d2b36SAndroid Build Coastguard Worker
75*333d2b36SAndroid Build Coastguard Worker		tableMode: true,
76*333d2b36SAndroid Build Coastguard Worker
77*333d2b36SAndroid Build Coastguard Worker		done:     make(chan bool),
78*333d2b36SAndroid Build Coastguard Worker		sigwinch: make(chan os.Signal),
79*333d2b36SAndroid Build Coastguard Worker	}
80*333d2b36SAndroid Build Coastguard Worker
81*333d2b36SAndroid Build Coastguard Worker	if env, ok := os.LookupEnv(tableHeightEnVar); ok {
82*333d2b36SAndroid Build Coastguard Worker		h, _ := strconv.Atoi(env)
83*333d2b36SAndroid Build Coastguard Worker		s.tableMode = h > 0
84*333d2b36SAndroid Build Coastguard Worker		s.requestedTableHeight = h
85*333d2b36SAndroid Build Coastguard Worker	}
86*333d2b36SAndroid Build Coastguard Worker
87*333d2b36SAndroid Build Coastguard Worker	if w, h, ok := termSize(s.writer); ok {
88*333d2b36SAndroid Build Coastguard Worker		s.termWidth, s.termHeight = w, h
89*333d2b36SAndroid Build Coastguard Worker		s.computeTableHeight()
90*333d2b36SAndroid Build Coastguard Worker	} else {
91*333d2b36SAndroid Build Coastguard Worker		s.tableMode = false
92*333d2b36SAndroid Build Coastguard Worker	}
93*333d2b36SAndroid Build Coastguard Worker
94*333d2b36SAndroid Build Coastguard Worker	if s.tableMode {
95*333d2b36SAndroid Build Coastguard Worker		// Add empty lines at the bottom of the screen to scroll back the existing history
96*333d2b36SAndroid Build Coastguard Worker		// and make room for the action table.
97*333d2b36SAndroid Build Coastguard Worker		// TODO: read the cursor position to see if the empty lines are necessary?
98*333d2b36SAndroid Build Coastguard Worker		for i := 0; i < s.tableHeight; i++ {
99*333d2b36SAndroid Build Coastguard Worker			fmt.Fprintln(w)
100*333d2b36SAndroid Build Coastguard Worker		}
101*333d2b36SAndroid Build Coastguard Worker
102*333d2b36SAndroid Build Coastguard Worker		// Hide the cursor to prevent seeing it bouncing around
103*333d2b36SAndroid Build Coastguard Worker		fmt.Fprintf(s.writer, ansi.hideCursor())
104*333d2b36SAndroid Build Coastguard Worker
105*333d2b36SAndroid Build Coastguard Worker		// Configure the empty action table
106*333d2b36SAndroid Build Coastguard Worker		s.actionTable()
107*333d2b36SAndroid Build Coastguard Worker
108*333d2b36SAndroid Build Coastguard Worker		// Start a tick to update the action table periodically
109*333d2b36SAndroid Build Coastguard Worker		s.startActionTableTick()
110*333d2b36SAndroid Build Coastguard Worker	}
111*333d2b36SAndroid Build Coastguard Worker
112*333d2b36SAndroid Build Coastguard Worker	s.startSigwinch()
113*333d2b36SAndroid Build Coastguard Worker
114*333d2b36SAndroid Build Coastguard Worker	return s
115*333d2b36SAndroid Build Coastguard Worker}
116*333d2b36SAndroid Build Coastguard Worker
117*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
118*333d2b36SAndroid Build Coastguard Worker	if level < status.StatusLvl {
119*333d2b36SAndroid Build Coastguard Worker		return
120*333d2b36SAndroid Build Coastguard Worker	}
121*333d2b36SAndroid Build Coastguard Worker
122*333d2b36SAndroid Build Coastguard Worker	str := s.formatter.message(level, message)
123*333d2b36SAndroid Build Coastguard Worker
124*333d2b36SAndroid Build Coastguard Worker	s.lock.Lock()
125*333d2b36SAndroid Build Coastguard Worker	defer s.lock.Unlock()
126*333d2b36SAndroid Build Coastguard Worker
127*333d2b36SAndroid Build Coastguard Worker	if level > status.StatusLvl {
128*333d2b36SAndroid Build Coastguard Worker		s.print(str)
129*333d2b36SAndroid Build Coastguard Worker	} else {
130*333d2b36SAndroid Build Coastguard Worker		s.statusLine(str)
131*333d2b36SAndroid Build Coastguard Worker	}
132*333d2b36SAndroid Build Coastguard Worker}
133*333d2b36SAndroid Build Coastguard Worker
134*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
135*333d2b36SAndroid Build Coastguard Worker	startTime := time.Now()
136*333d2b36SAndroid Build Coastguard Worker
137*333d2b36SAndroid Build Coastguard Worker	str := action.Description
138*333d2b36SAndroid Build Coastguard Worker	if str == "" {
139*333d2b36SAndroid Build Coastguard Worker		str = action.Command
140*333d2b36SAndroid Build Coastguard Worker	}
141*333d2b36SAndroid Build Coastguard Worker
142*333d2b36SAndroid Build Coastguard Worker	progress := s.formatter.progress(counts)
143*333d2b36SAndroid Build Coastguard Worker
144*333d2b36SAndroid Build Coastguard Worker	s.lock.Lock()
145*333d2b36SAndroid Build Coastguard Worker	defer s.lock.Unlock()
146*333d2b36SAndroid Build Coastguard Worker
147*333d2b36SAndroid Build Coastguard Worker	s.runningActions = append(s.runningActions, actionTableEntry{
148*333d2b36SAndroid Build Coastguard Worker		action:    action,
149*333d2b36SAndroid Build Coastguard Worker		startTime: startTime,
150*333d2b36SAndroid Build Coastguard Worker	})
151*333d2b36SAndroid Build Coastguard Worker
152*333d2b36SAndroid Build Coastguard Worker	s.statusLine(progress + str)
153*333d2b36SAndroid Build Coastguard Worker}
154*333d2b36SAndroid Build Coastguard Worker
155*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
156*333d2b36SAndroid Build Coastguard Worker	str := result.Description
157*333d2b36SAndroid Build Coastguard Worker	if str == "" {
158*333d2b36SAndroid Build Coastguard Worker		str = result.Command
159*333d2b36SAndroid Build Coastguard Worker	}
160*333d2b36SAndroid Build Coastguard Worker
161*333d2b36SAndroid Build Coastguard Worker	progress := s.formatter.progress(counts) + str
162*333d2b36SAndroid Build Coastguard Worker
163*333d2b36SAndroid Build Coastguard Worker	output := s.formatter.result(result)
164*333d2b36SAndroid Build Coastguard Worker
165*333d2b36SAndroid Build Coastguard Worker	s.lock.Lock()
166*333d2b36SAndroid Build Coastguard Worker	defer s.lock.Unlock()
167*333d2b36SAndroid Build Coastguard Worker
168*333d2b36SAndroid Build Coastguard Worker	for i, runningAction := range s.runningActions {
169*333d2b36SAndroid Build Coastguard Worker		if runningAction.action == result.Action {
170*333d2b36SAndroid Build Coastguard Worker			s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
171*333d2b36SAndroid Build Coastguard Worker			break
172*333d2b36SAndroid Build Coastguard Worker		}
173*333d2b36SAndroid Build Coastguard Worker	}
174*333d2b36SAndroid Build Coastguard Worker
175*333d2b36SAndroid Build Coastguard Worker	s.statusLine(progress)
176*333d2b36SAndroid Build Coastguard Worker
177*333d2b36SAndroid Build Coastguard Worker	// Stop printing when there are failures, but don't skip actions that also have their own errors.
178*333d2b36SAndroid Build Coastguard Worker	if output != "" {
179*333d2b36SAndroid Build Coastguard Worker		if !s.haveFailures || result.Error != nil {
180*333d2b36SAndroid Build Coastguard Worker			s.requestLine()
181*333d2b36SAndroid Build Coastguard Worker			s.print(output)
182*333d2b36SAndroid Build Coastguard Worker		} else {
183*333d2b36SAndroid Build Coastguard Worker			s.postFailureActionCount++
184*333d2b36SAndroid Build Coastguard Worker		}
185*333d2b36SAndroid Build Coastguard Worker	}
186*333d2b36SAndroid Build Coastguard Worker
187*333d2b36SAndroid Build Coastguard Worker	if result.Error != nil {
188*333d2b36SAndroid Build Coastguard Worker		s.haveFailures = true
189*333d2b36SAndroid Build Coastguard Worker	}
190*333d2b36SAndroid Build Coastguard Worker}
191*333d2b36SAndroid Build Coastguard Worker
192*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) Flush() {
193*333d2b36SAndroid Build Coastguard Worker	if s.tableMode {
194*333d2b36SAndroid Build Coastguard Worker		// Stop the action table tick outside of the lock to avoid lock ordering issues between s.done and
195*333d2b36SAndroid Build Coastguard Worker		// s.lock, the goroutine in startActionTableTick can get blocked on the lock and be unable to read
196*333d2b36SAndroid Build Coastguard Worker		// from the channel.
197*333d2b36SAndroid Build Coastguard Worker		s.stopActionTableTick()
198*333d2b36SAndroid Build Coastguard Worker	}
199*333d2b36SAndroid Build Coastguard Worker
200*333d2b36SAndroid Build Coastguard Worker	s.lock.Lock()
201*333d2b36SAndroid Build Coastguard Worker	defer s.lock.Unlock()
202*333d2b36SAndroid Build Coastguard Worker
203*333d2b36SAndroid Build Coastguard Worker	s.stopSigwinch()
204*333d2b36SAndroid Build Coastguard Worker
205*333d2b36SAndroid Build Coastguard Worker	if s.postFailureActionCount > 0 {
206*333d2b36SAndroid Build Coastguard Worker		s.requestLine()
207*333d2b36SAndroid Build Coastguard Worker		if s.postFailureActionCount == 1 {
208*333d2b36SAndroid Build Coastguard Worker			s.print(fmt.Sprintf("There was 1 action that completed after the action that failed. See verbose.log.gz for its output."))
209*333d2b36SAndroid Build Coastguard Worker		} else {
210*333d2b36SAndroid Build Coastguard Worker			s.print(fmt.Sprintf("There were %d actions that completed after the action that failed. See verbose.log.gz for their output.", s.postFailureActionCount))
211*333d2b36SAndroid Build Coastguard Worker		}
212*333d2b36SAndroid Build Coastguard Worker	}
213*333d2b36SAndroid Build Coastguard Worker
214*333d2b36SAndroid Build Coastguard Worker	s.requestLine()
215*333d2b36SAndroid Build Coastguard Worker
216*333d2b36SAndroid Build Coastguard Worker	s.runningActions = nil
217*333d2b36SAndroid Build Coastguard Worker
218*333d2b36SAndroid Build Coastguard Worker	if s.tableMode {
219*333d2b36SAndroid Build Coastguard Worker		// Update the table after clearing runningActions to clear it
220*333d2b36SAndroid Build Coastguard Worker		s.actionTable()
221*333d2b36SAndroid Build Coastguard Worker
222*333d2b36SAndroid Build Coastguard Worker		// Reset the scrolling region to the whole terminal
223*333d2b36SAndroid Build Coastguard Worker		fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
224*333d2b36SAndroid Build Coastguard Worker		_, height, _ := termSize(s.writer)
225*333d2b36SAndroid Build Coastguard Worker		// Move the cursor to the top of the now-blank, previously non-scrolling region
226*333d2b36SAndroid Build Coastguard Worker		fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 1))
227*333d2b36SAndroid Build Coastguard Worker		// Turn the cursor back on
228*333d2b36SAndroid Build Coastguard Worker		fmt.Fprintf(s.writer, ansi.showCursor())
229*333d2b36SAndroid Build Coastguard Worker	}
230*333d2b36SAndroid Build Coastguard Worker}
231*333d2b36SAndroid Build Coastguard Worker
232*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) Write(p []byte) (int, error) {
233*333d2b36SAndroid Build Coastguard Worker	s.lock.Lock()
234*333d2b36SAndroid Build Coastguard Worker	defer s.lock.Unlock()
235*333d2b36SAndroid Build Coastguard Worker	s.print(string(p))
236*333d2b36SAndroid Build Coastguard Worker	return len(p), nil
237*333d2b36SAndroid Build Coastguard Worker}
238*333d2b36SAndroid Build Coastguard Worker
239*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) requestLine() {
240*333d2b36SAndroid Build Coastguard Worker	if !s.haveBlankLine {
241*333d2b36SAndroid Build Coastguard Worker		fmt.Fprintln(s.writer)
242*333d2b36SAndroid Build Coastguard Worker		s.haveBlankLine = true
243*333d2b36SAndroid Build Coastguard Worker	}
244*333d2b36SAndroid Build Coastguard Worker}
245*333d2b36SAndroid Build Coastguard Worker
246*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) print(str string) {
247*333d2b36SAndroid Build Coastguard Worker	if !s.haveBlankLine {
248*333d2b36SAndroid Build Coastguard Worker		fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
249*333d2b36SAndroid Build Coastguard Worker		s.haveBlankLine = true
250*333d2b36SAndroid Build Coastguard Worker	}
251*333d2b36SAndroid Build Coastguard Worker	fmt.Fprint(s.writer, str)
252*333d2b36SAndroid Build Coastguard Worker	if len(str) == 0 || str[len(str)-1] != '\n' {
253*333d2b36SAndroid Build Coastguard Worker		fmt.Fprint(s.writer, "\n")
254*333d2b36SAndroid Build Coastguard Worker	}
255*333d2b36SAndroid Build Coastguard Worker}
256*333d2b36SAndroid Build Coastguard Worker
257*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) statusLine(str string) {
258*333d2b36SAndroid Build Coastguard Worker	idx := strings.IndexRune(str, '\n')
259*333d2b36SAndroid Build Coastguard Worker	if idx != -1 {
260*333d2b36SAndroid Build Coastguard Worker		str = str[0:idx]
261*333d2b36SAndroid Build Coastguard Worker	}
262*333d2b36SAndroid Build Coastguard Worker
263*333d2b36SAndroid Build Coastguard Worker	// Limit line width to the terminal width, otherwise we'll wrap onto
264*333d2b36SAndroid Build Coastguard Worker	// another line and we won't delete the previous line.
265*333d2b36SAndroid Build Coastguard Worker	str = elide(str, s.termWidth)
266*333d2b36SAndroid Build Coastguard Worker
267*333d2b36SAndroid Build Coastguard Worker	// Move to the beginning on the line, turn on bold, print the output,
268*333d2b36SAndroid Build Coastguard Worker	// turn off bold, then clear the rest of the line.
269*333d2b36SAndroid Build Coastguard Worker	start := "\r" + ansi.bold()
270*333d2b36SAndroid Build Coastguard Worker	end := ansi.regular() + ansi.clearToEndOfLine()
271*333d2b36SAndroid Build Coastguard Worker	fmt.Fprint(s.writer, start, str, end)
272*333d2b36SAndroid Build Coastguard Worker	s.haveBlankLine = false
273*333d2b36SAndroid Build Coastguard Worker}
274*333d2b36SAndroid Build Coastguard Worker
275*333d2b36SAndroid Build Coastguard Workerfunc elide(str string, width int) string {
276*333d2b36SAndroid Build Coastguard Worker	if width > 0 && len(str) > width {
277*333d2b36SAndroid Build Coastguard Worker		// TODO: Just do a max. Ninja elides the middle, but that's
278*333d2b36SAndroid Build Coastguard Worker		// more complicated and these lines aren't that important.
279*333d2b36SAndroid Build Coastguard Worker		str = str[:width]
280*333d2b36SAndroid Build Coastguard Worker	}
281*333d2b36SAndroid Build Coastguard Worker
282*333d2b36SAndroid Build Coastguard Worker	return str
283*333d2b36SAndroid Build Coastguard Worker}
284*333d2b36SAndroid Build Coastguard Worker
285*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) startActionTableTick() {
286*333d2b36SAndroid Build Coastguard Worker	s.ticker = time.NewTicker(time.Second)
287*333d2b36SAndroid Build Coastguard Worker	go func() {
288*333d2b36SAndroid Build Coastguard Worker		for {
289*333d2b36SAndroid Build Coastguard Worker			select {
290*333d2b36SAndroid Build Coastguard Worker			case <-s.ticker.C:
291*333d2b36SAndroid Build Coastguard Worker				s.lock.Lock()
292*333d2b36SAndroid Build Coastguard Worker				s.actionTable()
293*333d2b36SAndroid Build Coastguard Worker				s.lock.Unlock()
294*333d2b36SAndroid Build Coastguard Worker			case <-s.done:
295*333d2b36SAndroid Build Coastguard Worker				return
296*333d2b36SAndroid Build Coastguard Worker			}
297*333d2b36SAndroid Build Coastguard Worker		}
298*333d2b36SAndroid Build Coastguard Worker	}()
299*333d2b36SAndroid Build Coastguard Worker}
300*333d2b36SAndroid Build Coastguard Worker
301*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) stopActionTableTick() {
302*333d2b36SAndroid Build Coastguard Worker	s.ticker.Stop()
303*333d2b36SAndroid Build Coastguard Worker	s.done <- true
304*333d2b36SAndroid Build Coastguard Worker}
305*333d2b36SAndroid Build Coastguard Worker
306*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) startSigwinch() {
307*333d2b36SAndroid Build Coastguard Worker	signal.Notify(s.sigwinch, syscall.SIGWINCH)
308*333d2b36SAndroid Build Coastguard Worker	go func() {
309*333d2b36SAndroid Build Coastguard Worker		for _ = range s.sigwinch {
310*333d2b36SAndroid Build Coastguard Worker			s.lock.Lock()
311*333d2b36SAndroid Build Coastguard Worker			s.updateTermSize()
312*333d2b36SAndroid Build Coastguard Worker			if s.tableMode {
313*333d2b36SAndroid Build Coastguard Worker				s.actionTable()
314*333d2b36SAndroid Build Coastguard Worker			}
315*333d2b36SAndroid Build Coastguard Worker			s.lock.Unlock()
316*333d2b36SAndroid Build Coastguard Worker			if s.sigwinchHandled != nil {
317*333d2b36SAndroid Build Coastguard Worker				s.sigwinchHandled <- true
318*333d2b36SAndroid Build Coastguard Worker			}
319*333d2b36SAndroid Build Coastguard Worker		}
320*333d2b36SAndroid Build Coastguard Worker	}()
321*333d2b36SAndroid Build Coastguard Worker}
322*333d2b36SAndroid Build Coastguard Worker
323*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) stopSigwinch() {
324*333d2b36SAndroid Build Coastguard Worker	signal.Stop(s.sigwinch)
325*333d2b36SAndroid Build Coastguard Worker	close(s.sigwinch)
326*333d2b36SAndroid Build Coastguard Worker}
327*333d2b36SAndroid Build Coastguard Worker
328*333d2b36SAndroid Build Coastguard Worker// computeTableHeight recomputes s.tableHeight based on s.termHeight and s.requestedTableHeight.
329*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) computeTableHeight() {
330*333d2b36SAndroid Build Coastguard Worker	tableHeight := s.requestedTableHeight
331*333d2b36SAndroid Build Coastguard Worker	if tableHeight == 0 {
332*333d2b36SAndroid Build Coastguard Worker		tableHeight = s.termHeight / 4
333*333d2b36SAndroid Build Coastguard Worker		if tableHeight < 1 {
334*333d2b36SAndroid Build Coastguard Worker			tableHeight = 1
335*333d2b36SAndroid Build Coastguard Worker		} else if tableHeight > 10 {
336*333d2b36SAndroid Build Coastguard Worker			tableHeight = 10
337*333d2b36SAndroid Build Coastguard Worker		}
338*333d2b36SAndroid Build Coastguard Worker	}
339*333d2b36SAndroid Build Coastguard Worker	if tableHeight > s.termHeight-1 {
340*333d2b36SAndroid Build Coastguard Worker		tableHeight = s.termHeight - 1
341*333d2b36SAndroid Build Coastguard Worker	}
342*333d2b36SAndroid Build Coastguard Worker	s.tableHeight = tableHeight
343*333d2b36SAndroid Build Coastguard Worker}
344*333d2b36SAndroid Build Coastguard Worker
345*333d2b36SAndroid Build Coastguard Worker// updateTermSize recomputes the table height after a SIGWINCH and pans any existing text if
346*333d2b36SAndroid Build Coastguard Worker// necessary.
347*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) updateTermSize() {
348*333d2b36SAndroid Build Coastguard Worker	if w, h, ok := termSize(s.writer); ok {
349*333d2b36SAndroid Build Coastguard Worker		oldScrollingHeight := s.termHeight - s.tableHeight
350*333d2b36SAndroid Build Coastguard Worker
351*333d2b36SAndroid Build Coastguard Worker		s.termWidth, s.termHeight = w, h
352*333d2b36SAndroid Build Coastguard Worker
353*333d2b36SAndroid Build Coastguard Worker		if s.tableMode {
354*333d2b36SAndroid Build Coastguard Worker			s.computeTableHeight()
355*333d2b36SAndroid Build Coastguard Worker
356*333d2b36SAndroid Build Coastguard Worker			scrollingHeight := s.termHeight - s.tableHeight
357*333d2b36SAndroid Build Coastguard Worker
358*333d2b36SAndroid Build Coastguard Worker			// If the scrolling region has changed, attempt to pan the existing text so that it is
359*333d2b36SAndroid Build Coastguard Worker			// not overwritten by the table.
360*333d2b36SAndroid Build Coastguard Worker			if scrollingHeight < oldScrollingHeight {
361*333d2b36SAndroid Build Coastguard Worker				pan := oldScrollingHeight - scrollingHeight
362*333d2b36SAndroid Build Coastguard Worker				if pan > s.tableHeight {
363*333d2b36SAndroid Build Coastguard Worker					pan = s.tableHeight
364*333d2b36SAndroid Build Coastguard Worker				}
365*333d2b36SAndroid Build Coastguard Worker				fmt.Fprint(s.writer, ansi.panDown(pan))
366*333d2b36SAndroid Build Coastguard Worker			}
367*333d2b36SAndroid Build Coastguard Worker		}
368*333d2b36SAndroid Build Coastguard Worker	}
369*333d2b36SAndroid Build Coastguard Worker}
370*333d2b36SAndroid Build Coastguard Worker
371*333d2b36SAndroid Build Coastguard Workerfunc (s *smartStatusOutput) actionTable() {
372*333d2b36SAndroid Build Coastguard Worker	scrollingHeight := s.termHeight - s.tableHeight
373*333d2b36SAndroid Build Coastguard Worker
374*333d2b36SAndroid Build Coastguard Worker	// Update the scrolling region in case the height of the terminal changed
375*333d2b36SAndroid Build Coastguard Worker
376*333d2b36SAndroid Build Coastguard Worker	fmt.Fprint(s.writer, ansi.setScrollingMargins(1, scrollingHeight))
377*333d2b36SAndroid Build Coastguard Worker
378*333d2b36SAndroid Build Coastguard Worker	// Write as many status lines as fit in the table
379*333d2b36SAndroid Build Coastguard Worker	for tableLine := 0; tableLine < s.tableHeight; tableLine++ {
380*333d2b36SAndroid Build Coastguard Worker		if tableLine >= s.tableHeight {
381*333d2b36SAndroid Build Coastguard Worker			break
382*333d2b36SAndroid Build Coastguard Worker		}
383*333d2b36SAndroid Build Coastguard Worker		// Move the cursor to the correct line of the non-scrolling region
384*333d2b36SAndroid Build Coastguard Worker		fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1+tableLine, 1))
385*333d2b36SAndroid Build Coastguard Worker
386*333d2b36SAndroid Build Coastguard Worker		if tableLine < len(s.runningActions) {
387*333d2b36SAndroid Build Coastguard Worker			runningAction := s.runningActions[tableLine]
388*333d2b36SAndroid Build Coastguard Worker
389*333d2b36SAndroid Build Coastguard Worker			seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
390*333d2b36SAndroid Build Coastguard Worker
391*333d2b36SAndroid Build Coastguard Worker			desc := runningAction.action.Description
392*333d2b36SAndroid Build Coastguard Worker			if desc == "" {
393*333d2b36SAndroid Build Coastguard Worker				desc = runningAction.action.Command
394*333d2b36SAndroid Build Coastguard Worker			}
395*333d2b36SAndroid Build Coastguard Worker
396*333d2b36SAndroid Build Coastguard Worker			color := ""
397*333d2b36SAndroid Build Coastguard Worker			if seconds >= 60 {
398*333d2b36SAndroid Build Coastguard Worker				color = ansi.red() + ansi.bold()
399*333d2b36SAndroid Build Coastguard Worker			} else if seconds >= 30 {
400*333d2b36SAndroid Build Coastguard Worker				color = ansi.yellow() + ansi.bold()
401*333d2b36SAndroid Build Coastguard Worker			}
402*333d2b36SAndroid Build Coastguard Worker
403*333d2b36SAndroid Build Coastguard Worker			durationStr := fmt.Sprintf("   %2d:%02d ", seconds/60, seconds%60)
404*333d2b36SAndroid Build Coastguard Worker			desc = elide(desc, s.termWidth-len(durationStr))
405*333d2b36SAndroid Build Coastguard Worker			durationStr = color + durationStr + ansi.regular()
406*333d2b36SAndroid Build Coastguard Worker			fmt.Fprint(s.writer, durationStr, desc)
407*333d2b36SAndroid Build Coastguard Worker		}
408*333d2b36SAndroid Build Coastguard Worker		fmt.Fprint(s.writer, ansi.clearToEndOfLine())
409*333d2b36SAndroid Build Coastguard Worker	}
410*333d2b36SAndroid Build Coastguard Worker
411*333d2b36SAndroid Build Coastguard Worker	// Move the cursor back to the last line of the scrolling region
412*333d2b36SAndroid Build Coastguard Worker	fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 1))
413*333d2b36SAndroid Build Coastguard Worker}
414*333d2b36SAndroid Build Coastguard Worker
415*333d2b36SAndroid Build Coastguard Workervar ansi = ansiImpl{}
416*333d2b36SAndroid Build Coastguard Worker
417*333d2b36SAndroid Build Coastguard Workertype ansiImpl struct{}
418*333d2b36SAndroid Build Coastguard Worker
419*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) clearToEndOfLine() string {
420*333d2b36SAndroid Build Coastguard Worker	return "\x1b[K"
421*333d2b36SAndroid Build Coastguard Worker}
422*333d2b36SAndroid Build Coastguard Worker
423*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) setCursor(row, column int) string {
424*333d2b36SAndroid Build Coastguard Worker	// Direct cursor address
425*333d2b36SAndroid Build Coastguard Worker	return fmt.Sprintf("\x1b[%d;%dH", row, column)
426*333d2b36SAndroid Build Coastguard Worker}
427*333d2b36SAndroid Build Coastguard Worker
428*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) setScrollingMargins(top, bottom int) string {
429*333d2b36SAndroid Build Coastguard Worker	// Set Top and Bottom Margins DECSTBM
430*333d2b36SAndroid Build Coastguard Worker	return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
431*333d2b36SAndroid Build Coastguard Worker}
432*333d2b36SAndroid Build Coastguard Worker
433*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) resetScrollingMargins() string {
434*333d2b36SAndroid Build Coastguard Worker	// Set Top and Bottom Margins DECSTBM
435*333d2b36SAndroid Build Coastguard Worker	return fmt.Sprintf("\x1b[r")
436*333d2b36SAndroid Build Coastguard Worker}
437*333d2b36SAndroid Build Coastguard Worker
438*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) red() string {
439*333d2b36SAndroid Build Coastguard Worker	return "\x1b[31m"
440*333d2b36SAndroid Build Coastguard Worker}
441*333d2b36SAndroid Build Coastguard Worker
442*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) yellow() string {
443*333d2b36SAndroid Build Coastguard Worker	return "\x1b[33m"
444*333d2b36SAndroid Build Coastguard Worker}
445*333d2b36SAndroid Build Coastguard Worker
446*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) bold() string {
447*333d2b36SAndroid Build Coastguard Worker	return "\x1b[1m"
448*333d2b36SAndroid Build Coastguard Worker}
449*333d2b36SAndroid Build Coastguard Worker
450*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) regular() string {
451*333d2b36SAndroid Build Coastguard Worker	return "\x1b[0m"
452*333d2b36SAndroid Build Coastguard Worker}
453*333d2b36SAndroid Build Coastguard Worker
454*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) showCursor() string {
455*333d2b36SAndroid Build Coastguard Worker	return "\x1b[?25h"
456*333d2b36SAndroid Build Coastguard Worker}
457*333d2b36SAndroid Build Coastguard Worker
458*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) hideCursor() string {
459*333d2b36SAndroid Build Coastguard Worker	return "\x1b[?25l"
460*333d2b36SAndroid Build Coastguard Worker}
461*333d2b36SAndroid Build Coastguard Worker
462*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) panDown(lines int) string {
463*333d2b36SAndroid Build Coastguard Worker	return fmt.Sprintf("\x1b[%dS", lines)
464*333d2b36SAndroid Build Coastguard Worker}
465*333d2b36SAndroid Build Coastguard Worker
466*333d2b36SAndroid Build Coastguard Workerfunc (ansiImpl) panUp(lines int) string {
467*333d2b36SAndroid Build Coastguard Worker	return fmt.Sprintf("\x1b[%dT", lines)
468*333d2b36SAndroid Build Coastguard Worker}
469