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
4 changed files with 141 additions and 84 deletions
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue