2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-15 17:47:21 +00:00

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, processed.Files, formatBytes(processed.Bytes), errors,
) )
} else { } else {
var eta string var eta, percent string
if secs > 0 && processed.Bytes < total.Bytes { if secs > 0 && processed.Bytes < total.Bytes {
eta = fmt.Sprintf(" ETA %s", formatSeconds(secs)) eta = fmt.Sprintf(" ETA %s", formatSeconds(secs))
percent = formatPercent(processed.Bytes, total.Bytes)
percent += " "
} }
// include totals // 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)), formatDuration(time.Since(b.start)),
formatPercent(processed.Bytes, total.Bytes), percent,
processed.Files, processed.Files,
formatBytes(processed.Bytes), formatBytes(processed.Bytes),
total.Files, total.Files,

View File

@ -21,10 +21,14 @@ type Terminal struct {
msg chan message msg chan message
status chan status status chan status
canUpdateStatus bool 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 { type message struct {
line string line string
@ -53,6 +57,7 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
buf: bytes.NewBuffer(nil), buf: bytes.NewBuffer(nil),
msg: make(chan message), msg: make(chan message),
status: make(chan status), status: make(chan status),
closed: make(chan struct{}),
} }
if disableStatus { 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. // only use the fancy status code when we're running on a real terminal.
t.canUpdateStatus = true t.canUpdateStatus = true
t.fd = d.Fd() t.fd = d.Fd()
t.clearLines = clearLines(wr, t.fd) t.clearCurrentLine = clearCurrentLine(wr, t.fd)
t.moveCursorUp = moveCursorUp(wr, t.fd)
} }
return t 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 // Run updates the screen. It should be run in a separate goroutine. When
// ctx is cancelled, the status lines are cleanly removed. // ctx is cancelled, the status lines are cleanly removed.
func (t *Terminal) Run(ctx context.Context) { func (t *Terminal) Run(ctx context.Context) {
defer close(t.closed)
if t.canUpdateStatus { if t.canUpdateStatus {
t.run(ctx) t.run(ctx)
return return
@ -80,23 +87,13 @@ func (t *Terminal) Run(ctx context.Context) {
t.runWithoutStatus(ctx) 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 { type stringWriter interface {
WriteString(string) (int, error) WriteString(string) (int, error)
} }
// run listens on the channels and updates the terminal screen. // run listens on the channels and updates the terminal screen.
func (t *Terminal) run(ctx context.Context) { func (t *Terminal) run(ctx context.Context) {
statusBuf := bytes.NewBuffer(nil) var status []string
statusLines := 0
for { for {
select { select {
case <-ctx.Done(): 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 // ignore all messages, do nothing, we are in the background process group
continue continue
} }
t.undoStatus(statusLines) t.undoStatus(len(status))
err := t.wr.Flush()
if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
return return
@ -118,14 +110,14 @@ func (t *Terminal) run(ctx context.Context) {
// ignore all messages, do nothing, we are in the background process group // ignore all messages, do nothing, we are in the background process group
continue continue
} }
t.undoStatus(statusLines) t.clearCurrentLine(t.wr, t.fd)
var dst io.Writer var dst io.Writer
if msg.err { if msg.err {
dst = t.errWriter dst = t.errWriter
// assume t.wr and t.errWriter are different, so we need to // 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() err := t.wr.Flush()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
@ -146,10 +138,7 @@ func (t *Terminal) run(ctx context.Context) {
continue continue
} }
_, err = t.wr.Write(statusBuf.Bytes()) t.writeStatus(status)
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
}
err = t.wr.Flush() err = t.wr.Flush()
if err != nil { 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 // ignore all messages, do nothing, we are in the background process group
continue continue
} }
t.undoStatus(statusLines)
statusBuf.Reset() status = status[:0]
for _, line := range stat.lines { status = append(status, stat.lines...)
statusBuf.WriteString(line) t.writeStatus(status)
}
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)
}
} }
} }
} }
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, // runWithoutStatus listens on the channels and just prints out the messages,
// without status lines. // without status lines.
func (t *Terminal) runWithoutStatus(ctx context.Context) { func (t *Terminal) runWithoutStatus(ctx context.Context) {
@ -227,12 +229,27 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) {
} }
func (t *Terminal) undoStatus(lines int) { func (t *Terminal) undoStatus(lines int) {
if lines == 0 { for i := 0; i < lines; i++ {
return 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.moveCursorUp(t.wr, t.fd, lines)
t.clearLines(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. // Print writes a line to the terminal.
@ -242,7 +259,10 @@ func (t *Terminal) Print(line string) {
line += "\n" 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. // Printf uses fmt.Sprintf to write a line to the terminal.
@ -258,7 +278,10 @@ func (t *Terminal) Error(line string) {
line += "\n" 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. // 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 last := len(lines) - 1
lines[last] = strings.TrimRight(lines[last], "\n") 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 package termstatus
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
) )
const ( const (
posixMoveCursorHome = "\r" posixControlMoveCursorHome = "\r"
posixMoveCursorUp = "\x1b[1A" posixControlMoveCursorUp = "\x1b[1A"
posixClearLine = "\x1b[2K" posixControlClearLine = "\x1b[2K"
) )
// posixClearLines will clear the current line and the n lines above. // posixClearCurrentLine removes all characters from the current line and resets the
// Afterwards the cursor is positioned at the start of the first cleared line. // cursor position to the first column.
func posixClearLines(wr io.Writer, fd uintptr, n int) { func posixClearCurrentLine(wr io.Writer, fd uintptr) {
// clear current line // clear current line
_, err := wr.Write([]byte(posixMoveCursorHome + posixClearLine)) _, err := wr.Write([]byte(posixControlMoveCursorHome + posixControlClearLine))
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err) fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return return
} }
}
for ; n > 0; n-- { // posixMoveCursorUp moves the cursor to the line n lines above the current one.
// clear current line and move on line up func posixMoveCursorUp(wr io.Writer, fd uintptr, n int) {
_, err := wr.Write([]byte(posixMoveCursorUp + posixClearLine)) data := []byte(posixControlMoveCursorHome)
if err != nil { data = append(data, bytes.Repeat([]byte(posixControlMoveCursorUp), n)...)
fmt.Fprintf(os.Stderr, "write failed: %v\n", err) _, err := wr.Write(data)
return 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" isatty "github.com/mattn/go-isatty"
) )
// clearLines will clear the current line and the n lines above. Afterwards the // clearCurrentLine removes all characters from the current line and resets the
// cursor is positioned at the start of the first cleared line. // cursor position to the first column.
func clearLines(wr io.Writer, fd uintptr) clearLinesFunc { func clearCurrentLine(wr io.Writer, fd uintptr) func(io.Writer, uintptr) {
return posixClearLines 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 // canUpdateStatus returns true if status lines can be printed, the process

View File

@ -8,11 +8,29 @@ import (
"unsafe" "unsafe"
) )
// clearLines clears the current line and n lines above it. // clearCurrentLine removes all characters from the current line and resets the
func clearLines(wr io.Writer, fd uintptr) clearLinesFunc { // 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 // easy case, the terminal is cmd or psh, without redirection
if isWindowsTerminal(fd) { 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) // 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 // assume we're running in mintty/cygwin
return posixClearLines return posixMoveCursorUp
} }
var kernel32 = syscall.NewLazyDLL("kernel32.dll") var kernel32 = syscall.NewLazyDLL("kernel32.dll")
@ -60,22 +78,27 @@ type (
} }
) )
// windowsClearLines clears the current line and n lines above it. // windowsClearCurrentLine removes all characters from the current line and
func windowsClearLines(wr io.Writer, fd uintptr, n int) { // resets the cursor position to the first column.
func windowsClearCurrentLine(wr io.Writer, fd uintptr) {
var info consoleScreenBufferInfo var info consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info))) procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info)))
for i := 0; i <= n; i++ { // clear the line
// clear the line cursor := coord{
cursor := coord{ x: info.window.left,
x: info.window.left, y: info.cursorPosition.y,
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)))
} }
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 // move cursor up by n lines and to the first column
info.cursorPosition.y -= short(n) info.cursorPosition.y -= short(n)