• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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