blob: 82c04d46ce61022f1b3b6cbcf783ca4e9b33acc8 [file] [log] [blame]
Colin Crossce525352019-06-08 18:58:00 -07001// Copyright 2019 Google Inc. All rights reserved.
2//
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//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package terminal
16
17import (
18 "fmt"
Colin Cross097ed2a2019-06-08 21:48:58 -070019 "io"
Colin Cross4355ee62019-06-11 23:01:36 -070020 "os"
21 "os/signal"
Colin Crossce525352019-06-08 18:58:00 -070022 "strings"
23 "sync"
Colin Cross4355ee62019-06-11 23:01:36 -070024 "syscall"
Colin Crossce525352019-06-08 18:58:00 -070025
26 "android/soong/ui/status"
27)
28
29type smartStatusOutput struct {
Colin Cross097ed2a2019-06-08 21:48:58 -070030 writer io.Writer
Colin Crossce525352019-06-08 18:58:00 -070031 formatter formatter
32
33 lock sync.Mutex
34
35 haveBlankLine bool
Colin Cross4355ee62019-06-11 23:01:36 -070036
37 termWidth int
38 sigwinch chan os.Signal
39 sigwinchHandled chan bool
Colin Crossce525352019-06-08 18:58:00 -070040}
41
42// NewSmartStatusOutput returns a StatusOutput that represents the
43// current build status similarly to Ninja's built-in terminal
44// output.
Colin Cross097ed2a2019-06-08 21:48:58 -070045func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
Colin Cross4355ee62019-06-11 23:01:36 -070046 s := &smartStatusOutput{
Colin Crossce525352019-06-08 18:58:00 -070047 writer: w,
48 formatter: formatter,
49
50 haveBlankLine: true,
Colin Cross4355ee62019-06-11 23:01:36 -070051
52 sigwinch: make(chan os.Signal),
Colin Crossce525352019-06-08 18:58:00 -070053 }
Colin Cross4355ee62019-06-11 23:01:36 -070054
55 s.updateTermSize()
56
57 s.startSigwinch()
58
59 return s
Colin Crossce525352019-06-08 18:58:00 -070060}
61
62func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
63 if level < status.StatusLvl {
64 return
65 }
66
67 str := s.formatter.message(level, message)
68
69 s.lock.Lock()
70 defer s.lock.Unlock()
71
72 if level > status.StatusLvl {
73 s.print(str)
74 } else {
75 s.statusLine(str)
76 }
77}
78
79func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
80 str := action.Description
81 if str == "" {
82 str = action.Command
83 }
84
85 progress := s.formatter.progress(counts)
86
87 s.lock.Lock()
88 defer s.lock.Unlock()
89
90 s.statusLine(progress + str)
91}
92
93func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
94 str := result.Description
95 if str == "" {
96 str = result.Command
97 }
98
99 progress := s.formatter.progress(counts) + str
100
101 output := s.formatter.result(result)
102
103 s.lock.Lock()
104 defer s.lock.Unlock()
105
106 if output != "" {
107 s.statusLine(progress)
108 s.requestLine()
109 s.print(output)
110 } else {
111 s.statusLine(progress)
112 }
113}
114
115func (s *smartStatusOutput) Flush() {
116 s.lock.Lock()
117 defer s.lock.Unlock()
118
Colin Cross4355ee62019-06-11 23:01:36 -0700119 s.stopSigwinch()
120
Colin Crossce525352019-06-08 18:58:00 -0700121 s.requestLine()
122}
123
Colin Crosse0df1a32019-06-09 19:40:08 -0700124func (s *smartStatusOutput) Write(p []byte) (int, error) {
125 s.lock.Lock()
126 defer s.lock.Unlock()
127 s.print(string(p))
128 return len(p), nil
129}
130
Colin Crossce525352019-06-08 18:58:00 -0700131func (s *smartStatusOutput) requestLine() {
132 if !s.haveBlankLine {
133 fmt.Fprintln(s.writer)
134 s.haveBlankLine = true
135 }
136}
137
138func (s *smartStatusOutput) print(str string) {
139 if !s.haveBlankLine {
140 fmt.Fprint(s.writer, "\r", "\x1b[K")
141 s.haveBlankLine = true
142 }
143 fmt.Fprint(s.writer, str)
144 if len(str) == 0 || str[len(str)-1] != '\n' {
145 fmt.Fprint(s.writer, "\n")
146 }
147}
148
149func (s *smartStatusOutput) statusLine(str string) {
150 idx := strings.IndexRune(str, '\n')
151 if idx != -1 {
152 str = str[0:idx]
153 }
154
155 // Limit line width to the terminal width, otherwise we'll wrap onto
156 // another line and we won't delete the previous line.
Colin Cross4355ee62019-06-11 23:01:36 -0700157 if s.termWidth > 0 {
158 str = s.elide(str)
Colin Crossce525352019-06-08 18:58:00 -0700159 }
160
Colin Cross00bdfd82019-06-11 11:16:23 -0700161 // Move to the beginning on the line, turn on bold, print the output,
162 // turn off bold, then clear the rest of the line.
163 start := "\r\x1b[1m"
164 end := "\x1b[0m\x1b[K"
165 fmt.Fprint(s.writer, start, str, end)
Colin Crossce525352019-06-08 18:58:00 -0700166 s.haveBlankLine = false
167}
Colin Cross4355ee62019-06-11 23:01:36 -0700168
169func (s *smartStatusOutput) elide(str string) string {
170 if len(str) > s.termWidth {
171 // TODO: Just do a max. Ninja elides the middle, but that's
172 // more complicated and these lines aren't that important.
173 str = str[:s.termWidth]
174 }
175
176 return str
177}
178
179func (s *smartStatusOutput) startSigwinch() {
180 signal.Notify(s.sigwinch, syscall.SIGWINCH)
181 go func() {
182 for _ = range s.sigwinch {
183 s.lock.Lock()
184 s.updateTermSize()
185 s.lock.Unlock()
186 if s.sigwinchHandled != nil {
187 s.sigwinchHandled <- true
188 }
189 }
190 }()
191}
192
193func (s *smartStatusOutput) stopSigwinch() {
194 signal.Stop(s.sigwinch)
195 close(s.sigwinch)
196}
197
198func (s *smartStatusOutput) updateTermSize() {
199 if w, ok := termWidth(s.writer); ok {
200 s.termWidth = w
201 }
202}