mirror of
https://github.com/octoleo/restic.git
synced 2024-11-15 17:47:21 +00:00
commit
cf1fb50f9c
@ -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,
|
||||||
|
@ -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:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user