From 593de059be69e4f7612cdc57ddc6783c2ce25be5 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 26 Sep 2019 21:14:47 +0100 Subject: [PATCH] lib/terminal: factor from cmd/progress, swap Azure/go-ansiterm for mattn/go-colorable --- cmd/progress.go | 54 ++++--------------- cmd/progress_other.go | 9 ---- cmd/progress_windows.go | 52 ------------------ lib/terminal/terminal.go | 111 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 106 deletions(-) delete mode 100644 cmd/progress_other.go delete mode 100644 cmd/progress_windows.go create mode 100644 lib/terminal/terminal.go diff --git a/cmd/progress.go b/cmd/progress.go index 124d99108..0ea120371 100644 --- a/cmd/progress.go +++ b/cmd/progress.go @@ -5,7 +5,6 @@ package cmd import ( "bytes" "fmt" - "os" "strings" "sync" "time" @@ -13,7 +12,7 @@ import ( "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/log" - "golang.org/x/crypto/ssh/terminal" + "github.com/rclone/rclone/lib/terminal" ) const ( @@ -23,34 +22,10 @@ const ( logTimeFormat = "2006-01-02 15:04:05" ) -var ( - initTerminal func() error - writeToTerminal func([]byte) -) - -// Initialise the VT100 terminal -func initTerminalVT100() error { - return nil -} - -// Write to the VT100 terminal -func writeToTerminalVT100(b []byte) { - _, _ = os.Stdout.Write(b) -} - // startProgress starts the progress bar printing // // It returns a func which should be called to stop the stats. func startProgress() func() { - if os.Getenv("TERM") != "" { - initTerminal = initTerminalVT100 - writeToTerminal = writeToTerminalVT100 - } - err := initTerminal() - if err != nil { - fs.Errorf(nil, "Failed to start progress: %v", err) - return func() {} - } stopStats := make(chan struct{}) oldLogPrint := fs.LogPrint if !log.Redirected() { @@ -88,13 +63,6 @@ func startProgress() func() { } } -// VT100 codes -const ( - eraseLine = "\x1b[2K" - moveToStartOfLine = "\x1b[0G" - moveUp = "\x1b[A" -) - // state for the progress printing var ( nlines = 0 // number of lines in the previous stats block @@ -107,11 +75,7 @@ func printProgress(logMessage string) { defer progressMu.Unlock() var buf bytes.Buffer - w, h, err := terminal.GetSize(int(os.Stdout.Fd())) - if err != nil { - w, h = 80, 25 - } - _ = h + w, _ := terminal.GetSize() stats := strings.TrimSpace(accounting.GlobalStats().String()) logMessage = strings.TrimSpace(logMessage) @@ -121,17 +85,17 @@ func printProgress(logMessage string) { if logMessage != "" { out("\n") - out(moveUp) + out(terminal.MoveUp) } // Move to the start of the block we wrote erasing all the previous lines for i := 0; i < nlines-1; i++ { - out(eraseLine) - out(moveUp) + out(terminal.EraseLine) + out(terminal.MoveUp) } - out(eraseLine) - out(moveToStartOfLine) + out(terminal.EraseLine) + out(terminal.MoveToStartOfLine) if logMessage != "" { - out(eraseLine) + out(terminal.EraseLine) out(logMessage + "\n") } fixedLines := strings.Split(stats, "\n") @@ -145,5 +109,5 @@ func printProgress(logMessage string) { out("\n") } } - writeToTerminal(buf.Bytes()) + terminal.Write(buf.Bytes()) } diff --git a/cmd/progress_other.go b/cmd/progress_other.go deleted file mode 100644 index ccefc1773..000000000 --- a/cmd/progress_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//+build !windows - -package cmd - -func init() { - // Default terminal is VT100 for non Windows - initTerminal = initTerminalVT100 - writeToTerminal = writeToTerminalVT100 -} diff --git a/cmd/progress_windows.go b/cmd/progress_windows.go deleted file mode 100644 index 141eaeed8..000000000 --- a/cmd/progress_windows.go +++ /dev/null @@ -1,52 +0,0 @@ -//+build windows - -package cmd - -import ( - "fmt" - "os" - "syscall" - - ansiterm "github.com/Azure/go-ansiterm" - "github.com/Azure/go-ansiterm/winterm" - "github.com/pkg/errors" -) - -var ( - ansiParser *ansiterm.AnsiParser -) - -func init() { - // Default terminal is Windows console for Windows - initTerminal = initTerminalWindows - writeToTerminal = writeToTerminalWindows -} - -func initTerminalWindows() error { - winEventHandler := winterm.CreateWinEventHandler(os.Stdout.Fd(), os.Stdout) - if winEventHandler == nil { - err := syscall.GetLastError() - if err == nil { - err = errors.New("initialization failed") - } - return errors.Wrap(err, "windows terminal") - } - ansiParser = ansiterm.CreateParser("Ground", winEventHandler) - return nil -} - -func writeToTerminalWindows(b []byte) { - // Remove all non-ASCII characters until this is fixed - // https://github.com/Azure/go-ansiterm/issues/26 - r := []rune(string(b)) - for i := range r { - if r[i] >= 127 { - r[i] = '.' - } - } - b = []byte(string(r)) - _, err := ansiParser.Parse(b) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "\n*** Error from ANSI parser: %v\n", err) - } -} diff --git a/lib/terminal/terminal.go b/lib/terminal/terminal.go new file mode 100644 index 000000000..9a7c1ab7b --- /dev/null +++ b/lib/terminal/terminal.go @@ -0,0 +1,111 @@ +// Package terminal provides VT100 terminal codes and a windows +// implementation of that. +package terminal + +import ( + "io" + "os" + "runtime" + "sync" + + colorable "github.com/mattn/go-colorable" + "golang.org/x/crypto/ssh/terminal" +) + +// VT100 codes +const ( + EraseLine = "\x1b[2K" + MoveToStartOfLine = "\x1b[1G" + MoveUp = "\x1b[1A" + + Reset = "\x1b[0m" + Bright = "\x1b[1m" + Dim = "\x1b[2m" + Underscore = "\x1b[4m" + Blink = "\x1b[5m" + Reverse = "\x1b[7m" + Hidden = "\x1b[8m" + + BlackFg = "\x1b[30m" + RedFg = "\x1b[31m" + GreenFg = "\x1b[32m" + YellowFg = "\x1b[33m" + BlueFg = "\x1b[34m" + MagentaFg = "\x1b[35m" + CyanFg = "\x1b[36m" + WhiteFg = "\x1b[37m" + + BlackBg = "\x1b[40m" + RedBg = "\x1b[41m" + GreenBg = "\x1b[42m" + YellowBg = "\x1b[43m" + BlueBg = "\x1b[44m" + MagentaBg = "\x1b[45m" + CyanBg = "\x1b[46m" + WhiteBg = "\x1b[47m" + + HiBlackFg = "\x1b[90m" + HiRedFg = "\x1b[91m" + HiGreenFg = "\x1b[92m" + HiYellowFg = "\x1b[93m" + HiBlueFg = "\x1b[94m" + HiMagentaFg = "\x1b[95m" + HiCyanFg = "\x1b[96m" + HiWhiteFg = "\x1b[97m" + + HiBlackBg = "\x1b[100m" + HiRedBg = "\x1b[101m" + HiGreenBg = "\x1b[102m" + HiYellowBg = "\x1b[103m" + HiBlueBg = "\x1b[104m" + HiMagentaBg = "\x1b[105m" + HiCyanBg = "\x1b[106m" + HiWhiteBg = "\x1b[107m" +) + +var ( + // make sure that start is only called once + once sync.Once +) + +// Start the terminal - must be called before use +func Start() { + once.Do(func() { + f := os.Stdout + if !terminal.IsTerminal(int(f.Fd())) { + // If stdout not a tty then remove escape codes + Out = colorable.NewNonColorable(f) + } else if runtime.GOOS == "windows" && os.Getenv("TERM") != "" { + // If TERM is set just use stdout + Out = f + } else { + Out = colorable.NewColorable(f) + } + }) +} + +// WriteString writes the string passed in to the terminal +func WriteString(s string) { + Write([]byte(s)) +} + +// GetSize reads the dimensions of the current terminal or returns a +// sensible default +func GetSize() (w, h int) { + w, h, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + w, h = 80, 25 + } + return w, h +} + +// Out is an io.Writer which can be used to write to the terminal +// eg for use with fmt.Fprintf(terminal.Out, "terminal fun: %d\n", n) +var Out io.Writer + +// Write sends out to the VT100 terminal. +// It will initialise the terminal if this is the first call. +func Write(out []byte) { + Start() + _, _ = Out.Write(out) +}