lib/terminal: factor from cmd/progress, swap Azure/go-ansiterm for mattn/go-colorable
This commit is contained in:
parent
c78d1dd18b
commit
593de059be
4 changed files with 120 additions and 106 deletions
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
//+build !windows
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Default terminal is VT100 for non Windows
|
|
||||||
initTerminal = initTerminalVT100
|
|
||||||
writeToTerminal = writeToTerminalVT100
|
|
||||||
}
|
|
|
@ -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
111
lib/terminal/terminal.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue