2
2
mirror of https://github.com/octoleo/restic.git synced 2024-05-29 07:00:49 +00:00

Rework termstatus

This now keeps the cursor at the first column of the first status line
so that messages printed to stdout or stderr by some other part of the
progarm will still be visible. The message will overwrite the status
lines, but those are easily reprinted on the next status update.
This commit is contained in:
Alexander Neumann 2018-05-07 21:40:07 +02:00
parent 4c25495d68
commit b2208bb9c2
4 changed files with 141 additions and 84 deletions

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)