Repository: charmbracelet/bubbletea
Pull request #478 (2022-10-04 01:33:39)

fix: move output handling to renderer
Move output handling to renderer.


Patch:
diff --git a/nil_renderer.go b/nil_renderer.go
index 03dd949..37b3c45 100644
--- a/nil_renderer.go
+++ b/nil_renderer.go
@@ -2,10 +2,17 @@ package tea
 
 type nilRenderer struct{}
 
-func (n nilRenderer) start()              {}
-func (n nilRenderer) stop()               {}
-func (n nilRenderer) kill()               {}
-func (n nilRenderer) write(v string)      {}
-func (n nilRenderer) repaint()            {}
-func (n nilRenderer) altScreen() bool     { return false }
-func (n nilRenderer) setAltScreen(v bool) {}
+func (n nilRenderer) start()                  {}
+func (n nilRenderer) stop()                   {}
+func (n nilRenderer) kill()                   {}
+func (n nilRenderer) write(v string)          {}
+func (n nilRenderer) repaint()                {}
+func (n nilRenderer) altScreen() bool         { return false }
+func (n nilRenderer) enterAltScreen()         {}
+func (n nilRenderer) exitAltScreen()          {}
+func (n nilRenderer) showCursor()             {}
+func (n nilRenderer) hideCursor()             {}
+func (n nilRenderer) enableMouseCellMotion()  {}
+func (n nilRenderer) disableMouseCellMotion() {}
+func (n nilRenderer) enableMouseAllMotion()   {}
+func (n nilRenderer) disableMouseAllMotion()  {}
diff --git a/renderer.go b/renderer.go
index aedbb66..a975e33 100644
--- a/renderer.go
+++ b/renderer.go
@@ -23,10 +23,30 @@ type renderer interface {
 
 	// Whether or not the alternate screen buffer is enabled.
 	altScreen() bool
-
-	// Record internally that the alternate screen buffer is enabled. This
-	// does not actually toggle the alternate screen buffer.
-	setAltScreen(bool)
+	// Enable the alternate screen buffer.
+	enterAltScreen()
+	// Disable the alternate screen buffer.
+	exitAltScreen()
+
+	// Show the cursor.
+	showCursor()
+	// Hide the cursor.
+	hideCursor()
+
+	// enableMouseCellMotion enables mouse click, release, wheel and motion
+	// events if a mouse button is pressed (i.e., drag events).
+	enableMouseCellMotion()
+
+	// DisableMouseCellMotion disables Mouse Cell Motion tracking.
+	disableMouseCellMotion()
+
+	// EnableMouseAllMotion enables mouse click, release, wheel and motion
+	// events, regardless of whether a mouse button is pressed. Many modern
+	// terminals support this, but not all.
+	enableMouseAllMotion()
+
+	// DisableMouseAllMotion disables All Motion mouse tracking.
+	disableMouseAllMotion()
 }
 
 // repaintMsg forces a full repaint.
