lib/terminal: factor from cmd/progress, swap Azure/go-ansiterm for mattn/go-colorable

This commit is contained in:
Nick Craig-Wood 2019-09-26 21:14:47 +01:00
parent c78d1dd18b
commit 593de059be
4 changed files with 120 additions and 106 deletions

View file

@ -5,7 +5,6 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -13,7 +12,7 @@ import (
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/log" "github.com/rclone/rclone/fs/log"
"golang.org/x/crypto/ssh/terminal" "github.com/rclone/rclone/lib/terminal"
) )
const ( const (
@ -23,34 +22,10 @@ const (
logTimeFormat = "2006-01-02 15:04:05" 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 // startProgress starts the progress bar printing
// //
// It returns a func which should be called to stop the stats. // It returns a func which should be called to stop the stats.
func startProgress() func() { 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{}) stopStats := make(chan struct{})
oldLogPrint := fs.LogPrint oldLogPrint := fs.LogPrint
if !log.Redirected() { 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 // state for the progress printing
var ( var (
nlines = 0 // number of lines in the previous stats block nlines = 0 // number of lines in the previous stats block
@ -107,11 +75,7 @@ func printProgress(logMessage string) {
defer progressMu.Unlock() defer progressMu.Unlock()
var buf bytes.Buffer var buf bytes.Buffer
w, h, err := terminal.GetSize(int(os.Stdout.Fd())) w, _ := terminal.GetSize()
if err != nil {
w, h = 80, 25
}
_ = h
stats := strings.TrimSpace(accounting.GlobalStats().String()) stats := strings.TrimSpace(accounting.GlobalStats().String())
logMessage = strings.TrimSpace(logMessage) logMessage = strings.TrimSpace(logMessage)
@ -121,17 +85,17 @@ func printProgress(logMessage string) {
if logMessage != "" { if logMessage != "" {
out("\n") out("\n")
out(moveUp) out(terminal.MoveUp)
} }
// Move to the start of the block we wrote erasing all the previous lines // Move to the start of the block we wrote erasing all the previous lines
for i := 0; i < nlines-1; i++ { for i := 0; i < nlines-1; i++ {
out(eraseLine) out(terminal.EraseLine)
out(moveUp) out(terminal.MoveUp)
} }
out(eraseLine) out(terminal.EraseLine)
out(moveToStartOfLine) out(terminal.MoveToStartOfLine)
if logMessage != "" { if logMessage != "" {
out(eraseLine) out(terminal.EraseLine)
out(logMessage + "\n") out(logMessage + "\n")
} }
fixedLines := strings.Split(stats, "\n") fixedLines := strings.Split(stats, "\n")
@ -145,5 +109,5 @@ func printProgress(logMessage string) {
out("\n") out("\n")
} }
} }
writeToTerminal(buf.Bytes()) terminal.Write(buf.Bytes())
} }

View file

@ -1,9 +0,0 @@
//+build !windows
package cmd
func init() {
// Default terminal is VT100 for non Windows
initTerminal = initTerminalVT100
writeToTerminal = writeToTerminalVT100
}

View file

@ -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)
}
}

111
lib/terminal/terminal.go Normal file
View file

@ -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)
}