mirror of
https://github.com/restic/restic.git
synced 2026-05-14 14:25:23 +00:00
terminal: Do not write unchanged status lines
Check if each line of status is changed, and write the line to the terminal only if it has changed
This commit is contained in:
committed by
Michael Eischer
parent
ff575a978d
commit
e33bcede2f
@@ -0,0 +1,10 @@
|
||||
Enhancement: Do not rewrite unchanged lines every frame in status bar
|
||||
|
||||
Status bars were entirely rewritten every frame if any of its content has updated.
|
||||
This behavior had made any user interaction (such as selection) with status bar
|
||||
impossible in certain terminal emulators, even with the unchanged lines.
|
||||
Now it writes changed lines only at status bar update, thereby improving
|
||||
user experience in those terminal emulators.
|
||||
|
||||
https://github.com/restic/restic/issues/5562
|
||||
https://github.com/restic/restic/pull/5648
|
||||
@@ -11,6 +11,8 @@ const (
|
||||
PosixControlMoveCursorHome = "\r"
|
||||
// PosixControlMoveCursorUp moves cursor up one line
|
||||
PosixControlMoveCursorUp = "\x1b[1A"
|
||||
// PosixControlMoveCursorDown moves cursor down one line
|
||||
PosixControlMoveCursorDown = "\x1b[1B"
|
||||
// PosixControlClearLine clears the current line
|
||||
PosixControlClearLine = "\x1b[2K"
|
||||
)
|
||||
@@ -36,3 +38,14 @@ func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PosixMoveCursorDown moves the cursor to the line n lines below the current one.
|
||||
func PosixMoveCursorDown(wr io.Writer, _ uintptr, n int) error {
|
||||
data := []byte(PosixControlMoveCursorHome)
|
||||
data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorDown), n)...)
|
||||
_, err := wr.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) error {
|
||||
return PosixMoveCursorUp
|
||||
}
|
||||
|
||||
// MoveCursorDown moves the cursor to the line n lines below the current one.
|
||||
func MoveCursorDown(_ uintptr) func(io.Writer, uintptr, int) error {
|
||||
return PosixMoveCursorDown
|
||||
}
|
||||
|
||||
// CanUpdateStatus returns true if status lines can be printed, the process
|
||||
// output is not redirected to a file or pipe.
|
||||
func CanUpdateStatus(fd uintptr) bool {
|
||||
|
||||
@@ -35,6 +35,17 @@ func MoveCursorUp(fd uintptr) func(io.Writer, uintptr, int) error {
|
||||
return PosixMoveCursorUp
|
||||
}
|
||||
|
||||
// moveCursorDown moves the cursor to the line n lines below the current one.
|
||||
func MoveCursorDown(fd uintptr) func(io.Writer, uintptr, int) error {
|
||||
// easy case, the terminal is cmd or psh, without redirection
|
||||
if isWindowsTerminal(fd) {
|
||||
return windowsMoveCursorDown
|
||||
}
|
||||
|
||||
// assume we're running in mintty/cygwin
|
||||
return PosixMoveCursorDown
|
||||
}
|
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
var (
|
||||
@@ -73,6 +84,19 @@ func windowsMoveCursorUp(_ io.Writer, fd uintptr, n int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// windowsMoveCursorDown moves the cursor to the line n lines below the current one.
|
||||
func windowsMoveCursorDown(_ io.Writer, fd uintptr, n int) error {
|
||||
var info windows.ConsoleScreenBufferInfo
|
||||
windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info)
|
||||
|
||||
// move cursor up by n lines and to the first column
|
||||
windows.SetConsoleCursorPosition(windows.Handle(fd), windows.Coord{
|
||||
X: 0,
|
||||
Y: info.CursorPosition.Y + int16(n),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWindowsTerminal return true if the file descriptor is a windows terminal (cmd, psh).
|
||||
func isWindowsTerminal(fd uintptr) bool {
|
||||
return term.IsTerminal(int(fd))
|
||||
|
||||
@@ -40,6 +40,7 @@ type Terminal struct {
|
||||
|
||||
clearCurrentLine func(io.Writer, uintptr) error
|
||||
moveCursorUp func(io.Writer, uintptr, int) error
|
||||
moveCursorDown func(io.Writer, uintptr, int) error
|
||||
}
|
||||
|
||||
type message struct {
|
||||
@@ -123,6 +124,7 @@ func New(rd io.ReadCloser, wr io.Writer, errWriter io.Writer, disableStatus bool
|
||||
t.fd = d.Fd()
|
||||
t.clearCurrentLine = terminal.ClearCurrentLine(t.fd)
|
||||
t.moveCursorUp = terminal.MoveCursorUp(t.fd)
|
||||
t.moveCursorDown = terminal.MoveCursorDown(t.fd)
|
||||
}
|
||||
if terminal.OutputIsTerminal(d.Fd()) {
|
||||
t.outputIsTerminal = true
|
||||
@@ -202,6 +204,18 @@ func (t *Terminal) Run(ctx context.Context) {
|
||||
t.runWithoutStatus(ctx)
|
||||
}
|
||||
|
||||
func findUnchangedLines(curr, last []string) []bool {
|
||||
unchanged := make([]bool, len(curr))
|
||||
|
||||
for i := range min(len(curr), len(last)) {
|
||||
if curr[i] == last[i] {
|
||||
unchanged[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
return unchanged
|
||||
}
|
||||
|
||||
// run listens on the channels and updates the terminal screen.
|
||||
func (t *Terminal) run(ctx context.Context) {
|
||||
var status []string
|
||||
@@ -210,7 +224,7 @@ func (t *Terminal) run(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !terminal.IsProcessBackground(t.fd) {
|
||||
t.writeStatus([]string{})
|
||||
t.writeStatus([]string{}, nil)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -241,7 +255,7 @@ func (t *Terminal) run(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
t.writeStatus(status)
|
||||
t.writeStatus(status, nil)
|
||||
lastWrittenStatus = append([]string{}, status...)
|
||||
case stat := <-t.status:
|
||||
status = append(status[:0], stat.lines...)
|
||||
@@ -252,7 +266,8 @@ func (t *Terminal) run(ctx context.Context) {
|
||||
}
|
||||
|
||||
if !slices.Equal(status, lastWrittenStatus) {
|
||||
t.writeStatus(status)
|
||||
unchangedLines := findUnchangedLines(status, lastWrittenStatus)
|
||||
t.writeStatus(status, unchangedLines)
|
||||
// Copy the status slice to avoid aliasing
|
||||
lastWrittenStatus = append([]string{}, status...)
|
||||
}
|
||||
@@ -260,7 +275,7 @@ func (t *Terminal) run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) writeStatus(status []string) {
|
||||
func (t *Terminal) writeStatus(status []string, unchanged []bool) {
|
||||
statusLen := len(status)
|
||||
status = append([]string{}, status...)
|
||||
for i := len(status); i < t.lastStatusLen; i++ {
|
||||
@@ -273,7 +288,18 @@ func (t *Terminal) writeStatus(status []string) {
|
||||
}
|
||||
t.lastStatusLen = statusLen
|
||||
|
||||
for _, line := range status {
|
||||
for i, line := range status {
|
||||
if unchanged != nil && i < len(unchanged) && unchanged[i] {
|
||||
// don't write unchanged lines every frame
|
||||
if i < len(status)-1 {
|
||||
// just move the cursor down to the next line
|
||||
if err := t.moveCursorDown(t.wr, t.fd, 1); err != nil {
|
||||
_, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := t.clearCurrentLine(t.wr, t.fd); err != nil {
|
||||
_, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user