diff --git a/standard_renderer.go b/standard_renderer.go
index 6c1c50b..feafb82 100644
--- a/standard_renderer.go
+++ b/standard_renderer.go
@@ -25,12 +25,13 @@ const (
 // In cases where very high performance is needed the renderer can be told
 // to exclude ranges of lines, allowing them to be written to directly.
 type standardRenderer struct {
-	out                *termenv.Output
+	mtx *sync.Mutex
+	out *termenv.Output
+
 	buf                bytes.Buffer
 	queuedMessageLines []string
 	framerate          time.Duration
 	ticker             *time.Ticker
-	mtx                *sync.Mutex
 	done               chan struct{}
 	lastRender         string
 	linesRendered      int
@@ -50,10 +51,10 @@ type standardRenderer struct {
 
 // newRenderer creates a new renderer. Normally you'll want to initialize it
 // with os.Stdout as the first argument.
-func newRenderer(out *termenv.Output, mtx *sync.Mutex, useANSICompressor bool) renderer {
+func newRenderer(out *termenv.Output, useANSICompressor bool) renderer {
 	r := &standardRenderer{
 		out:                out,
-		mtx:                mtx,
+		mtx:                &sync.Mutex{},
 		framerate:          defaultFramerate,
 		useANSICompressor:  useANSICompressor,
 		queuedMessageLines: []string{},
@@ -248,11 +249,69 @@ func (r *standardRenderer) altScreen() bool {
 	return r.altScreenActive
 }
 
-func (r *standardRenderer) setAltScreen(v bool) {
-	r.altScreenActive = v
+func (r *standardRenderer) enterAltScreen() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.altScreenActive = true
+
+	r.out.AltScreen()
+	r.out.MoveCursor(1, 1)
+	r.repaint()
+}
+
+func (r *standardRenderer) exitAltScreen() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.altScreenActive = false
+
+	r.out.ExitAltScreen()
 	r.repaint()
 }
 
+func (r *standardRenderer) showCursor() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.ShowCursor()
+}
+
+func (r *standardRenderer) hideCursor() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.HideCursor()
+}
+
+func (r *standardRenderer) enableMouseCellMotion() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.EnableMouseCellMotion()
+}
+
+func (r *standardRenderer) disableMouseCellMotion() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.DisableMouseCellMotion()
+}
+
+func (r *standardRenderer) enableMouseAllMotion() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.EnableMouseAllMotion()
+}
+
+func (r *standardRenderer) disableMouseAllMotion() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
+	r.out.DisableMouseAllMotion()
+}
+
 // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
 // renderer.
 func (r *standardRenderer) setIgnoredLines(from int, to int) {
@@ -371,6 +430,7 @@ func (r *standardRenderer) handleMessages(msg Msg) {
 		r.mtx.Lock()
 		r.width = msg.Width
 		r.height = msg.Height
+		r.repaint()
 		r.mtx.Unlock()
 
 	case clearScrollAreaMsg:
diff --git a/tea.go b/tea.go
index 9e5d87e..5533f63 100644
--- a/tea.go
+++ b/tea.go
@@ -16,7 +16,6 @@ import (
 	"os"
 	"os/signal"
 	"runtime/debug"
-	"sync"
 	"syscall"
 	"time"
 
@@ -83,7 +82,6 @@ type Program struct {
 	startupOptions startupOptions
 
 	ctx context.Context
-	mtx *sync.Mutex
 
 	msgs         chan Msg
 	errs         chan error
@@ -95,7 +93,6 @@ type Program struct {
 	cancelReader  cancelreader.CancelReader
 
 	renderer           renderer
-	altScreenActive    bool
 	altScreenWasActive bool // was the altscreen active before releasing the terminal?
 
 	// CatchPanics is incredibly useful for restoring the terminal to a usable
@@ -252,7 +249,6 @@ type hideCursorMsg struct{}
 // NewProgram creates a new Program.
 func NewProgram(model Model, opts ...ProgramOption) *Program {
 	p := &Program{
-		mtx:          &sync.Mutex{},
 		initialModel: model,
 		input:        os.Stdin,
 		msgs:         make(chan Msg),
@@ -389,17 +385,17 @@ func (p *Program) StartReturningModel() (Model, error) {
 
 	// If no renderer is set use the standard one.
 	if p.renderer == nil {
-		p.renderer = newRenderer(p.output, p.mtx, p.startupOptions.has(withANSICompressor))
+		p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
 	}
 
 	// Honor program startup options.
 	if p.startupOptions&withAltScreen != 0 {
-		p.EnterAltScreen()
+		p.renderer.enterAltScreen()
 	}
 	if p.startupOptions&withMouseCellMotion != 0 {
-		p.EnableMouseCellMotion()
+		p.renderer.enableMouseCellMotion()
 	} else if p.startupOptions&withMouseAllMotion != 0 {
-		p.EnableMouseAllMotion()
+		p.renderer.enableMouseAllMotion()
 	}
 
 	// Initialize the program.
@@ -418,7 +414,6 @@ func (p *Program) StartReturningModel() (Model, error) {
 
 	// Start the renderer.
 	p.renderer.start()
-	p.renderer.setAltScreen(p.altScreenActive)
 
 	// Render the initial view.
 	p.renderer.write(model.View())
@@ -503,40 +498,35 @@ func (p *Program) StartReturningModel() (Model, error) {
 				p.shutdown(false)
 				return model, nil
 
-			case batchMsg:
-				for _, cmd := range msg {
-					cmds <- cmd
-				}
-				continue
-
-			case WindowSizeMsg:
-				p.mtx.Lock()
-				p.renderer.repaint()
-				p.mtx.Unlock()
-
 			case enterAltScreenMsg:
-				p.EnterAltScreen()
+				p.renderer.enterAltScreen()
 
 			case exitAltScreenMsg:
-				p.ExitAltScreen()
+				p.renderer.exitAltScreen()
 
 			case enableMouseCellMotionMsg:
-				p.EnableMouseCellMotion()
+				p.renderer.enableMouseCellMotion()
 
 			case enableMouseAllMotionMsg:
-				p.EnableMouseAllMotion()
+				p.renderer.enableMouseAllMotion()
 
 			case disableMouseMsg:
-				p.DisableMouseCellMotion()
-				p.DisableMouseAllMotion()
+				p.renderer.disableMouseCellMotion()
+				p.renderer.disableMouseAllMotion()
 
 			case hideCursorMsg:
-				p.output.HideCursor()
+				p.renderer.hideCursor()
 
 			case execMsg:
 				// NB: this blocks.
 				p.exec(msg.cmd, msg.fn)
 
+			case batchMsg:
+				for _, cmd := range msg {
+					cmds <- cmd
+				}
+				continue
+
 			case sequenceMsg:
 				go func() {
 					// Execute commands one at a time, in order.
@@ -621,19 +611,8 @@ func (p *Program) shutdown(kill bool) {
 //
 // Deprecated: Use the WithAltScreen ProgramOption instead.
 func (p *Program) EnterAltScreen() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	if p.altScreenActive {
-		return
-	}
-
-	p.output.AltScreen()
-	p.output.MoveCursor(1, 1)
-
-	p.altScreenActive = true
 	if p.renderer != nil {
-		p.renderer.setAltScreen(p.altScreenActive)
+		p.renderer.enterAltScreen()
 	}
 }
 
@@ -641,18 +620,8 @@ func (p *Program) EnterAltScreen() {
 //
 // Deprecated: The altscreen will exited automatically when the program exits.
 func (p *Program) ExitAltScreen() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	if !p.altScreenActive {
-		return
-	}
-
-	p.output.ExitAltScreen()
-
-	p.altScreenActive = false
 	if p.renderer != nil {
-		p.renderer.setAltScreen(p.altScreenActive)
+		p.renderer.exitAltScreen()
 	}
 }
 
@@ -661,10 +630,7 @@ func (p *Program) ExitAltScreen() {
 //
 // Deprecated: Use the WithMouseCellMotion ProgramOption instead.
 func (p *Program) EnableMouseCellMotion() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	p.output.EnableMouseCellMotion()
+	p.renderer.enableMouseCellMotion()
 }
 
 // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
@@ -672,10 +638,7 @@ func (p *Program) EnableMouseCellMotion() {
 //
 // Deprecated: The mouse will automatically be disabled when the program exits.
 func (p *Program) DisableMouseCellMotion() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	p.output.DisableMouseCellMotion()
+	p.renderer.disableMouseCellMotion()
 }
 
 // EnableMouseAllMotion enables mouse click, release, wheel and motion events,
@@ -684,10 +647,7 @@ func (p *Program) DisableMouseCellMotion() {
 //
 // Deprecated: Use the WithMouseAllMotion ProgramOption instead.
 func (p *Program) EnableMouseAllMotion() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	p.output.EnableMouseAllMotion()
+	p.renderer.enableMouseAllMotion()
 }
 
 // DisableMouseAllMotion disables All Motion mouse tracking. This will be
@@ -695,10 +655,7 @@ func (p *Program) EnableMouseAllMotion() {
 //
 // Deprecated: The mouse will automatically be disabled when the program exits.
 func (p *Program) DisableMouseAllMotion() {
-	p.mtx.Lock()
-	defer p.mtx.Unlock()
-
-	p.output.DisableMouseAllMotion()
+	p.renderer.disableMouseAllMotion()
 }
 
 // ReleaseTerminal restores the original terminal state and cancels the input
@@ -708,8 +665,8 @@ func (p *Program) ReleaseTerminal() error {
 	p.cancelInput()
 	p.waitForReadLoop()
 
-	p.altScreenWasActive = p.altScreenActive
-	if p.altScreenActive {
+	p.altScreenWasActive = p.renderer.altScreen()
+	if p.renderer.altScreen() {
 		p.ExitAltScreen()
 		time.Sleep(time.Millisecond * 10) // give the terminal a moment to catch up
 	}


Test patch:
diff --git a/nil_renderer_test.go b/nil_renderer_test.go
index 7364484..9ab7b58 100644
--- a/nil_renderer_test.go
+++ b/nil_renderer_test.go
@@ -9,7 +9,7 @@ func TestNilRenderer(t *testing.T) {
 	r.kill()
 	r.write("a")
 	r.repaint()
-	r.setAltScreen(true)
+	r.enterAltScreen()
 	if r.altScreen() {
 		t.Errorf("altScreen should always return false")
 	}