From faf67db089c6b2168fb1bab99b4385d374c64efe Mon Sep 17 00:00:00 2001 From: Jeremy Schlatter Date: Mon, 17 Feb 2025 13:37:40 -0800 Subject: [PATCH 1/4] cmd: fix progress bar flickering Previous code cleared the display before writing new content, creating a window where the terminal could (and in some cases did) render empty lines. Instead, we now write new content over the old content, only clearing the trailing end of lines for cases where the new line is shorter. Fixes #1664 --- progress/progress.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/progress/progress.go b/progress/progress.go index 102830a82..580d8123a 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -84,17 +84,16 @@ func (p *Progress) render() { fmt.Fprint(p.w, "\033[?25l") defer fmt.Fprint(p.w, "\033[?25h") - // clear already rendered progress lines - for i := range p.pos { - if i > 0 { - fmt.Fprint(p.w, "\033[A") - } - fmt.Fprint(p.w, "\033[2K\033[1G") + // move the cursor back to the beginning + for range p.pos - 1 { + fmt.Fprint(p.w, "\033[A") } + fmt.Fprint(p.w, "\033[1G") // render progress lines for i, state := range p.states { fmt.Fprint(p.w, state.String()) + fmt.Fprintf(p.w, "\033[K") if i < len(p.states)-1 { fmt.Fprint(p.w, "\n") } From 5930aaeb1ae420eb9a0963247ff4463d1eadce5a Mon Sep 17 00:00:00 2001 From: Jeremy Schlatter Date: Mon, 17 Feb 2025 14:55:32 -0800 Subject: [PATCH 2/4] cmd: fix cursor flickering in progress bar The previous commit fixed flickering in the progress bar itself. Cursor flickering is harder to address. Cursor flickering could be fixed by hiding the cursor altogether while the progress bar is displayed. The downside of this is that if the program is killed in such a way that it can't clean up its state, it would leave the cursor invisible. Instead, this commit introduces an output buffer. All of the escape codes and content for a single progress update are written to a buffer, which is then flushed to the terminal all at once. This significantly decreases the time during which the terminal has seen the cursor-hiding code but has not yet seen the cursor-showing code, thus minimizing (but not 100% eliminating) cursor flickering. For more context, see: https://gitlab.gnome.org/GNOME/vte/-/issues/2837#note_2269501 --- progress/progress.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/progress/progress.go b/progress/progress.go index 580d8123a..104af2c9f 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -1,6 +1,7 @@ package progress import ( + "bytes" "fmt" "io" "sync" @@ -12,8 +13,9 @@ type State interface { } type Progress struct { - mu sync.Mutex - w io.Writer + mu sync.Mutex + w io.Writer + buf bytes.Buffer pos int @@ -81,21 +83,26 @@ func (p *Progress) render() { p.mu.Lock() defer p.mu.Unlock() - fmt.Fprint(p.w, "\033[?25l") - defer fmt.Fprint(p.w, "\033[?25h") + // buffer the terminal update to minimize cursor flickering + // https://gitlab.gnome.org/GNOME/vte/-/issues/2837#note_2269501 + p.buf.Reset() + defer p.buf.WriteTo(p.w) + + fmt.Fprint(&p.buf, "\033[?25l") + defer fmt.Fprint(&p.buf, "\033[?25h") // move the cursor back to the beginning for range p.pos - 1 { - fmt.Fprint(p.w, "\033[A") + fmt.Fprint(&p.buf, "\033[A") } - fmt.Fprint(p.w, "\033[1G") + fmt.Fprint(&p.buf, "\033[1G") // render progress lines for i, state := range p.states { - fmt.Fprint(p.w, state.String()) - fmt.Fprintf(p.w, "\033[K") + fmt.Fprint(&p.buf, state.String()) + fmt.Fprintf(&p.buf, "\033[K") if i < len(p.states)-1 { - fmt.Fprint(p.w, "\n") + fmt.Fprint(&p.buf, "\n") } } From f9c7ead1601f286d619e4d6988cd2da2c86474ce Mon Sep 17 00:00:00 2001 From: Jeremy Schlatter Date: Mon, 17 Feb 2025 20:01:03 -0800 Subject: [PATCH 3/4] cmd: eliminate flickering with synchronized output --- progress/progress.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/progress/progress.go b/progress/progress.go index 104af2c9f..6aa26dd33 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -83,11 +83,14 @@ func (p *Progress) render() { p.mu.Lock() defer p.mu.Unlock() - // buffer the terminal update to minimize cursor flickering - // https://gitlab.gnome.org/GNOME/vte/-/issues/2837#note_2269501 + // buffer output to minimize flickering on all terminals p.buf.Reset() defer p.buf.WriteTo(p.w) + // eliminate flickering on terminals that support synchronized output + fmt.Fprint(&p.buf, "\033[?2026h") + defer fmt.Fprint(&p.buf, "\033[?2026l") + fmt.Fprint(&p.buf, "\033[?25l") defer fmt.Fprint(&p.buf, "\033[?25h") From 78f403ff4530441cd9c32198e0a4f9e9f8284f45 Mon Sep 17 00:00:00 2001 From: Jeremy Schlatter Date: Tue, 18 Feb 2025 14:50:09 -0800 Subject: [PATCH 4/4] address code review comments --- progress/progress.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/progress/progress.go b/progress/progress.go index 6aa26dd33..0cd0ea1f9 100644 --- a/progress/progress.go +++ b/progress/progress.go @@ -1,7 +1,7 @@ package progress import ( - "bytes" + "bufio" "fmt" "io" "sync" @@ -13,9 +13,9 @@ type State interface { } type Progress struct { - mu sync.Mutex - w io.Writer - buf bytes.Buffer + mu sync.Mutex + // buffer output to minimize flickering on all terminals + w *bufio.Writer pos int @@ -24,7 +24,7 @@ type Progress struct { } func NewProgress(w io.Writer) *Progress { - p := &Progress{w: w} + p := &Progress{w: bufio.NewWriter(w)} go p.start() return p } @@ -50,11 +50,14 @@ func (p *Progress) Stop() bool { stopped := p.stop() if stopped { fmt.Fprint(p.w, "\n") + p.w.Flush() } return stopped } func (p *Progress) StopAndClear() bool { + defer p.w.Flush() + fmt.Fprint(p.w, "\033[?25l") defer fmt.Fprint(p.w, "\033[?25h") @@ -83,29 +86,26 @@ func (p *Progress) render() { p.mu.Lock() defer p.mu.Unlock() - // buffer output to minimize flickering on all terminals - p.buf.Reset() - defer p.buf.WriteTo(p.w) + defer p.w.Flush() // eliminate flickering on terminals that support synchronized output - fmt.Fprint(&p.buf, "\033[?2026h") - defer fmt.Fprint(&p.buf, "\033[?2026l") + fmt.Fprint(p.w, "\033[?2026h") + defer fmt.Fprint(p.w, "\033[?2026l") - fmt.Fprint(&p.buf, "\033[?25l") - defer fmt.Fprint(&p.buf, "\033[?25h") + fmt.Fprint(p.w, "\033[?25l") + defer fmt.Fprint(p.w, "\033[?25h") // move the cursor back to the beginning for range p.pos - 1 { - fmt.Fprint(&p.buf, "\033[A") + fmt.Fprint(p.w, "\033[A") } - fmt.Fprint(&p.buf, "\033[1G") + fmt.Fprint(p.w, "\033[1G") // render progress lines for i, state := range p.states { - fmt.Fprint(&p.buf, state.String()) - fmt.Fprintf(&p.buf, "\033[K") + fmt.Fprint(p.w, state.String(), "\033[K") if i < len(p.states)-1 { - fmt.Fprint(&p.buf, "\n") + fmt.Fprint(p.w, "\n") } }