Repository: charmbracelet/bubbletea
Pull request #517 (2022-10-09 03:24:52)

fix: restoreTerminalState resets mouse handling state
- fixes mouse handling being enabled during sub commands
- fixes shutdown order: restore mouse state before alt screen


Patch:
diff --git a/commands.go b/commands.go
index b095e5d..08f1a2e 100644
--- a/commands.go
+++ b/commands.go
@@ -40,7 +40,7 @@ func Sequence(cmds ...Cmd) Cmd {
 	}
 }
 
-// sequenceMsg is used interally to run the the given commands in order.
+// sequenceMsg is used interally to run the given commands in order.
 type sequenceMsg []Cmd
 
 // Every is a command that ticks in sync with the system clock. So, if you
diff --git a/examples/debounce/main.go b/examples/debounce/main.go
index d22fe4d..6058aca 100644
--- a/examples/debounce/main.go
+++ b/examples/debounce/main.go
@@ -7,7 +7,7 @@ package main
 // by the Tick command.
 //
 // In a subsequent Update, if the tag in the Msg matches current tag on the
-// model's state we know that the debouncing is complete and we can proceeed as
+// model's state we know that the debouncing is complete and we can proceed as
 // normal. If not, we simply ignore the inbound message.
 
 import (
diff --git a/examples/pager/artichoke.md b/examples/pager/artichoke.md
index 55b673a..00824b9 100644
--- a/examples/pager/artichoke.md
+++ b/examples/pager/artichoke.md
@@ -7,7 +7,7 @@ A casual introduction. 你好世界!
 
 The _artichoke_ is mentioned as a garden plant in the 8th century BC by Homer
 **and** Hesiod. The naturally occurring variant of the artichoke, the cardoon,
-which is native the the Mediterranean area, also has records of use as a food
+which is native to the Mediterranean area, also has records of use as a food
 among the ancient Greeks and Romans. Pliny the Elder mentioned growing of
 _carduus_ in Carthage and Cordoba.
 
diff --git a/examples/pipe/main.go b/examples/pipe/main.go
index 5d7e0ac..b60aa7e 100644
--- a/examples/pipe/main.go
+++ b/examples/pipe/main.go
@@ -1,6 +1,6 @@
 package main
 
-// An example illustating how to pipe in data to a Bubble Tea application.
+// An example illustrating how to pipe in data to a Bubble Tea application.
 // Moreso, this serves as proof that Bubble Tea will automatically listen for
 // keystrokes when input is not a TTY, such as when data is piped or redirected
 // in.
diff --git a/exec.go b/exec.go
index 53bf5d4..7a14d2a 100644
--- a/exec.go
+++ b/exec.go
@@ -13,7 +13,7 @@ type execMsg struct {
 }
 
 // Exec is used to perform arbitrary I/O in a blocking fashion, effectively
-// pausing the Program while execution is runnning and resuming it when
+// pausing the Program while execution is running and resuming it when
 // execution has completed.
 //
 // Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
diff --git a/screen.go b/screen.go
index 5780290..899db3d 100644
--- a/screen.go
+++ b/screen.go
@@ -90,7 +90,7 @@ func DisableMouse() Msg {
 	return disableMouseMsg{}
 }
 
-// disableMouseMsg is an internal message that that signals to stop listening
+// disableMouseMsg is an internal message that signals to stop listening
 // for mouse events. To send a disableMouseMsg, use the DisableMouse command.
 type disableMouseMsg struct{}
 
diff --git a/standard_renderer.go b/standard_renderer.go
index 195ef59..c0b685d 100644
--- a/standard_renderer.go
+++ b/standard_renderer.go
@@ -76,7 +76,12 @@ func (r *standardRenderer) start() {
 
 // stop permanently halts the renderer, rendering the final frame.
 func (r *standardRenderer) stop() {
+	// flush locks the mutex
 	r.flush()
+
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
 	r.out.ClearLine()
 	r.once.Do(func() {
 		close(r.done)
@@ -91,6 +96,9 @@ func (r *standardRenderer) stop() {
 
 // kill halts the renderer. The final frame will not be rendered.
 func (r *standardRenderer) kill() {
+	r.mtx.Lock()
+	defer r.mtx.Unlock()
+
 	r.out.ClearLine()
 	r.once.Do(func() {
 		close(r.done)
diff --git a/tea.go b/tea.go
index bf4779f..fbd7235 100644
--- a/tea.go
+++ b/tea.go
@@ -18,7 +18,6 @@ import (
 	"runtime/debug"
 	"sync"
 	"syscall"
-	"time"
 
 	"github.com/containerd/console"
 	isatty "github.com/mattn/go-isatty"
@@ -446,7 +445,6 @@ func (p *Program) Run() (Model, error) {
 			return model, err
 		}
 		defer p.cancelReader.Close() //nolint:errcheck
-		handlers.add(p.readLoopDone)
 	}
 
 	// Handle resize events.
@@ -498,7 +496,7 @@ func (p *Program) Start() error {
 // messages to be injected from outside the program for interoperability
 // purposes.
 //
-// If the program is not running this this will be a no-op, so it's safe to
+// If the program is not running this will be a no-op, so it's safe to
 // send messages if the program is unstarted, or has exited.
 func (p *Program) Send(msg Msg) {
 	p.msgs <- msg
@@ -532,11 +530,8 @@ func (p *Program) shutdown(kill bool) {
 			p.renderer.stop()
 		}
 	}
-	p.ExitAltScreen()
-	p.DisableMouseCellMotion()
-	p.DisableMouseAllMotion()
-	_ = p.restoreTerminalState()
 
+	_ = p.restoreTerminalState()
 	if p.restoreOutput != nil {
 		_ = p.restoreOutput()
 	}
@@ -546,14 +541,10 @@ func (p *Program) shutdown(kill bool) {
 // reader. You can return control to the Program with RestoreTerminal.
 func (p *Program) ReleaseTerminal() error {
 	p.ignoreSignals = true
-	p.cancelInput()
+	p.cancelReader.Cancel()
 	p.waitForReadLoop()
 
 	p.altScreenWasActive = p.renderer.altScreen()
-	if p.renderer.altScreen() {
-		p.ExitAltScreen()
-		time.Sleep(time.Millisecond * 10) // give the terminal a moment to catch up
-	}
 	return p.restoreTerminalState()
 }
 
@@ -566,17 +557,17 @@ func (p *Program) RestoreTerminal() error {
 	if err := p.initTerminal(); err != nil {
 		return err
 	}
-
 	if err := p.initCancelReader(); err != nil {
 		return err
 	}
 
 	if p.altScreenWasActive {
-		p.EnterAltScreen()
+		p.renderer.enterAltScreen()
+	} else {
+		// entering alt screen already causes a repaint.
+		go p.Send(repaintMsg{})
 	}
 
-	go p.Send(repaintMsg{})
-
 	return nil
 }
 
diff --git a/tty.go b/tty.go
index 85725c2..4f33d86 100644
--- a/tty.go
+++ b/tty.go
@@ -30,6 +30,15 @@ func (p *Program) initTerminal() error {
 func (p *Program) restoreTerminalState() error {
 	if p.renderer != nil {
 		p.renderer.showCursor()
+		p.renderer.disableMouseCellMotion()
+		p.renderer.disableMouseAllMotion()
+
+		if p.renderer.altScreen() {
+			p.renderer.exitAltScreen()
+
+			// give the terminal a moment to catch up
+			time.Sleep(time.Millisecond * 10)
+		}
 	}
 
 	if p.console != nil {
@@ -79,11 +88,6 @@ func (p *Program) readLoop() {
 	}
 }
 
-// cancelInput cancels the input reader.
-func (p *Program) cancelInput() {
-	p.cancelReader.Cancel()
-}
-
 // waitForReadLoop waits for the cancelReader to finish its read loop.
 func (p *Program) waitForReadLoop() {
 	select {


Test patch:
diff --git a/screen_test.go b/screen_test.go
index 5e3d4e6..33b5169 100644
--- a/screen_test.go
+++ b/screen_test.go
@@ -14,42 +14,42 @@ func TestClearMsg(t *testing.T) {
 		{
 			name:     "clear_screen",
 			cmds:     []Cmd{ClearScreen},
-			expected: "\x1b[?25l\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "altscreen",
 			cmds:     []Cmd{EnterAltScreen, ExitAltScreen},
-			expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?1049lsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?1049lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "altscreen_autoexit",
 			cmds:     []Cmd{EnterAltScreen},
-			expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?1049l\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1049l",
 		},
 		{
 			name:     "mouse_cellmotion",
 			cmds:     []Cmd{EnableMouseCellMotion},
-			expected: "\x1b[?25l\x1b[?1002hsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?1002hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "mouse_allmotion",
 			cmds:     []Cmd{EnableMouseAllMotion},
-			expected: "\x1b[?25l\x1b[?1003hsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?1003hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "mouse_disable",
 			cmds:     []Cmd{EnableMouseAllMotion, DisableMouse},
-			expected: "\x1b[?25l\x1b[?1003h\x1b[?1002l\x1b[?1003lsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?1003h\x1b[?1002l\x1b[?1003lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "cursor_hide",
 			cmds:     []Cmd{HideCursor},
-			expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 		{
 			name:     "cursor_hideshow",
 			cmds:     []Cmd{HideCursor, ShowCursor},
-			expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?1002l\x1b[?1003l\x1b[?25h",
+			expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l",
 		},
 	}