Repository: ollama/ollama
Pull request: cmd: fix flickering in progress bar

In some cases the progress bar flickers during model downloads. This is caused by the way the progress bar was issuing screen updates: first clearing the screen, then drawing the new content. If the terminal emulator renders a frame between clearing and printing new content, then what shows up is empty space. Toggle rapidly between the empty space and the normal content, and you get flickering.

This PR adds three fixes to resolve the flickering as thoroughly as possible.

1. There is a terminal mode called [synchronized output](https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036) designed exactly for this purpose: to make sure the terminal does not render partial state updates. This PR adds synchronized output to resolve the issue on terminals that support this mode.

2. Instead of clearing-then-drawing, we can just write the new content directly over the old content. For cases where the new content is shorter, issue a `\033[K` at the end to clear the trailing end of the line. This way there is never a moment when the content is blank, so there is no flickering regardless of when the terminal happens to render new frames.

3. Output buffering. Rather than emitting partial content updates as they are ready (clearing one line at a time, then rendering one line at a time), we can write all of the partial updates to a buffer and then write out that buffer all at once. This significantly decreases the window of time in which the terminal can only see a partial update. This is particularly relevant for cursor flickering. While the changes in (2) fix flickering in the progress bar itself, they do not prevent the cursor from flickering. While cursor flickering is harder to fix completely, output buffering significantly reduces it. Output buffering also decreases the chances that a synchronized output terminal (1) times out while we are rendering an update.

Before:

https://github.com/user-attachments/assets/655a994e-c8f1-40e5-bffa-d22d8879a8f8

After:

https://github.com/user-attachments/assets/e855cac9-ec7a-4a56-b648-9ff989c8c204

Fixes #1664

Changed files:
filename: progress/progress.go
patch: @@ -1,6 +1,7 @@
 package progress
 
 import (
+	"bufio"
 	"fmt"
 	"io"
 	"sync"
@@ -13,7 +14,8 @@ type State interface {
 
 type Progress struct {
 	mu sync.Mutex
-	w  io.Writer
+	// buffer output to minimize flickering on all terminals
+	w *bufio.Writer
 
 	pos int
 
@@ -22,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
 }
@@ -48,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")
 
@@ -81,20 +86,24 @@ func (p *Progress) render() {
 	p.mu.Lock()
 	defer p.mu.Unlock()
 
+	defer p.w.Flush()
+
+	// eliminate flickering on terminals that support synchronized output
+	fmt.Fprint(p.w, "\033[?2026h")
+	defer fmt.Fprint(p.w, "\033[?2026l")
+
 	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.Fprint(p.w, state.String(), "\033[K")
 		if i < len(p.states)-1 {
 			fmt.Fprint(p.w, "\n")
 		}