1// 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.smartTerminal = isTerminal(stdio.Stdout()) 85 } 86 w.stripEscapes = !w.smartTerminal 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.Stdout(), str) 131 if len(str) == 0 || str[len(str)-1] != '\n' { 132 fmt.Fprint(w.stdio.Stdout(), "\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{} 230