Repository: charmbracelet/bubbletea
Pull request #501 (2022-10-07 14:48:10)

fix: Fixup code in one more thing about commands tutorial section

meowgorithm: Thank you!

Patch:
diff --git a/README.md b/README.md
index 031f170..490e252 100644
--- a/README.md
+++ b/README.md
@@ -239,7 +239,7 @@ The last step is to simply run our program. We pass our initial model to
 ```go
 func main() {
     p := tea.NewProgram(initialModel())
-    if err := p.Start(); err != nil {
+    if _, err := p.Run(); err != nil {
         fmt.Printf("Alas, there's been an error: %v", err)
         os.Exit(1)
     }
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/altscreen-toggle/main.go b/examples/altscreen-toggle/main.go
index 539ee52..54a3f09 100644
--- a/examples/altscreen-toggle/main.go
+++ b/examples/altscreen-toggle/main.go
@@ -66,7 +66,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(model{}).Start(); err != nil {
+	if _, err := tea.NewProgram(model{}).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/chat/main.go b/examples/chat/main.go
index 3d04a49..4a573d5 100644
--- a/examples/chat/main.go
+++ b/examples/chat/main.go
@@ -17,14 +17,13 @@ import (
 func main() {
 	p := tea.NewProgram(initialModel())
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
 
 type (
-	tickMsg struct{}
-	errMsg  error
+	errMsg error
 )
 
 type model struct {
diff --git a/examples/composable-views/main.go b/examples/composable-views/main.go
index 1dbe4f9..f81ce6a 100644
--- a/examples/composable-views/main.go
+++ b/examples/composable-views/main.go
@@ -156,7 +156,7 @@ func (m *mainModel) resetSpinner() {
 func main() {
 	p := tea.NewProgram(newModel(defaultTime))
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/examples/credit-card-form/main.go b/examples/credit-card-form/main.go
index 9128524..5ac6854 100644
--- a/examples/credit-card-form/main.go
+++ b/examples/credit-card-form/main.go
@@ -14,14 +14,13 @@ import (
 func main() {
 	p := tea.NewProgram(initialModel())
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
 
 type (
-	tickMsg struct{}
-	errMsg  error
+	errMsg error
 )
 
 const (
@@ -139,9 +138,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case tea.KeyEnter:
 			if m.focused == len(m.inputs)-1 {
 				return m, tea.Quit
-			} else {
-				m.nextInput()
 			}
+			m.nextInput()
 		case tea.KeyCtrlC, tea.KeyEsc:
 			return m, tea.Quit
 		case tea.KeyShiftTab, tea.KeyCtrlP:
@@ -172,7 +170,7 @@ func (m model) View() string {
 
  %s
  %s
- 
+
  %s  %s
  %s  %s
 
diff --git a/examples/debounce/main.go b/examples/debounce/main.go
index 834f521..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 (
@@ -59,7 +59,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(model{}).Start(); err != nil {
+	if _, err := tea.NewProgram(model{}).Run(); err != nil {
 		fmt.Println("uh oh:", err)
 		os.Exit(1)
 	}
diff --git a/examples/exec/main.go b/examples/exec/main.go
index 709efe8..0c8965b 100644
--- a/examples/exec/main.go
+++ b/examples/exec/main.go
@@ -64,7 +64,7 @@ func (m model) View() string {
 
 func main() {
 	m := model{}
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/fullscreen/main.go b/examples/fullscreen/main.go
index 9c27d4b..1c56be0 100644
--- a/examples/fullscreen/main.go
+++ b/examples/fullscreen/main.go
@@ -17,7 +17,7 @@ type tickMsg time.Time
 
 func main() {
 	p := tea.NewProgram(model(5), tea.WithAltScreen())
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
@@ -28,7 +28,6 @@ func (m model) Init() tea.Cmd {
 
 func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := message.(type) {
-
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "q", "esc", "ctrl+c":
@@ -36,12 +35,11 @@ func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
 		}
 
 	case tickMsg:
-		m -= 1
+		m--
 		if m <= 0 {
 			return m, tea.Quit
 		}
 		return m, tick()
-
 	}
 
 	return m, nil
diff --git a/examples/glamour/main.go b/examples/glamour/main.go
index 3b38911..976ee3b 100644
--- a/examples/glamour/main.go
+++ b/examples/glamour/main.go
@@ -120,7 +120,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	if err := tea.NewProgram(model).Start(); err != nil {
+	if _, err := tea.NewProgram(model).Run(); err != nil {
 		fmt.Println("Bummer, there's been an error:", err)
 		os.Exit(1)
 	}
diff --git a/examples/help/main.go b/examples/help/main.go
index e1ecc01..caedc9e 100644
--- a/examples/help/main.go
+++ b/examples/help/main.go
@@ -140,7 +140,7 @@ func main() {
 		}
 	}
 
-	if err := tea.NewProgram(newModel()).Start(); err != nil {
+	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
 		fmt.Printf("Could not start program :(\n%v\n", err)
 		os.Exit(1)
 	}
diff --git a/examples/http/main.go b/examples/http/main.go
index 24f552f..1b6a1e6 100644
--- a/examples/http/main.go
+++ b/examples/http/main.go
@@ -26,7 +26,7 @@ func (e errMsg) Error() string { return e.error.Error() }
 
 func main() {
 	p := tea.NewProgram(model{})
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
@@ -37,7 +37,6 @@ func (m model) Init() tea.Cmd {
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "q", "ctrl+c", "esc":
@@ -77,5 +76,7 @@ func checkServer() tea.Msg {
 	if err != nil {
 		return errMsg{err}
 	}
+	defer res.Body.Close()
+
 	return statusMsg(res.StatusCode)
 }
diff --git a/examples/list-default/main.go b/examples/list-default/main.go
index 5c5fcc8..91be1df 100644
--- a/examples/list-default/main.go
+++ b/examples/list-default/main.go
@@ -79,7 +79,7 @@ func main() {
 
 	p := tea.NewProgram(m, tea.WithAltScreen())
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/list-fancy/main.go b/examples/list-fancy/main.go
index 69617cb..3646ceb 100644
--- a/examples/list-fancy/main.go
+++ b/examples/list-fancy/main.go
@@ -183,7 +183,7 @@ func (m model) View() string {
 func main() {
 	rand.Seed(time.Now().UTC().UnixNano())
 
-	if err := tea.NewProgram(newModel()).Start(); err != nil {
+	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/list-simple/main.go b/examples/list-simple/main.go
index 97628db..9208d05 100644
--- a/examples/list-simple/main.go
+++ b/examples/list-simple/main.go
@@ -45,12 +45,11 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		}
 	}
 
-	fmt.Fprintf(w, fn(str))
+	fmt.Fprint(w, fn(str))
 }
 
 type model struct {
 	list     list.Model
-	items    []item
 	choice   string
 	quitting bool
 }
@@ -121,7 +120,7 @@ func main() {
 
 	m := model{list: l}
 
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/mouse/main.go b/examples/mouse/main.go
index f3ba0bd..ea5c11d 100644
--- a/examples/mouse/main.go
+++ b/examples/mouse/main.go
@@ -12,7 +12,7 @@ import (
 
 func main() {
 	p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseAllMotion())
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/examples/package-manager/main.go b/examples/package-manager/main.go
index e6f20ec..ced7c77 100644
--- a/examples/package-manager/main.go
+++ b/examples/package-manager/main.go
@@ -25,7 +25,6 @@ type model struct {
 
 var (
 	currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
-	subtleStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
 	doneStyle           = lipgloss.NewStyle().Margin(1, 2)
 	checkMark           = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
 )
@@ -132,7 +131,7 @@ func max(a, b int) int {
 func main() {
 	rand.Seed(time.Now().Unix())
 
-	if err := tea.NewProgram(newModel()).Start(); err != nil {
+	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
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/pager/main.go b/examples/pager/main.go
index 67b6a48..a98a9ec 100644
--- a/examples/pager/main.go
+++ b/examples/pager/main.go
@@ -141,7 +141,7 @@ func main() {
 		tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
 	)
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("could not run program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/paginator/main.go b/examples/paginator/main.go
index 35356d5..6eb1190 100644
--- a/examples/paginator/main.go
+++ b/examples/paginator/main.go
@@ -70,7 +70,7 @@ func (m model) View() string {
 
 func main() {
 	p := tea.NewProgram(newModel())
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/examples/pipe/main.go b/examples/pipe/main.go
index b138f9e..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.
@@ -45,7 +45,7 @@ func main() {
 
 	model := newModel(strings.TrimSpace(b.String()))
 
-	if err := tea.NewProgram(model).Start(); err != nil {
+	if _, err := tea.NewProgram(model).Run(); err != nil {
 		fmt.Println("Couldn't start program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/progress-animated/main.go b/examples/progress-animated/main.go
index ca6b610..be75bb1 100644
--- a/examples/progress-animated/main.go
+++ b/examples/progress-animated/main.go
@@ -30,7 +30,7 @@ func main() {
 		progress: progress.New(progress.WithDefaultGradient()),
 	}
 
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Oh no!", err)
 		os.Exit(1)
 	}
@@ -42,7 +42,7 @@ type model struct {
 	progress progress.Model
 }
 
-func (_ model) Init() tea.Cmd {
+func (m model) Init() tea.Cmd {
 	return tickCmd()
 }
 
@@ -79,10 +79,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	}
 }
 
-func (e model) View() string {
+func (m model) View() string {
 	pad := strings.Repeat(" ", padding)
 	return "\n" +
-		pad + e.progress.View() + "\n\n" +
+		pad + m.progress.View() + "\n\n" +
 		pad + helpStyle("Press any key to quit")
 }
 
diff --git a/examples/progress-download/main.go b/examples/progress-download/main.go
index 9da62fc..4639aa1 100644
--- a/examples/progress-download/main.go
+++ b/examples/progress-download/main.go
@@ -100,7 +100,7 @@ func main() {
 	// Start the download
 	go pw.Start()
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/progress-download/tui.go b/examples/progress-download/tui.go
index d4f5bf2..f8ff8e8 100644
--- a/examples/progress-download/tui.go
+++ b/examples/progress-download/tui.go
@@ -27,10 +27,9 @@ func finalPause() tea.Cmd {
 }
 
 type model struct {
-	url, path string
-	pw        *progressWriter
-	progress  progress.Model
-	err       error
+	pw       *progressWriter
+	progress progress.Model
+	err      error
 }
 
 func (m model) Init() tea.Cmd {
diff --git a/examples/progress-static/main.go b/examples/progress-static/main.go
index ba74cb6..a96a731 100644
--- a/examples/progress-static/main.go
+++ b/examples/progress-static/main.go
@@ -37,7 +37,7 @@ var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
 func main() {
 	prog := progress.New(progress.WithScaledGradient("#FF7CCB", "#FDFF8C"))
 
-	if err := tea.NewProgram(model{progress: prog}).Start(); err != nil {
+	if _, err := tea.NewProgram(model{progress: prog}).Run(); err != nil {
 		fmt.Println("Oh no!", err)
 		os.Exit(1)
 	}
@@ -50,13 +50,12 @@ type model struct {
 	progress progress.Model
 }
 
-func (_ model) Init() tea.Cmd {
+func (m model) Init() tea.Cmd {
 	return tickCmd()
 }
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-
 	case tea.KeyMsg:
 		return m, tea.Quit
 
@@ -80,10 +79,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	}
 }
 
-func (e model) View() string {
+func (m model) View() string {
 	pad := strings.Repeat(" ", padding)
 	return "\n" +
-		pad + e.progress.ViewAs(e.percent) + "\n\n" +
+		pad + m.progress.ViewAs(m.percent) + "\n\n" +
 		pad + helpStyle("Press any key to quit")
 }
 
diff --git a/examples/realtime/main.go b/examples/realtime/main.go
index 3390d81..516fcd6 100644
--- a/examples/realtime/main.go
+++ b/examples/realtime/main.go
@@ -85,8 +85,8 @@ func main() {
 		spinner: spinner.New(),
 	})
 
-	if p.Start() != nil {
-		fmt.Println("could not start program")
+	if _, err := p.Run(); err != nil {
+		fmt.Println("could not start program:", err)
 		os.Exit(1)
 	}
 }
diff --git a/examples/result/main.go b/examples/result/main.go
index 60d55a3..e0d7265 100644
--- a/examples/result/main.go
+++ b/examples/result/main.go
@@ -72,8 +72,8 @@ func (m model) View() string {
 func main() {
 	p := tea.NewProgram(model{})
 
-	// StartReturningModel returns the model as a tea.Model.
-	m, err := p.StartReturningModel()
+	// Run returns the model as a tea.Model.
+	m, err := p.Run()
 	if err != nil {
 		fmt.Println("Oh no:", err)
 		os.Exit(1)
diff --git a/examples/send-msg/main.go b/examples/send-msg/main.go
index b1b40a5..7badf6a 100644
--- a/examples/send-msg/main.go
+++ b/examples/send-msg/main.go
@@ -19,7 +19,6 @@ var (
 	spinnerStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
 	helpStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
 	dotStyle      = helpStyle.Copy().UnsetMargins()
-	foodStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("36"))
 	durationStyle = dotStyle.Copy()
 	appStyle      = lipgloss.NewStyle().Margin(1, 2, 0, 2)
 )
@@ -118,7 +117,7 @@ func main() {
 		}
 	}()
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/sequence/main.go b/examples/sequence/main.go
index 79f7e23..200b738 100644
--- a/examples/sequence/main.go
+++ b/examples/sequence/main.go
@@ -33,7 +33,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(model{}).Start(); err != nil {
+	if _, err := tea.NewProgram(model{}).Run(); err != nil {
 		fmt.Println("Uh oh:", err)
 		os.Exit(1)
 	}
diff --git a/examples/simple/main.go b/examples/simple/main.go
index a52757b..478f95a 100644
--- a/examples/simple/main.go
+++ b/examples/simple/main.go
@@ -23,7 +23,7 @@ func main() {
 
 	// Initialize our program
 	p := tea.NewProgram(model(5))
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
@@ -47,7 +47,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.KeyMsg:
 		return m, tea.Quit
 	case tickMsg:
-		m -= 1
+		m--
 		if m <= 0 {
 			return m, tea.Quit
 		}
diff --git a/examples/spinner/main.go b/examples/spinner/main.go
index c1095f6..de67b40 100644
--- a/examples/spinner/main.go
+++ b/examples/spinner/main.go
@@ -33,7 +33,6 @@ func (m model) Init() tea.Cmd {
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "q", "esc", "ctrl+c":
@@ -67,7 +66,7 @@ func (m model) View() string {
 
 func main() {
 	p := tea.NewProgram(initialModel())
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println(err)
 		os.Exit(1)
 	}
diff --git a/examples/spinners/main.go b/examples/spinners/main.go
index 53e5853..84e437f 100644
--- a/examples/spinners/main.go
+++ b/examples/spinners/main.go
@@ -32,7 +32,7 @@ func main() {
 	m := model{}
 	m.resetSpinner()
 
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("could not run program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/split-editors/main.go b/examples/split-editors/main.go
index 5e7f404..9009bb7 100644
--- a/examples/split-editors/main.go
+++ b/examples/split-editors/main.go
@@ -197,7 +197,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(newModel(), tea.WithAltScreen()).Start(); err != nil {
+	if _, err := tea.NewProgram(newModel(), tea.WithAltScreen()).Run(); err != nil {
 		fmt.Println("Error while running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/stopwatch/main.go b/examples/stopwatch/main.go
index e203327..69884b1 100644
--- a/examples/stopwatch/main.go
+++ b/examples/stopwatch/main.go
@@ -96,7 +96,7 @@ func main() {
 
 	m.keymap.start.SetEnabled(false)
 
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Oh no, it didn't work:", err)
 		os.Exit(1)
 	}
diff --git a/examples/table/main.go b/examples/table/main.go
index b0f529f..76736f3 100644
--- a/examples/table/main.go
+++ b/examples/table/main.go
@@ -177,7 +177,7 @@ func main() {
 	t.SetStyles(s)
 
 	m := model{t}
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/tabs/main.go b/examples/tabs/main.go
index f5cce63..463c5c4 100644
--- a/examples/tabs/main.go
+++ b/examples/tabs/main.go
@@ -46,8 +46,6 @@ func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
 }
 
 var (
-	defaultWidth = 20
-
 	inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
 	activeTabBorder   = tabBorderWithBottom("┘", " ", "└")
 	docStyle          = lipgloss.NewStyle().Padding(1, 2, 1, 2)
@@ -95,7 +93,7 @@ func main() {
 	tabs := []string{"Lip Gloss", "Blush", "Eye Shadow", "Mascara", "Foundation"}
 	tabContent := []string{"Lip Gloss Tab", "Blush Tab", "Eye Shadow Tab", "Mascara Tab", "Foundation Tab"}
 	m := model{Tabs: tabs, TabContent: tabContent}
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Error running program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/textarea/main.go b/examples/textarea/main.go
index 48ad44d..109059c 100644
--- a/examples/textarea/main.go
+++ b/examples/textarea/main.go
@@ -14,7 +14,7 @@ import (
 func main() {
 	p := tea.NewProgram(initialModel())
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/examples/textinput/main.go b/examples/textinput/main.go
index af90208..7c25344 100644
--- a/examples/textinput/main.go
+++ b/examples/textinput/main.go
@@ -13,15 +13,13 @@ import (
 
 func main() {
 	p := tea.NewProgram(initialModel())
-
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		log.Fatal(err)
 	}
 }
 
 type (
-	tickMsg struct{}
-	errMsg  error
+	errMsg error
 )
 
 type model struct {
diff --git a/examples/textinputs/main.go b/examples/textinputs/main.go
index 587b8df..2620bfc 100644
--- a/examples/textinputs/main.go
+++ b/examples/textinputs/main.go
@@ -170,7 +170,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(initialModel()).Start(); err != nil {
+	if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
 		fmt.Printf("could not start program: %s\n", err)
 		os.Exit(1)
 	}
diff --git a/examples/timer/main.go b/examples/timer/main.go
index bf3a2c9..7ee4f7d 100644
--- a/examples/timer/main.go
+++ b/examples/timer/main.go
@@ -115,7 +115,7 @@ func main() {
 	}
 	m.keymap.start.SetEnabled(false)
 
-	if err := tea.NewProgram(m).Start(); err != nil {
+	if _, err := tea.NewProgram(m).Run(); err != nil {
 		fmt.Println("Uh oh, we encountered an error:", err)
 		os.Exit(1)
 	}
diff --git a/examples/tui-daemon-combo/main.go b/examples/tui-daemon-combo/main.go
index de2d61c..cbe7799 100644
--- a/examples/tui-daemon-combo/main.go
+++ b/examples/tui-daemon-combo/main.go
@@ -45,7 +45,7 @@ func main() {
 	}
 
 	p := tea.NewProgram(newModel(), opts...)
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("Error starting Bubble Tea program:", err)
 		os.Exit(1)
 	}
diff --git a/examples/views/main.go b/examples/views/main.go
index f27d699..a06b592 100644
--- a/examples/views/main.go
+++ b/examples/views/main.go
@@ -41,7 +41,7 @@ var (
 func main() {
 	initialModel := model{0, false, 10, 0, 0, false, false}
 	p := tea.NewProgram(initialModel)
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Println("could not start program:", err)
 	}
 }
@@ -115,16 +115,15 @@ func (m model) View() string {
 // Update loop for the first view where you're choosing a task.
 func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "j", "down":
-			m.Choice += 1
+			m.Choice++
 			if m.Choice > 3 {
 				m.Choice = 3
 			}
 		case "k", "up":
-			m.Choice -= 1
+			m.Choice--
 			if m.Choice < 0 {
 				m.Choice = 0
 			}
@@ -138,7 +137,7 @@ func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
 			m.Quitting = true
 			return m, tea.Quit
 		}
-		m.Ticks -= 1
+		m.Ticks--
 		return m, tick()
 	}
 
@@ -148,10 +147,9 @@ func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
 // Update loop for the second view after a choice has been made
 func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
 	switch msg.(type) {
-
 	case frameMsg:
 		if !m.Loaded {
-			m.Frames += 1
+			m.Frames++
 			m.Progress = ease.OutBounce(float64(m.Frames) / float64(100))
 			if m.Progress >= 1 {
 				m.Progress = 1
@@ -168,7 +166,7 @@ func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
 				m.Quitting = true
 				return m, tea.Quit
 			}
-			m.Ticks -= 1
+			m.Ticks--
 			return m, tick()
 		}
 	}
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/options.go b/options.go
index 6ede540..59d0685 100644
--- a/options.go
+++ b/options.go
@@ -38,13 +38,21 @@ func WithInputTTY() ProgramOption {
 	}
 }
 
+// WithoutSignalHandler disables the signal handler that Bubble Tea sets up for
+// Programs. This is useful if you want to handle signals yourself.
+func WithoutSignalHandler() ProgramOption {
+	return func(p *Program) {
+		p.startupOptions |= withoutSignalHandler
+	}
+}
+
 // WithoutCatchPanics disables the panic catching that Bubble Tea does by
 // default. If panic catching is disabled the terminal will be in a fairly
 // unusable state after a panic because Bubble Tea will not perform its usual
 // cleanup on exit.
 func WithoutCatchPanics() ProgramOption {
 	return func(p *Program) {
-		p.CatchPanics = false
+		p.startupOptions |= withoutCatchPanics
 	}
 }
 
@@ -55,7 +63,7 @@ func WithoutCatchPanics() ProgramOption {
 // Example:
 //
 //	p := tea.NewProgram(Model{}, tea.WithAltScreen())
-//	if err := p.Start(); err != nil {
+//	if _, err := p.Run(); err != nil {
 //	    fmt.Println("Error running program:", err)
 //	    os.Exit(1)
 //	}
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 ccb0c50..b94ca4e 100644
--- a/tea.go
+++ b/tea.go
@@ -11,13 +11,14 @@ package tea
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
 	"os/signal"
 	"runtime/debug"
+	"sync"
 	"syscall"
-	"time"
 
 	"github.com/containerd/console"
 	isatty "github.com/mattn/go-isatty"
@@ -26,6 +27,9 @@ import (
 	"golang.org/x/term"
 )
 
+// ErrProgramKilled is returned by [Program.Run] when the program got killed.
+var ErrProgramKilled = errors.New("program was killed")
+
 // Msg contain data from the result of a IO operation. Msgs trigger the update
 // function and, henceforth, the UI.
 type Msg interface{}
@@ -54,6 +58,8 @@ type Model interface {
 // update function.
 type Cmd func() Msg
 
+type handlers []chan struct{}
+
 // Options to customize the program during its initialization. These are
 // generally set with ProgramOptions.
 //
@@ -71,6 +77,13 @@ const (
 	withInputTTY
 	withCustomInput
 	withANSICompressor
+	withoutSignalHandler
+
+	// Catching panics is incredibly useful for restoring the terminal to a
+	// usable state after a panic occurs. When this is set, Bubble Tea will
+	// recover from panics, print the stack trace, and disable raw mode. This
+	// feature is on by default.
+	withoutCatchPanics
 )
 
 // Program is a terminal user interface.
@@ -81,31 +94,26 @@ type Program struct {
 	// treated as bits. These options can be set via various ProgramOptions.
 	startupOptions startupOptions
 
-	ctx context.Context
+	ctx    context.Context
+	cancel context.CancelFunc
 
-	msgs         chan Msg
-	errs         chan error
-	readLoopDone chan struct{}
+	msgs chan Msg
+	errs chan error
 
-	output        *termenv.Output // where to send output. this will usually be os.Stdout.
+	// where to send output, this will usually be os.Stdout.
+	output        *termenv.Output
 	restoreOutput func() error
-	input         io.Reader // this will usually be os.Stdin.
-	cancelReader  cancelreader.CancelReader
+	renderer      renderer
 
-	renderer           renderer
-	altScreenWasActive bool // was the altscreen active before releasing the terminal?
-
-	// CatchPanics is incredibly useful for restoring the terminal to a usable
-	// state after a panic occurs. When this is set, Bubble Tea will recover
-	// from panics, print the stack trace, and disable raw mode. This feature
-	// is on by default.
-	CatchPanics bool
-
-	ignoreSignals bool
-
-	killc chan bool
+	// where to read inputs from, this will usually be os.Stdin.
+	input        io.Reader
+	cancelReader cancelreader.CancelReader
+	readLoopDone chan struct{}
+	console      console.Console
 
-	console console.Console
+	// was the altscreen active before releasing the terminal?
+	altScreenWasActive bool
+	ignoreSignals      bool
 
 	// Stores the original reference to stdin for cases where input is not a
 	// TTY on windows and we've automatically opened CONIN$ to receive input.
@@ -132,8 +140,6 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
 		initialModel: model,
 		input:        os.Stdin,
 		msgs:         make(chan Msg),
-		CatchPanics:  true,
-		killc:        make(chan bool, 1),
 	}
 
 	// Apply all options to the program.
@@ -154,68 +160,8 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
 	return p
 }
 
-// StartReturningModel initializes the program. Returns the final model.
-func (p *Program) StartReturningModel() (Model, error) {
-	cmds := make(chan Cmd)
-	p.errs = make(chan error)
-
-	// Channels for managing goroutine lifecycles.
-	var (
-		sigintLoopDone = make(chan struct{})
-		cmdLoopDone    = make(chan struct{})
-		resizeLoopDone = make(chan struct{})
-		initSignalDone = make(chan struct{})
-
-		waitForGoroutines = func(withReadLoop bool) {
-			if withReadLoop {
-				p.waitForReadLoop()
-			}
-			<-cmdLoopDone
-			<-resizeLoopDone
-			<-sigintLoopDone
-			<-initSignalDone
-		}
-	)
-
-	var cancelContext context.CancelFunc
-	p.ctx, cancelContext = context.WithCancel(context.Background())
-	defer cancelContext()
-
-	switch {
-	case p.startupOptions.has(withInputTTY):
-		// Open a new TTY, by request
-		f, err := openInputTTY()
-		if err != nil {
-			return p.initialModel, err
-		}
-
-		defer f.Close() //nolint:errcheck
-
-		p.input = f
-
-	case !p.startupOptions.has(withCustomInput):
-		// If the user hasn't set a custom input, and input's not a terminal,
-		// open a TTY so we can capture input as normal. This will allow things
-		// to "just work" in cases where data was piped or redirected into this
-		// application.
-		f, isFile := p.input.(*os.File)
-		if !isFile {
-			break
-		}
-
-		if isatty.IsTerminal(f.Fd()) {
-			break
-		}
-
-		f, err := openInputTTY()
-		if err != nil {
-			return p.initialModel, err
-		}
-
-		defer f.Close() //nolint:errcheck
-
-		p.input = f
-	}
+func (p *Program) handleSignals() chan struct{} {
+	ch := make(chan struct{})
 
 	// Listen for SIGINT and SIGTERM.
 	//
@@ -230,13 +176,14 @@ func (p *Program) StartReturningModel() (Model, error) {
 		signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
 		defer func() {
 			signal.Stop(sig)
-			close(sigintLoopDone)
+			close(ch)
 		}()
 
 		for {
 			select {
 			case <-p.ctx.Done():
 				return
+
 			case <-sig:
 				if !p.ignoreSignals {
 					p.msgs <- quitMsg{}
@@ -246,67 +193,12 @@ func (p *Program) StartReturningModel() (Model, error) {
 		}
 	}()
 
-	if p.CatchPanics {
-		defer func() {
-			if r := recover(); r != nil {
-				p.shutdown(true)
-				fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
-				debug.PrintStack()
-				return
-			}
-		}()
-	}
-
-	// If no renderer is set use the standard one.
-	if p.renderer == nil {
-		p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
-	}
-
-	// Check if output is a TTY before entering raw mode, hiding the cursor and
-	// so on.
-	if err := p.initTerminal(); err != nil {
-		return p.initialModel, err
-	}
-
-	// Honor program startup options.
-	if p.startupOptions&withAltScreen != 0 {
-		p.renderer.enterAltScreen()
-	}
-	if p.startupOptions&withMouseCellMotion != 0 {
-		p.renderer.enableMouseCellMotion()
-	} else if p.startupOptions&withMouseAllMotion != 0 {
-		p.renderer.enableMouseAllMotion()
-	}
-
-	// Initialize the program.
-	model := p.initialModel
-	if initCmd := model.Init(); initCmd != nil {
-		go func() {
-			defer close(initSignalDone)
-			select {
-			case cmds <- initCmd:
-			case <-p.ctx.Done():
-			}
-		}()
-	} else {
-		close(initSignalDone)
-	}
-
-	// Start the renderer.
-	p.renderer.start()
-
-	// Render the initial view.
-	p.renderer.write(model.View())
+	return ch
+}
 
-	// Subscribe to user input.
-	if p.input != nil {
-		if err := p.initCancelReader(); err != nil {
-			return model, err
-		}
-	} else {
-		defer close(p.readLoopDone)
-	}
-	defer p.cancelReader.Close() //nolint:errcheck
+// handleResize handles terminal resize events.
+func (p *Program) handleResize() chan struct{} {
+	ch := make(chan struct{})
 
 	if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) {
 		// Get the initial terminal size and send it to the program.
@@ -323,20 +215,27 @@ func (p *Program) StartReturningModel() (Model, error) {
 		}()
 
 		// Listen for window resizes.
-		go listenForResize(p.ctx, f, p.msgs, p.errs, resizeLoopDone)
+		go listenForResize(p.ctx, f, p.msgs, p.errs, ch)
 	} else {
-		close(resizeLoopDone)
+		close(ch)
 	}
 
-	// Process commands.
+	return ch
+}
+
+// handleCommands runs commands in a goroutine and sends the result to the
+// program's message channel.
+func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
+	ch := make(chan struct{})
+
 	go func() {
-		defer close(cmdLoopDone)
+		defer close(ch)
 
 		for {
 			select {
 			case <-p.ctx.Done():
-
 				return
+
 			case cmd := <-cmds:
 				if cmd == nil {
 					continue
@@ -357,25 +256,24 @@ func (p *Program) StartReturningModel() (Model, error) {
 		}
 	}()
 
-	// Handle updates and draw.
+	return ch
+}
+
+// eventLoop is the central message loop. It receives and handles the default
+// Bubble Tea messages, update the model and triggers redraws.
+func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
 	for {
 		select {
-		case <-p.killc:
-			return nil, nil
+		case <-p.ctx.Done():
+			return model, nil
+
 		case err := <-p.errs:
-			cancelContext()
-			waitForGoroutines(p.cancelReader.Cancel())
-			p.shutdown(false)
 			return model, err
 
 		case msg := <-p.msgs:
-
 			// Handle special internal messages.
 			switch msg := msg.(type) {
 			case quitMsg:
-				cancelContext()
-				waitForGoroutines(p.cancelReader.Cancel())
-				p.shutdown(false)
 				return model, nil
 
 			case clearScreenMsg:
@@ -438,9 +336,164 @@ func (p *Program) StartReturningModel() (Model, error) {
 	}
 }
 
-// Start initializes the program. Ignores the final model.
+// Run initializes the program and runs its event loops, blocking until it gets
+// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
+// Returns the final model.
+func (p *Program) Run() (Model, error) {
+	handlers := handlers{}
+	cmds := make(chan Cmd)
+	p.errs = make(chan error)
+
+	p.ctx, p.cancel = context.WithCancel(context.Background())
+	defer p.cancel()
+
+	switch {
+	case p.startupOptions.has(withInputTTY):
+		// Open a new TTY, by request
+		f, err := openInputTTY()
+		if err != nil {
+			return p.initialModel, err
+		}
+		p.input = f
+
+	case !p.startupOptions.has(withCustomInput):
+		// If the user hasn't set a custom input, and input's not a terminal,
+		// open a TTY so we can capture input as normal. This will allow things
+		// to "just work" in cases where data was piped or redirected into this
+		// application.
+		f, isFile := p.input.(*os.File)
+		if !isFile {
+			break
+		}
+		if isatty.IsTerminal(f.Fd()) {
+			break
+		}
+
+		f, err := openInputTTY()
+		if err != nil {
+			return p.initialModel, err
+		}
+		p.input = f
+	}
+
+	if f, ok := p.input.(io.ReadCloser); ok {
+		defer f.Close() //nolint:errcheck
+	}
+
+	// Handle signals.
+	if !p.startupOptions.has(withoutSignalHandler) {
+		handlers.add(p.handleSignals())
+	}
+
+	// Recover from panics.
+	if !p.startupOptions.has(withoutCatchPanics) {
+		defer func() {
+			if r := recover(); r != nil {
+				p.shutdown(true)
+				fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
+				debug.PrintStack()
+				return
+			}
+		}()
+	}
+
+	// If no renderer is set use the standard one.
+	if p.renderer == nil {
+		p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
+	}
+
+	// Check if output is a TTY before entering raw mode, hiding the cursor and
+	// so on.
+	if err := p.initTerminal(); err != nil {
+		return p.initialModel, err
+	}
+
+	// Honor program startup options.
+	if p.startupOptions&withAltScreen != 0 {
+		p.renderer.enterAltScreen()
+	}
+	if p.startupOptions&withMouseCellMotion != 0 {
+		p.renderer.enableMouseCellMotion()
+	} else if p.startupOptions&withMouseAllMotion != 0 {
+		p.renderer.enableMouseAllMotion()
+	}
+
+	// Initialize the program.
+	model := p.initialModel
+	if initCmd := model.Init(); initCmd != nil {
+		ch := make(chan struct{})
+		handlers.add(ch)
+
+		go func() {
+			defer close(ch)
+
+			select {
+			case cmds <- initCmd:
+			case <-p.ctx.Done():
+			}
+		}()
+	}
+
+	// Start the renderer.
+	p.renderer.start()
+
+	// Render the initial view.
+	p.renderer.write(model.View())
+
+	// Subscribe to user input.
+	if p.input != nil {
+		if err := p.initCancelReader(); err != nil {
+			return model, err
+		}
+		defer p.cancelReader.Close() //nolint:errcheck
+	}
+
+	// Handle resize events.
+	handlers.add(p.handleResize())
+
+	// Process commands.
+	handlers.add(p.handleCommands(cmds))
+
+	// Run event loop, handle updates and draw.
+	model, err := p.eventLoop(model, cmds)
+	killed := p.ctx.Err() != nil
+	if killed {
+		err = ErrProgramKilled
+	}
+
+	// Tear down.
+	p.cancel()
+
+	// Wait for input loop to finish.
+	if p.cancelReader.Cancel() {
+		p.waitForReadLoop()
+	}
+
+	// Wait for all handlers to finish.
+	handlers.shutdown()
+
+	// Restore terminal state.
+	p.shutdown(killed)
+
+	return model, err
+}
+
+// StartReturningModel initializes the program and runs its event loops,
+// blocking until it gets terminated by either [Program.Quit], [Program.Kill],
+// or its signal handler. Returns the final model.
+//
+// Deprecated: please use [Program.Run] instead.
+func (p *Program) StartReturningModel() (Model, error) {
+	return p.Run()
+}
+
+// Start initializes the program and runs its event loops, blocking until it
+// gets terminated by either [Program.Quit], [Program.Kill], or its signal
+// handler.
+//
+// Deprecated: please use [Program.Run] instead.
 func (p *Program) Start() error {
-	_, err := p.StartReturningModel()
+	_, err := p.Run()
 	return err
 }
 
@@ -448,7 +501,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
@@ -467,9 +520,9 @@ func (p *Program) Quit() {
 
 // Kill stops the program immediately and restores the former terminal state.
 // The final render that you would normally see when quitting will be skipped.
+// [program.Run] returns a [ErrProgramKilled] error.
 func (p *Program) Kill() {
-	p.killc <- true
-	p.shutdown(true)
+	p.cancel()
 }
 
 // shutdown performs operations to free up resources and restore the terminal
@@ -482,11 +535,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()
 	}
@@ -496,14 +546,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()
 }
 
@@ -516,17 +562,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
 }
 
@@ -553,3 +599,22 @@ func (p *Program) Printf(template string, args ...interface{}) {
 		messageBody: fmt.Sprintf(template, args...),
 	}
 }
+
+// Adds a handler to the list of handlers. We wait for all handlers to terminate
+// gracefully on shutdown.
+func (h *handlers) add(ch chan struct{}) {
+	*h = append(*h, ch)
+}
+
+// Shutdown waits for all handlers to terminate.
+func (h handlers) shutdown() {
+	var wg sync.WaitGroup
+	for _, ch := range h {
+		wg.Add(1)
+		go func(ch chan struct{}) {
+			<-ch
+			wg.Done()
+		}(ch)
+	}
+	wg.Wait()
+}
diff --git a/tty.go b/tty.go
index 80072d2..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 {
@@ -51,12 +60,12 @@ func (p *Program) initCancelReader() error {
 	}
 
 	p.readLoopDone = make(chan struct{})
-	go p.eventLoop()
+	go p.readLoop()
 
 	return nil
 }
 
-func (p *Program) eventLoop() {
+func (p *Program) readLoop() {
 	defer close(p.readLoopDone)
 
 	for {
@@ -79,11 +88,6 @@ func (p *Program) eventLoop() {
 	}
 }
 
-// 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 {
diff --git a/tutorials/basics/README.md b/tutorials/basics/README.md
index adec6aa..1b40823 100644
--- a/tutorials/basics/README.md
+++ b/tutorials/basics/README.md
@@ -200,7 +200,7 @@ The last step is to simply run our program. We pass our initial model to
 ```go
 func main() {
     p := tea.NewProgram(initialModel())
-    if err := p.Start(); err != nil {
+    if _, err := p.Run(); err != nil {
         fmt.Printf("Alas, there's been an error: %v", err)
         os.Exit(1)
     }
diff --git a/tutorials/basics/main.go b/tutorials/basics/main.go
index 05d40b4..0c5b573 100644
--- a/tutorials/basics/main.go
+++ b/tutorials/basics/main.go
@@ -79,7 +79,7 @@ func (m model) View() string {
 
 func main() {
 	p := tea.NewProgram(initialModel())
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		fmt.Printf("Alas, there's been an error: %v", err)
 		os.Exit(1)
 	}
diff --git a/tutorials/commands/README.md b/tutorials/commands/README.md
index 2492058..661f83b 100644
--- a/tutorials/commands/README.md
+++ b/tutorials/commands/README.md
@@ -169,7 +169,7 @@ as a `struct` with defaults.
 
 ```go
 func main() {
-    if err := tea.NewProgram(model{}).Start(); err != nil {
+    if _, err := tea.NewProgram(model{}).Run(); err != nil {
         fmt.Printf("Uh oh, there was an error: %v\n", err)
         os.Exit(1)
     }
@@ -189,7 +189,7 @@ that returns a command. For example:
 ```go
 func cmdWithArg(id int) tea.Cmd {
     return func() tea.Msg {
-        return someMsg{id: int}
+        return someMsg{id: id}
     }
 }
 ```
diff --git a/tutorials/commands/main.go b/tutorials/commands/main.go
index 47d048a..8ed68b7 100644
--- a/tutorials/commands/main.go
+++ b/tutorials/commands/main.go
@@ -22,6 +22,8 @@ func checkServer() tea.Msg {
 	if err != nil {
 		return errMsg{err}
 	}
+	defer res.Body.Close()
+
 	return statusMsg(res.StatusCode)
 }
 
@@ -39,7 +41,6 @@ func (m model) Init() tea.Cmd {
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-
 	case statusMsg:
 		m.status = int(msg)
 		return m, tea.Quit
@@ -70,7 +71,7 @@ func (m model) View() string {
 }
 
 func main() {
-	if err := tea.NewProgram(model{}).Start(); err != nil {
+	if _, err := tea.NewProgram(model{}).Run(); err != nil {
 		fmt.Printf("Uh oh, there was an error: %v\n", err)
 		os.Exit(1)
 	}


Test patch:
diff --git a/exec_test.go b/exec_test.go
index 37de3f5..b5fbbfa 100644
--- a/exec_test.go
+++ b/exec_test.go
@@ -7,10 +7,14 @@ import (
 )
 
 type execFinishedMsg struct{ err error }
-type testExecModel struct{ err error }
+
+type testExecModel struct {
+	cmd string
+	err error
+}
 
 func (m testExecModel) Init() Cmd {
-	c := exec.Command("true") //nolint:gosec
+	c := exec.Command(m.cmd) //nolint:gosec
 	return ExecProcess(c, func(err error) Msg {
 		return execFinishedMsg{err}
 	})
@@ -33,16 +37,45 @@ func (m *testExecModel) View() string {
 }
 
 func TestTeaExec(t *testing.T) {
-	var buf bytes.Buffer
-	var in bytes.Buffer
-
-	m := &testExecModel{}
-	p := NewProgram(m, WithInput(&in), WithOutput(&buf))
-	if err := p.Start(); err != nil {
-		t.Fatal(err)
+	tests := []struct {
+		name      string
+		cmd       string
+		expectErr bool
+	}{
+		{
+			name:      "true",
+			cmd:       "true",
+			expectErr: false,
+		},
+		{
+			name:      "false",
+			cmd:       "false",
+			expectErr: true,
+		},
+		{
+			name:      "invalid command",
+			cmd:       "invalid",
+			expectErr: true,
+		},
 	}
 
-	if m.err != nil {
-		t.Fatal(m.err)
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			var in bytes.Buffer
+
+			m := &testExecModel{cmd: test.cmd}
+			p := NewProgram(m, WithInput(&in), WithOutput(&buf))
+			if _, err := p.Run(); err != nil {
+				t.Error(err)
+			}
+
+			if m.err != nil && !test.expectErr {
+				t.Errorf("expected no error, got %v", m.err)
+			}
+			if m.err == nil && test.expectErr {
+				t.Error("expected error, got nil")
+			}
+		})
 	}
 }
diff --git a/nil_renderer_test.go b/nil_renderer_test.go
index 9ab7b58..ab94e34 100644
--- a/nil_renderer_test.go
+++ b/nil_renderer_test.go
@@ -13,4 +13,12 @@ func TestNilRenderer(t *testing.T) {
 	if r.altScreen() {
 		t.Errorf("altScreen should always return false")
 	}
+	r.exitAltScreen()
+	r.clearScreen()
+	r.showCursor()
+	r.hideCursor()
+	r.enableMouseCellMotion()
+	r.disableMouseCellMotion()
+	r.enableMouseAllMotion()
+	r.disableMouseAllMotion()
 }
diff --git a/options_test.go b/options_test.go
index 6648ddd..7e08f58 100644
--- a/options_test.go
+++ b/options_test.go
@@ -25,13 +25,6 @@ func TestOptions(t *testing.T) {
 		}
 	})
 
-	t.Run("catch panics", func(t *testing.T) {
-		p := NewProgram(nil, WithoutCatchPanics())
-		if p.CatchPanics {
-			t.Errorf("catch panics should not have been set")
-		}
-	})
-
 	t.Run("renderer", func(t *testing.T) {
 		p := NewProgram(nil, WithoutRenderer())
 		switch p.renderer.(type) {
@@ -62,6 +55,14 @@ func TestOptions(t *testing.T) {
 			exercise(t, WithANSICompressor(), withANSICompressor)
 		})
 
+		t.Run("without catch panics", func(t *testing.T) {
+			exercise(t, WithoutCatchPanics(), withoutCatchPanics)
+		})
+
+		t.Run("without signal handler", func(t *testing.T) {
+			exercise(t, WithoutSignalHandler(), withoutSignalHandler)
+		})
+
 		t.Run("mouse cell motion", func(t *testing.T) {
 			p := NewProgram(nil, WithMouseAllMotion(), WithMouseCellMotion())
 			if !p.startupOptions.has(withMouseCellMotion) {
diff --git a/screen_test.go b/screen_test.go
new file mode 100644
index 0000000..33b5169
--- /dev/null
+++ b/screen_test.go
@@ -0,0 +1,76 @@
+package tea
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestClearMsg(t *testing.T) {
+	tests := []struct {
+		name     string
+		cmds     sequenceMsg
+		expected string
+	}{
+		{
+			name:     "clear_screen",
+			cmds:     []Cmd{ClearScreen},
+			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[?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[?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[?25h\x1b[?1002l\x1b[?1003l",
+		},
+		{
+			name:     "mouse_allmotion",
+			cmds:     []Cmd{EnableMouseAllMotion},
+			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[?25h\x1b[?1002l\x1b[?1003l",
+		},
+		{
+			name:     "cursor_hide",
+			cmds:     []Cmd{HideCursor},
+			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[?25h\x1b[?1002l\x1b[?1003l",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			var in bytes.Buffer
+
+			m := &testModel{}
+			p := NewProgram(m, WithInput(&in), WithOutput(&buf))
+
+			test.cmds = append(test.cmds, Quit)
+			go p.Send(test.cmds)
+
+			if _, err := p.Run(); err != nil {
+				t.Fatal(err)
+			}
+
+			if buf.String() != test.expected {
+				t.Errorf("expected embedded sequence, got %q", buf.String())
+			}
+		})
+	}
+}
diff --git a/tea_test.go b/tea_test.go
index 7a3d315..c61c518 100644
--- a/tea_test.go
+++ b/tea_test.go
@@ -7,8 +7,11 @@ import (
 	"time"
 )
 
+type incrementMsg struct{}
+
 type testModel struct {
 	executed atomic.Value
+	counter  atomic.Value
 }
 
 func (m testModel) Init() Cmd {
@@ -17,9 +20,18 @@ func (m testModel) Init() Cmd {
 
 func (m *testModel) Update(msg Msg) (Model, Cmd) {
 	switch msg.(type) {
+	case incrementMsg:
+		i := m.counter.Load()
+		if i == nil {
+			m.counter.Store(1)
+		} else {
+			m.counter.Store(i.(int) + 1)
+		}
+
 	case KeyMsg:
 		return m, Quit
 	}
+
 	return m, nil
 }
 
@@ -34,7 +46,7 @@ func TestTeaModel(t *testing.T) {
 	in.Write([]byte("q"))
 
 	p := NewProgram(&testModel{}, WithInput(&in), WithOutput(&buf))
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		t.Fatal(err)
 	}
 
@@ -59,7 +71,7 @@ func TestTeaQuit(t *testing.T) {
 		}
 	}()
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != nil {
 		t.Fatal(err)
 	}
 }
@@ -80,7 +92,60 @@ func TestTeaKill(t *testing.T) {
 		}
 	}()
 
-	if err := p.Start(); err != nil {
+	if _, err := p.Run(); err != ErrProgramKilled {
+		t.Fatalf("Expected %v, got %v", ErrProgramKilled, err)
+	}
+}
+
+func TestTeaBatchMsg(t *testing.T) {
+	var buf bytes.Buffer
+	var in bytes.Buffer
+
+	inc := func() Msg {
+		return incrementMsg{}
+	}
+
+	m := &testModel{}
+	p := NewProgram(m, WithInput(&in), WithOutput(&buf))
+	go func() {
+		p.Send(batchMsg{inc, inc})
+
+		for {
+			time.Sleep(time.Millisecond)
+			i := m.counter.Load()
+			if i != nil && i.(int) >= 2 {
+				p.Quit()
+				return
+			}
+		}
+	}()
+
+	if _, err := p.Run(); err != nil {
+		t.Fatal(err)
+	}
+
+	if m.counter.Load() != 2 {
+		t.Fatalf("counter should be 2, got %d", m.counter.Load())
+	}
+}
+
+func TestTeaSequenceMsg(t *testing.T) {
+	var buf bytes.Buffer
+	var in bytes.Buffer
+
+	inc := func() Msg {
+		return incrementMsg{}
+	}
+
+	m := &testModel{}
+	p := NewProgram(m, WithInput(&in), WithOutput(&buf))
+	go p.Send(sequenceMsg{inc, inc, Quit})
+
+	if _, err := p.Run(); err != nil {
 		t.Fatal(err)
 	}
+
+	if m.counter.Load() != 2 {
+		t.Fatalf("counter should be 2, got %d", m.counter.Load())
+	}
 }