77fe361175
Bug: 111447427 Test: m nothing Test: TERM=dumb m nothing Test: m nothing | tee log Change-Id: I041ec8a60e633393a145996d9ba7b5dae2880b06
229 lines
6 KiB
Go
229 lines
6 KiB
Go
// Copyright 2018 Google Inc. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package terminal provides a set of interfaces that can be used to interact
|
|
// with the terminal (including falling back when the terminal is detected to
|
|
// be a redirect or other dumb terminal)
|
|
package terminal
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Writer provides an interface to write temporary and permanent messages to
|
|
// the terminal.
|
|
//
|
|
// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
|
|
// terminal isn't detected on stdout/stderr (generally because it's a pipe or
|
|
// file). Dumb terminals will strip out all ANSI escape sequences, including
|
|
// colors.
|
|
type Writer interface {
|
|
// Print prints the string to the terminal, overwriting any current
|
|
// status being displayed.
|
|
//
|
|
// On a dumb terminal, the status messages will be kept.
|
|
Print(str string)
|
|
|
|
// Status prints the first line of the string to the terminal,
|
|
// overwriting any previous status line. Strings longer than the width
|
|
// of the terminal will be cut off.
|
|
//
|
|
// On a dumb terminal, previous status messages will remain, and the
|
|
// entire first line of the string will be printed.
|
|
StatusLine(str string)
|
|
|
|
// StatusAndMessage prints the first line of status to the terminal,
|
|
// similarly to StatusLine(), then prints the full msg below that. The
|
|
// status line is retained.
|
|
//
|
|
// There is guaranteed to be no other output in between the status and
|
|
// message.
|
|
StatusAndMessage(status, msg string)
|
|
|
|
// Finish ensures that the output ends with a newline (preserving any
|
|
// current status line that is current displayed).
|
|
//
|
|
// This does nothing on dumb terminals.
|
|
Finish()
|
|
|
|
// Write implements the io.Writer interface. This is primarily so that
|
|
// the logger can use this interface to print to stderr without
|
|
// breaking the other semantics of this interface.
|
|
//
|
|
// Try to use any of the other functions if possible.
|
|
Write(p []byte) (n int, err error)
|
|
|
|
isSmartTerminal() bool
|
|
}
|
|
|
|
// NewWriter creates a new Writer based on the stdio and the TERM
|
|
// environment variable.
|
|
func NewWriter(stdio StdioInterface) Writer {
|
|
w := &writerImpl{
|
|
stdio: stdio,
|
|
|
|
haveBlankLine: true,
|
|
}
|
|
|
|
if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
|
|
w.smartTerminal = isTerminal(stdio.Stdout())
|
|
}
|
|
w.stripEscapes = !w.smartTerminal
|
|
|
|
return w
|
|
}
|
|
|
|
type writerImpl struct {
|
|
stdio StdioInterface
|
|
|
|
haveBlankLine bool
|
|
|
|
// Protecting the above, we assume that smartTerminal and stripEscapes
|
|
// does not change after initial setup.
|
|
lock sync.Mutex
|
|
|
|
smartTerminal bool
|
|
stripEscapes bool
|
|
}
|
|
|
|
func (w *writerImpl) isSmartTerminal() bool {
|
|
return w.smartTerminal
|
|
}
|
|
|
|
func (w *writerImpl) requestLine() {
|
|
if !w.haveBlankLine {
|
|
fmt.Fprintln(w.stdio.Stdout())
|
|
w.haveBlankLine = true
|
|
}
|
|
}
|
|
|
|
func (w *writerImpl) Print(str string) {
|
|
if w.stripEscapes {
|
|
str = string(stripAnsiEscapes([]byte(str)))
|
|
}
|
|
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
w.print(str)
|
|
}
|
|
|
|
func (w *writerImpl) print(str string) {
|
|
if !w.haveBlankLine {
|
|
fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
|
|
w.haveBlankLine = true
|
|
}
|
|
fmt.Fprint(w.stdio.Stdout(), str)
|
|
if len(str) == 0 || str[len(str)-1] != '\n' {
|
|
fmt.Fprint(w.stdio.Stdout(), "\n")
|
|
}
|
|
}
|
|
|
|
func (w *writerImpl) StatusLine(str string) {
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
|
|
w.statusLine(str)
|
|
}
|
|
|
|
func (w *writerImpl) statusLine(str string) {
|
|
if !w.smartTerminal {
|
|
fmt.Fprintln(w.stdio.Stdout(), str)
|
|
return
|
|
}
|
|
|
|
idx := strings.IndexRune(str, '\n')
|
|
if idx != -1 {
|
|
str = str[0:idx]
|
|
}
|
|
|
|
// Limit line width to the terminal width, otherwise we'll wrap onto
|
|
// another line and we won't delete the previous line.
|
|
//
|
|
// Run this on every line in case the window has been resized while
|
|
// we're printing. This could be optimized to only re-run when we get
|
|
// SIGWINCH if it ever becomes too time consuming.
|
|
if max, ok := termWidth(w.stdio.Stdout()); ok {
|
|
if len(str) > max {
|
|
// TODO: Just do a max. Ninja elides the middle, but that's
|
|
// more complicated and these lines aren't that important.
|
|
str = str[:max]
|
|
}
|
|
}
|
|
|
|
// Move to the beginning on the line, print the output, then clear
|
|
// the rest of the line.
|
|
fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
|
|
w.haveBlankLine = false
|
|
}
|
|
|
|
func (w *writerImpl) StatusAndMessage(status, msg string) {
|
|
if w.stripEscapes {
|
|
msg = string(stripAnsiEscapes([]byte(msg)))
|
|
}
|
|
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
|
|
w.statusLine(status)
|
|
w.requestLine()
|
|
w.print(msg)
|
|
}
|
|
|
|
func (w *writerImpl) Finish() {
|
|
w.lock.Lock()
|
|
defer w.lock.Unlock()
|
|
|
|
w.requestLine()
|
|
}
|
|
|
|
func (w *writerImpl) Write(p []byte) (n int, err error) {
|
|
w.Print(string(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
|
|
type StdioInterface interface {
|
|
Stdin() io.Reader
|
|
Stdout() io.Writer
|
|
Stderr() io.Writer
|
|
}
|
|
|
|
// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
|
|
type StdioImpl struct{}
|
|
|
|
func (StdioImpl) Stdin() io.Reader { return os.Stdin }
|
|
func (StdioImpl) Stdout() io.Writer { return os.Stdout }
|
|
func (StdioImpl) Stderr() io.Writer { return os.Stderr }
|
|
|
|
var _ StdioInterface = StdioImpl{}
|
|
|
|
type customStdio struct {
|
|
stdin io.Reader
|
|
stdout io.Writer
|
|
stderr io.Writer
|
|
}
|
|
|
|
func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
|
|
return customStdio{stdin, stdout, stderr}
|
|
}
|
|
|
|
func (c customStdio) Stdin() io.Reader { return c.stdin }
|
|
func (c customStdio) Stdout() io.Writer { return c.stdout }
|
|
func (c customStdio) Stderr() io.Writer { return c.stderr }
|
|
|
|
var _ StdioInterface = customStdio{}
|