mirror of
https://github.com/octoleo/restic.git
synced 2024-11-30 00:33:57 +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:
parent
4c25495d68
commit
b2208bb9c2
@ -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