blob: dd322268b61ef9882d1cf3810c74739e7cea5cdc [file] [log] [blame]
Dan Willemsenb82471a2018-05-17 16:37:09 -07001// Copyright 2018 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
15// Package terminal provides a set of interfaces that can be used to interact
16// with the terminal (including falling back when the terminal is detected to
17// be a redirect or other dumb terminal)
18package terminal
19
20import (
21 "fmt"
22 "io"
23 "os"
24 "strings"
25 "sync"
26)
27
28// Writer provides an interface to write temporary and permanent messages to
29// the terminal.
30//
31// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
32// terminal isn't detected on stdout/stderr (generally because it's a pipe or
33// file). Dumb terminals will strip out all ANSI escape sequences, including
34// colors.
35type Writer interface {
36 // Print prints the string to the terminal, overwriting any current
37 // status being displayed.
38 //
39 // On a dumb terminal, the status messages will be kept.
40 Print(str string)
41
42 // Status prints the first line of the string to the terminal,
43 // overwriting any previous status line. Strings longer than the width
44 // of the terminal will be cut off.
45 //
46 // On a dumb terminal, previous status messages will remain, and the
47 // entire first line of the string will be printed.
48 StatusLine(str string)
49
50 // StatusAndMessage prints the first line of status to the terminal,
51 // similarly to StatusLine(), then prints the full msg below that. The
52 // status line is retained.
53 //
54 // There is guaranteed to be no other output in between the status and
55 // message.
56 StatusAndMessage(status, msg string)
57
58 // Finish ensures that the output ends with a newline (preserving any
59 // current status line that is current displayed).
60 //
61 // This does nothing on dumb terminals.
62 Finish()
63
64 // Write implements the io.Writer interface. This is primarily so that
65 // the logger can use this interface to print to stderr without
66 // breaking the other semantics of this interface.
67 //
68 // Try to use any of the other functions if possible.
69 Write(p []byte) (n int, err error)
70
71 isSmartTerminal() bool
72}
73
74// NewWriter creates a new Writer based on the stdio and the TERM
75// environment variable.
76func NewWriter(stdio StdioInterface) Writer {
77 w := &writerImpl{
78 stdio: stdio,
79
80 haveBlankLine: true,
81 }
82
83 if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
84 w.stripEscapes = !isTerminal(stdio.Stderr())
85 w.smartTerminal = isTerminal(stdio.Stdout()) && !w.stripEscapes
86 }
87
88 return w
89}
90
91type writerImpl struct {
92 stdio StdioInterface
93
94 haveBlankLine bool
95
96 // Protecting the above, we assume that smartTerminal and stripEscapes
97 // does not change after initial setup.
98 lock sync.Mutex
99
100 smartTerminal bool
101 stripEscapes bool
102}
103
104func (w *writerImpl) isSmartTerminal() bool {
105 return w.smartTerminal
106}
107
108func (w *writerImpl) requestLine() {
109 if !w.haveBlankLine {
110 fmt.Fprintln(w.stdio.Stdout())
111 w.haveBlankLine = true
112 }
113}
114
115func (w *writerImpl) Print(str string) {
116 if w.stripEscapes {
117 str = string(stripAnsiEscapes([]byte(str)))
118 }
119
120 w.lock.Lock()
121 defer w.lock.Unlock()
122 w.print(str)
123}
124
125func (w *writerImpl) print(str string) {
126 if !w.haveBlankLine {
127 fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
128 w.haveBlankLine = true
129 }
130 fmt.Fprint(w.stdio.Stderr(), str)
131 if len(str) == 0 || str[len(str)-1] != '\n' {
132 fmt.Fprint(w.stdio.Stderr(), "\n")
133 }
134}
135
136func (w *writerImpl) StatusLine(str string) {
137 w.lock.Lock()
138 defer w.lock.Unlock()
139
140 w.statusLine(str)
141}
142
143func (w *writerImpl) statusLine(str string) {
144 if !w.smartTerminal {
145 fmt.Fprintln(w.stdio.Stdout(), str)
146 return
147 }
148
149 idx := strings.IndexRune(str, '\n')
150 if idx != -1 {
151 str = str[0:idx]
152 }
153
154 // Limit line width to the terminal width, otherwise we'll wrap onto
155 // another line and we won't delete the previous line.
156 //
157 // Run this on every line in case the window has been resized while
158 // we're printing. This could be optimized to only re-run when we get
159 // SIGWINCH if it ever becomes too time consuming.
160 if max, ok := termWidth(w.stdio.Stdout()); ok {
161 if len(str) > max {
162 // TODO: Just do a max. Ninja elides the middle, but that's
163 // more complicated and these lines aren't that important.
164 str = str[:max]
165 }
166 }
167
168 // Move to the beginning on the line, print the output, then clear
169 // the rest of the line.
170 fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
171 w.haveBlankLine = false
172}
173
174func (w *writerImpl) StatusAndMessage(status, msg string) {
175 if w.stripEscapes {
176 msg = string(stripAnsiEscapes([]byte(msg)))
177 }
178
179 w.lock.Lock()
180 defer w.lock.Unlock()
181
182 w.statusLine(status)
183 w.requestLine()
184 w.print(msg)
185}
186
187func (w *writerImpl) Finish() {
188 w.lock.Lock()
189 defer w.lock.Unlock()
190
191 w.requestLine()
192}
193
194func (w *writerImpl) Write(p []byte) (n int, err error) {
195 w.Print(string(p))
196 return len(p), nil
197}
198
199// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
200type StdioInterface interface {
201 Stdin() io.Reader
202 Stdout() io.Writer
203 Stderr() io.Writer
204}
205
206// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
207type StdioImpl struct{}
208
209func (StdioImpl) Stdin() io.Reader { return os.Stdin }
210func (StdioImpl) Stdout() io.Writer { return os.Stdout }
211func (StdioImpl) Stderr() io.Writer { return os.Stderr }
212
213var _ StdioInterface = StdioImpl{}
214
215type customStdio struct {
216 stdin io.Reader
217 stdout io.Writer
218 stderr io.Writer
219}
220
221func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
222 return customStdio{stdin, stdout, stderr}
223}
224
225func (c customStdio) Stdin() io.Reader { return c.stdin }
226func (c customStdio) Stdout() io.Writer { return c.stdout }
227func (c customStdio) Stderr() io.Writer { return c.stderr }
228
229var _ StdioInterface = customStdio{}