Merge pull request #1764 from restic/fix-display

Fix display
This commit is contained in:
Alexander Neumann 2018-05-07 23:30:50 +02:00
commit cf1fb50f9c
5 changed files with 146 additions and 87 deletions

View File

@ -150,16 +150,18 @@ func (b *Backup) update(total, processed counter, errors uint, currentFiles map[
processed.Files, formatBytes(processed.Bytes), errors,
)
} else {
var eta string
var eta, percent string
if secs > 0 && processed.Bytes < total.Bytes {
eta = fmt.Sprintf(" ETA %s", formatSeconds(secs))
percent = formatPercent(processed.Bytes, total.Bytes)
percent += " "
}
// include totals
status = fmt.Sprintf("[%s] %s %v files %s, total %v files %v, %d errors%s",
status = fmt.Sprintf("[%s] %s%v files %s, total %v files %v, %d errors%s",
formatDuration(time.Since(b.start)),
formatPercent(processed.Bytes, total.Bytes),
percent,
processed.Files,
formatBytes(processed.Bytes),
total.Files,

View File

@ -21,10 +21,14 @@ type Terminal struct {
msg chan message
status chan status
canUpdateStatus bool
clearLines clearLinesFunc
}
type clearLinesFunc func(wr io.Writer, fd uintptr, n int)
// will be closed when the goroutine which runs Run() terminates, so it'll
// yield a default value immediately
closed chan struct{}
clearCurrentLine func(io.Writer, uintptr)
moveCursorUp func(io.Writer, uintptr, int)
}
type message struct {
line string
@ -53,6 +57,7 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
buf: bytes.NewBuffer(nil),
msg: make(chan message),
status: make(chan status),
closed: make(chan struct{}),
}
if disableStatus {
@ -63,7 +68,8 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
// only use the fancy status code when we're running on a real terminal.
t.canUpdateStatus = true
t.fd = d.Fd()
t.clearLines = clearLines(wr, t.fd)
t.clearCurrentLine = clearCurrentLine(wr, t.fd)
t.moveCursorUp = moveCursorUp(wr, t.fd)
}
return t
@ -72,6 +78,7 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
// Run updates the screen. It should be run in a separate goroutine. When
// ctx is cancelled, the status lines are cleanly removed.
func (t *Terminal) Run(ctx context.Context) {
defer close(t.closed)
if t.canUpdateStatus {
t.run(ctx)
return
@ -80,23 +87,13 @@ func (t *Terminal) Run(ctx context.Context) {
t.runWithoutStatus(ctx)
}
func countLines(buf []byte) int {
lines := 0
sc := bufio.NewScanner(bytes.NewReader(buf))
for sc.Scan() {
lines++
}
return lines
}
type stringWriter interface {
WriteString(string) (int, error)
}
// run listens on the channels and updates the terminal screen.
func (t *Terminal) run(ctx context.Context) {
statusBuf := bytes.NewBuffer(nil)
statusLines := 0
var status []string
for {
select {
case <-ctx.Done():
@ -104,12 +101,7 @@ func (t *Terminal) run(ctx context.Context) {
// ignore all messages, do nothing, we are in the background process group
continue
}
t.undoStatus(statusLines)
err := t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
t.undoStatus(len(status))
return
@ -118,14 +110,14 @@ func (t *Terminal) run(ctx context.Context) {
// ignore all messages, do nothing, we are in the background process group
continue
}
t.undoStatus(statusLines)
t.clearCurrentLine(t.wr, t.fd)
var dst io.Writer
if msg.err {
dst = t.errWriter
// assume t.wr and t.errWriter are different, so we need to
// flush the removal of the status lines first.
// flush clearing the current line
err := t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
@ -146,10 +138,7 @@ func (t *Terminal) run(ctx context.Context) {
continue
}
_, err = t.wr.Write(statusBuf.Bytes())
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
}
t.writeStatus(status)
err = t.wr.Flush()
if err != nil {
@ -161,27 +150,40 @@ func (t *Terminal) run(ctx context.Context) {
// ignore all messages, do nothing, we are in the background process group
continue
}
t.undoStatus(statusLines)
statusBuf.Reset()
for _, line := range stat.lines {
statusBuf.WriteString(line)
}
statusLines = len(stat.lines)
_, err := t.wr.Write(statusBuf.Bytes())
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
}
err = t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
status = status[:0]
status = append(status, stat.lines...)
t.writeStatus(status)
}
}
}
func (t *Terminal) writeStatus(status []string) {
for _, line := range status {
t.clearCurrentLine(t.wr, t.fd)
_, err := t.wr.WriteString(line)
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
}
// flush is needed so that the current line is updated
err = t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
}
if len(status) > 0 {
t.moveCursorUp(t.wr, t.fd, len(status)-1)
}
err := t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
}
// runWithoutStatus listens on the channels and just prints out the messages,
// without status lines.
func (t *Terminal) runWithoutStatus(ctx context.Context) {
@ -227,12 +229,27 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) {
}
func (t *Terminal) undoStatus(lines int) {
if lines == 0 {
return
for i := 0; i < lines; i++ {
t.clearCurrentLine(t.wr, t.fd)
_, err := t.wr.WriteRune('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
}
// flush is needed so that the current line is updated
err = t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
}
lines--
t.clearLines(t.wr, t.fd, lines)
t.moveCursorUp(t.wr, t.fd, lines)
err := t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
}
// Print writes a line to the terminal.
@ -242,7 +259,10 @@ func (t *Terminal) Print(line string) {
line += "\n"
}
t.msg <- message{line: line}
select {
case t.msg <- message{line: line}:
case <-t.closed:
}
}
// Printf uses fmt.Sprintf to write a line to the terminal.
@ -258,7 +278,10 @@ func (t *Terminal) Error(line string) {
line += "\n"
}
t.msg <- message{line: line, err: true}
select {
case t.msg <- message{line: line, err: true}:
case <-t.closed:
}
}
// Errorf uses fmt.Sprintf to write an error line to the terminal.
@ -294,5 +317,8 @@ func (t *Terminal) SetStatus(lines []string) {
last := len(lines) - 1
lines[last] = strings.TrimRight(lines[last], "\n")
t.status <- status{lines: lines}
select {
case t.status <- status{lines: lines}:
case <-t.closed:
}
}

View File

@ -1,33 +1,36 @@
package termstatus
import (
"bytes"
"fmt"
"io"
"os"
)
const (
posixMoveCursorHome = "\r"
posixMoveCursorUp = "\x1b[1A"
posixClearLine = "\x1b[2K"
posixControlMoveCursorHome = "\r"
posixControlMoveCursorUp = "\x1b[1A"
posixControlClearLine = "\x1b[2K"
)
// posixClearLines will clear the current line and the n lines above.
// Afterwards the cursor is positioned at the start of the first cleared line.
func posixClearLines(wr io.Writer, fd uintptr, n int) {
// posixClearCurrentLine removes all characters from the current line and resets the
// cursor position to the first column.
func posixClearCurrentLine(wr io.Writer, fd uintptr) {
// clear current line
_, err := wr.Write([]byte(posixMoveCursorHome + posixClearLine))
_, err := wr.Write([]byte(posixControlMoveCursorHome + posixControlClearLine))
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}
for ; n > 0; n-- {
// clear current line and move on line up
_, err := wr.Write([]byte(posixMoveCursorUp + posixClearLine))
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
// posixMoveCursorUp moves the cursor to the line n lines above the current one.
func posixMoveCursorUp(wr io.Writer, fd uintptr, n int) {
data := []byte(posixControlMoveCursorHome)
data = append(data, bytes.Repeat([]byte(posixControlMoveCursorUp), n)...)
_, err := wr.Write(data)
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}

View File

@ -10,10 +10,15 @@ import (
isatty "github.com/mattn/go-isatty"
)
// clearLines will clear the current line and the n lines above. Afterwards the
// cursor is positioned at the start of the first cleared line.
func clearLines(wr io.Writer, fd uintptr) clearLinesFunc {
return posixClearLines
// clearCurrentLine removes all characters from the current line and resets the
// cursor position to the first column.
func clearCurrentLine(wr io.Writer, fd uintptr) func(io.Writer, uintptr) {
return posixClearCurrentLine
}
// moveCursorUp moves the cursor to the line n lines above the current one.
func moveCursorUp(wr io.Writer, fd uintptr) func(io.Writer, uintptr, int) {
return posixMoveCursorUp
}
// canUpdateStatus returns true if status lines can be printed, the process

View File

@ -8,11 +8,29 @@ import (
"unsafe"
)
// clearLines clears the current line and n lines above it.
func clearLines(wr io.Writer, fd uintptr) clearLinesFunc {
// clearCurrentLine removes all characters from the current line and resets the
// cursor position to the first column.
func clearCurrentLine(wr io.Writer, fd uintptr) func(io.Writer, uintptr) {
// easy case, the terminal is cmd or psh, without redirection
if isWindowsTerminal(fd) {
return windowsClearLines
return windowsClearCurrentLine
}
// check if the output file type is a pipe (0x0003)
if getFileType(fd) != fileTypePipe {
// return empty func, update state is not possible on this terminal
return func(io.Writer, uintptr) {}
}
// assume we're running in mintty/cygwin
return posixClearCurrentLine
}
// moveCursorUp moves the cursor to the line n lines above the current one.
func moveCursorUp(wr io.Writer, fd uintptr) func(io.Writer, uintptr, int) {
// easy case, the terminal is cmd or psh, without redirection
if isWindowsTerminal(fd) {
return windowsMoveCursorUp
}
// check if the output file type is a pipe (0x0003)
@ -22,7 +40,7 @@ func clearLines(wr io.Writer, fd uintptr) clearLinesFunc {
}
// assume we're running in mintty/cygwin
return posixClearLines
return posixMoveCursorUp
}
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
@ -60,22 +78,27 @@ type (
}
)
// windowsClearLines clears the current line and n lines above it.
func windowsClearLines(wr io.Writer, fd uintptr, n int) {
// windowsClearCurrentLine removes all characters from the current line and
// resets the cursor position to the first column.
func windowsClearCurrentLine(wr io.Writer, fd uintptr) {
var info consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info)))
for i := 0; i <= n; i++ {
// clear the line
cursor := coord{
x: info.window.left,
y: info.cursorPosition.y - short(i),
}
var count, w dword
count = dword(info.size.x)
procFillConsoleOutputAttribute.Call(fd, uintptr(info.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
// clear the line
cursor := coord{
x: info.window.left,
y: info.cursorPosition.y,
}
var count, w dword
count = dword(info.size.x)
procFillConsoleOutputAttribute.Call(fd, uintptr(info.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
}
// windowsMoveCursorUp moves the cursor to the line n lines above the current one.
func windowsMoveCursorUp(wr io.Writer, fd uintptr, n int) {
var info consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info)))
// move cursor up by n lines and to the first column
info.cursorPosition.y -= short(n)