Repository: charmbracelet/bubbletea
Pull request #418 (2022-08-25 15:45:44)

Support the insert key
This can be used in e.g. textarea / textinput to enable "overwrite
mode".

cc @muesli 


Patch:
diff --git a/.gitignore b/.gitignore
index 5919448..9cc5235 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,4 +20,3 @@ tutorials/basics/basics
 tutorials/commands/commands
 .idea
 coverage.txt
-README.md.*
diff --git a/README.md b/README.md
index 4889573..65edc9f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-Bubble Tea
-==========
+# Bubble Tea
 
 <p>
     <img src="https://stuff.charm.sh/bubbletea/bubbletea-github-header-simple.png" width="313" alt="Bubble Tea Title Treatment"><br>
@@ -12,38 +11,254 @@ The fun, functional and stateful way to build terminal apps. A Go framework
 based on [The Elm Architecture][elm]. Bubble Tea is well-suited for simple and
 complex terminal applications, either inline, full-window, or a mix of both.
 
-<p>
-    <img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif?0" width="800" alt="Bubble Tea Example">
-</p>
+<picture>
+  <source media="(max-width: 800px)" srcset="https://stuff.charm.sh/bubbletea/bubbletea-example.gif?0">
+  <source media="(min-width: 800px)" width="800" srcset="https://stuff.charm.sh/bubbletea/bubbletea-example.gif?0">
+  <img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif?0" alt="Bubble Tea Example">
+</picture>
 
 Bubble Tea is in use in production and includes a number of features and
 performance optimizations we’ve added along the way. Among those is a standard
 framerate-based renderer, a renderer for high-performance scrollable
 regions which works alongside the main renderer, and mouse support.
 
-## Getting Started
-
-We recommend starting with the [basics tutorial][basics] followed by the
-[commands tutorial][commands], both of which should give you a good
-understanding of how things work.
+To get started, see the tutorial below, the [examples][examples], the
+[docs][docs], the [video tutorials][youtube] and some common [resources](#libraries-we-use-with-bubble-tea).
 
-There are a bunch of [examples][examples], too!
+[youtube]: https://charm.sh/yt
 
-[basics]: https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
-[commands]: https://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands
-[documentation]: https://github.com/charmbracelet/bubbletea/tree/master/docs
-[examples]: https://github.com/charmbracelet/bubbletea/tree/master/examples
+## By the way
 
-## Components
-
-For a bunch of basic user interface components check out [Bubbles][bubbles],
-the official Bubble Tea component library.
+Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
 
 <p>
     <a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles/bubbles-badge.png" width="174" alt="Bubbles Badge"></a>&nbsp;&nbsp;
     <a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example from Bubbles"></a>
 </p>
 
+* * *
+
+## Tutorial
+
+Bubble Tea is based on the functional design paradigms of [The Elm
+Architecture][elm], which happens to work nicely with Go. It's a delightful way
+to build applications.
+
+This tutorial assumes you have a working knowledge of Go.
+
+By the way, the non-annotated source code for this program is available
+[on GitHub][tut-source].
+
+[elm]: https://guide.elm-lang.org/architecture/
+[tut-source]:https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
+
+### Enough! Let's get to it.
+
+For this tutorial, we're making a shopping list.
+
+To start we'll define our package and import some libraries. Our only external
+import will be the Bubble Tea library, which we'll call `tea` for short.
+
+```go
+package main
+
+import (
+    "fmt"
+    "os"
+
+    tea "github.com/charmbracelet/bubbletea"
+)
+```
+
+Bubble Tea programs are comprised of a **model** that describes the application
+state and three simple methods on that model:
+
+* **Init**, a function that returns an initial command for the application to run.
+* **Update**, a function that handles incoming events and updates the model accordingly.
+* **View**, a function that renders the UI based on the data in the model.
+
+### The Model
+
+So let's start by defining our model which will store our application's state.
+It can be any type, but a `struct` usually makes the most sense.
+
+```go
+type model struct {
+    choices  []string           // items on the to-do list
+    cursor   int                // which to-do list item our cursor is pointing at
+    selected map[int]struct{}   // which to-do items are selected
+}
+```
+
+### Initialization
+
+Next, we’ll define our application’s initial state. In this case, we’re defining
+a function to return our initial model, however, we could just as easily define
+the initial model as a variable elsewhere, too.
+
+```go
+func initialModel() model {
+	return model{
+		// Our to-do list is a grocery list
+		choices:  []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
+
+		// A map which indicates which choices are selected. We're using
+		// the  map like a mathematical set. The keys refer to the indexes
+		// of the `choices` slice, above.
+		selected: make(map[int]struct{}),
+	}
+}
+```
+
+Next, we define the `Init` method. `Init` can return a `Cmd` that could perform
+some initial I/O. For now, we don't need to do any I/O, so for the command,
+we'll just return `nil`, which translates to "no command."
+
+```go
+func (m model) Init() tea.Cmd {
+    // Just return `nil`, which means "no I/O right now, please."
+    return nil
+}
+```
+
+### The Update Method
+
+Next up is the update method. The update function is called when ”things
+happen.” Its job is to look at what has happened and return an updated model in
+response. It can also return a `Cmd` to make more things happen, but for now
+don't worry about that part.
+
+In our case, when a user presses the down arrow, `Update`’s job is to notice
+that the down arrow was pressed and move the cursor accordingly (or not).
+
+The “something happened” comes in the form of a `Msg`, which can be any type.
+Messages are the result of some I/O that took place, such as a keypress, timer
+tick, or a response from a server.
+
+We usually figure out which type of `Msg` we received with a type switch, but
+you could also use a type assertion.
+
+For now, we'll just deal with `tea.KeyMsg` messages, which are automatically
+sent to the update function when keys are pressed.
+
+```go
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+    switch msg := msg.(type) {
+
+    // Is it a key press?
+    case tea.KeyMsg:
+
+        // Cool, what was the actual key pressed?
+        switch msg.String() {
+
+        // These keys should exit the program.
+        case "ctrl+c", "q":
+            return m, tea.Quit
+
+        // The "up" and "k" keys move the cursor up
+        case "up", "k":
+            if m.cursor > 0 {
+                m.cursor--
+            }
+
+        // The "down" and "j" keys move the cursor down
+        case "down", "j":
+            if m.cursor < len(m.choices)-1 {
+                m.cursor++
+            }
+
+        // The "enter" key and the spacebar (a literal space) toggle
+        // the selected state for the item that the cursor is pointing at.
+        case "enter", " ":
+            _, ok := m.selected[m.cursor]
+            if ok {
+                delete(m.selected, m.cursor)
+            } else {
+                m.selected[m.cursor] = struct{}{}
+            }
+        }
+    }
+
+    // Return the updated model to the Bubble Tea runtime for processing.
+    // Note that we're not returning a command.
+    return m, nil
+}
+```
+
+You may have noticed that <kbd>ctrl+c</kbd> and <kbd>q</kbd> above return
+a `tea.Quit` command with the model. That’s a special command which instructs
+the Bubble Tea runtime to quit, exiting the program.
+
+### The View Method
+
+At last, it’s time to render our UI. Of all the methods, the view is the
+simplest. We look at the model in its current state and use it to return
+a `string`. That string is our UI!
+
+Because the view describes the entire UI of your application, you don’t have to
+worry about redrawing logic and stuff like that. Bubble Tea takes care of it
+for you.
+
+```go
+func (m model) View() string {
+    // The header
+    s := "What should we buy at the market?\n\n"
+
+    // Iterate over our choices
+    for i, choice := range m.choices {
+
+        // Is the cursor pointing at this choice?
+        cursor := " " // no cursor
+        if m.cursor == i {
+            cursor = ">" // cursor!
+        }
+
+        // Is this choice selected?
+        checked := " " // not selected
+        if _, ok := m.selected[i]; ok {
+            checked = "x" // selected!
+        }
+
+        // Render the row
+        s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
+    }
+
+    // The footer
+    s += "\nPress q to quit.\n"
+
+    // Send the UI for rendering
+    return s
+}
+```
+
+### All Together Now
+
+The last step is to simply run our program. We pass our initial model to
+`tea.NewProgram` and let it rip:
+
+```go
+func main() {
+    p := tea.NewProgram(initialModel())
+    if err := p.Start(); err != nil {
+        fmt.Printf("Alas, there's been an error: %v", err)
+        os.Exit(1)
+    }
+}
+```
+
+## What’s Next?
+
+This tutorial covers the basics of building an interactive terminal UI, but
+in the real world you'll also need to perform I/O. To learn about that have a
+look at the [Command Tutorial][cmd]. It's pretty simple.
+
+There are also several [Bubble Tea examples][examples] available and, of course,
+there are [Go Docs][docs].
+
+[cmd]: http://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands/
+[examples]: http://github.com/charmbracelet/bubbletea/tree/master/examples
+[docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc
+
 ## Debugging
 
 ### Debugging with Delve
@@ -65,8 +280,9 @@ actually watch out what address the first `dlv` run tells you to connect to.
 
 ### Logging Stuff
 
-You can log to a debug file to print debug Bubble Tea applications. To do so,
-include something like…
+You can’t really log to stdout with Bubble Tea because your TUI is busy
+occupying that! You can, however, log to a file by including something like
+the following prior to starting your Bubble Tea program:
 
 ```go
 if len(os.Getenv("DEBUG")) > 0 {
@@ -79,8 +295,8 @@ if len(os.Getenv("DEBUG")) > 0 {
 }
 ```
 
-…before you start your Bubble Tea program. To see what’s printed in real time,
-run `tail -f debug.log` while you run your program in another window.
+To see what’s being logged in real time, run `tail -f debug.log` while you run
+your program in another window.
 
 ## Libraries we use with Bubble Tea
 
@@ -106,13 +322,14 @@ For some Bubble Tea programs in production, see:
 * [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
 * [Canard](https://github.com/mrusme/canard): an RSS client
 * [charm](https://github.com/charmbracelet/charm): the official Charm user account manager
+* [chezmoi](https://github.com/twpayne/chezmoi): manage your dotfiles across multiple machines, securely
 * [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in your terminal
 * [clidle](https://github.com/ajeetdsouza/clidle): a Wordle clone for your terminal
 * [container-canary](https://github.com/NVIDIA/container-canary): a container validator
 * [dns53](https://github.com/purpleclay/dns53): dynamic DNS with Amazon Route53. Expose your EC2 quickly, securely and privately
 * [fm](https://github.com/knipferrc/fm): a terminal-based file manager
 * [flapioca](https://github.com/kbrgl/flapioca): Flappy Bird on the CLI!
-* [fztea](https://github.com/jon4hz/fztea): connect to your Flipper's UI over serial or make it accessible via SSH 
+* [fztea](https://github.com/jon4hz/fztea): connect to your Flipper's UI over serial or make it accessible via SSH
 * [fork-cleaner](https://github.com/caarlos0/fork-cleaner): cleans up old and inactive forks in your GitHub account
 * [gambit](https://github.com/maaslalani/gambit): play chess in the terminal
 * [gembro](https://git.sr.ht/~rafael/gembro): a mouse-driven Gemini browser
@@ -121,6 +338,7 @@ For some Bubble Tea programs in production, see:
 * [gitflow-toolkit](https://github.com/mritd/gitflow-toolkit): a GitFlow submission tool
 * [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser and online markdown stash
 * [gocovsh](https://github.com/orlangure/gocovsh): explore Go coverage reports from the CLI
+* [got](https://github.com/fedeztk/got): a simple translator and text-to-speech app build on top of simplytranslate's APIs
 * [httpit](https://github.com/gonetx/httpit): a rapid http(s) benchmark tool
 * [IDNT](https://github.com/r-darwish/idnt): batch software uninstaller
 * [kboard](https://github.com/CamiloGarciaLaRotta/kboard): a typing game
@@ -154,7 +372,6 @@ We'd love to hear your thoughts on this tutorial. Feel free to drop us a note!
 
 * [Twitter](https://twitter.com/charmcli)
 * [The Fediverse](https://mastodon.technology/@charm)
-* [Slack](https://charm.sh/slack)
 
 ## Acknowledgments
 
@@ -171,10 +388,10 @@ of days past.
 
 [MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE)
 
-***
+* * *
 
 Part of [Charm](https://charm.sh).
 
 <a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
 
-Charm热爱开源 • Charm loves open source
+Charm 热爱开源 • Charm loves open source
diff --git a/commands.go b/commands.go
index 9111638..dd3305e 100644
--- a/commands.go
+++ b/commands.go
@@ -119,6 +119,8 @@ func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
 //	}
 //
 //	cmd := Sequentially(saveStateCmd, Quit)
+//
+// Deprecated: use Sequence instead.
 func Sequentially(cmds ...Cmd) Cmd {
 	return func() Msg {
 		for _, cmd := range cmds {
diff --git a/examples/chat/main.go b/examples/chat/main.go
index 99fd0c6..bbf3b3a 100644
--- a/examples/chat/main.go
+++ b/examples/chat/main.go
@@ -22,8 +22,10 @@ func main() {
 	}
 }
 
-type tickMsg struct{}
-type errMsg error
+type (
+	tickMsg struct{}
+	errMsg  error
+)
 
 type model struct {
 	viewport    viewport.Model
diff --git a/examples/composable-views/main.go b/examples/composable-views/main.go
index 9a876c2..2167d53 100644
--- a/examples/composable-views/main.go
+++ b/examples/composable-views/main.go
@@ -120,7 +120,7 @@ func (m mainModel) View() string {
 	var s string
 	model := m.currentFocusedModel()
 	if m.state == timerView {
-		s += lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render( m.spinner.View()))
+		s += lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render(m.spinner.View()))
 	} else {
 		s += lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), focusedModelStyle.Render(m.spinner.View()))
 	}
diff --git a/examples/credit-card-form/main.go b/examples/credit-card-form/main.go
index f4a9293..9128524 100644
--- a/examples/credit-card-form/main.go
+++ b/examples/credit-card-form/main.go
@@ -19,8 +19,10 @@ func main() {
 	}
 }
 
-type tickMsg struct{}
-type errMsg error
+type (
+	tickMsg struct{}
+	errMsg  error
+)
 
 const (
 	ccn = iota
@@ -129,9 +131,7 @@ func (m model) Init() tea.Cmd {
 }
 
 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	var (
-		cmds []tea.Cmd = make([]tea.Cmd, len(m.inputs))
-	)
+	var cmds []tea.Cmd = make([]tea.Cmd, len(m.inputs))
 
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
diff --git a/examples/glamour/main.go b/examples/glamour/main.go
index 1768abb..3b38911 100644
--- a/examples/glamour/main.go
+++ b/examples/glamour/main.go
@@ -11,7 +11,9 @@ import (
 )
 
 const content = `
-Today’s Menu
+# Today’s Menu
+
+## Appetizers
 
 | Name        | Price | Notes                           |
 | ---         | ---   | ---                             |
@@ -20,7 +22,7 @@ Today’s Menu
 | Okonomiyaki | $4    | Takes a few minutes to make     |
 | Curry       | $3    | We can add squash if you’d like |
 
-Seasonal Dishes
+## Seasonal Dishes
 
 | Name                 | Price | Notes              |
 | ---                  | ---   | ---                |
@@ -28,7 +30,7 @@ Seasonal Dishes
 | Takoyaki             | $3    | Fun to eat         |
 | Winter squash        | $3    | Today it's pumpkin |
 
-Desserts
+## Desserts
 
 | Name         | Price | Notes                 |
 | ---          | ---   | ---                   |
@@ -55,13 +57,18 @@ type example struct {
 }
 
 func newExample() (*example, error) {
-	vp := viewport.New(78, 20)
+	const width = 78
+
+	vp := viewport.New(width, 20)
 	vp.Style = lipgloss.NewStyle().
 		BorderStyle(lipgloss.RoundedBorder()).
 		BorderForeground(lipgloss.Color("62")).
 		PaddingRight(2)
 
-	renderer, err := glamour.NewTermRenderer(glamour.WithStylePath("notty"))
+	renderer, err := glamour.NewTermRenderer(
+		glamour.WithAutoStyle(),
+		glamour.WithWordWrap(width),
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -84,10 +91,6 @@ func (e example) Init() tea.Cmd {
 
 func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.WindowSizeMsg:
-		e.viewport.Width = msg.Width
-		return e, nil
-
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "q", "ctrl+c", "esc":
diff --git a/examples/go.mod b/examples/go.mod
index d3601c8..042b22f 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -1,17 +1,17 @@
 module examples
 
-go 1.13
+go 1.16
 
 require (
 	github.com/charmbracelet/bubbles v0.13.1-0.20220815142520-649f78e1fd8b
 	github.com/charmbracelet/bubbletea v0.22.0
 	github.com/charmbracelet/glamour v0.5.0
-	github.com/charmbracelet/lipgloss v0.5.0
+	github.com/charmbracelet/lipgloss v0.6.0
 	github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
 	github.com/lucasb-eyer/go-colorful v1.2.0
 	github.com/mattn/go-isatty v0.0.16
 	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898
+	github.com/muesli/termenv v0.13.0
 )
 
 replace github.com/charmbracelet/bubbletea => ../
diff --git a/examples/go.sum b/examples/go.sum
index ab7a16e..65731b4 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -12,8 +12,9 @@ github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj
 github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
 github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
 github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
-github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
 github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
+github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
+github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -38,8 +39,9 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
 github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
@@ -52,8 +54,8 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt
 github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
 github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 h1:0j+cbZdhLgpNxjg0nWCasHUA82fgWOXxxGgWNVOLS1I=
-github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898/go.mod h1:bN6sPNtkiahdhHv2Xm6RGU16LSCxfbIZvMfqjOCfrR4=
+github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
+github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -80,7 +82,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -90,7 +91,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/result/main.go b/examples/result/main.go
index a73b17d..60d55a3 100644
--- a/examples/result/main.go
+++ b/examples/result/main.go
@@ -46,7 +46,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.cursor = len(choices) - 1
 			}
 		}
-
 	}
 
 	return m, nil
diff --git a/examples/send-msg/main.go b/examples/send-msg/main.go
index 2146387..b1b40a5 100644
--- a/examples/send-msg/main.go
+++ b/examples/send-msg/main.go
@@ -125,8 +125,10 @@ func main() {
 }
 
 func randomFood() string {
-	food := []string{"an apple", "a pear", "a gherkin", "a party gherkin",
+	food := []string{
+		"an apple", "a pear", "a gherkin", "a party gherkin",
 		"a kohlrabi", "some spaghetti", "tacos", "a currywurst", "some curry",
-		"a sandwich", "some peanut butter", "some cashews", "some ramen"}
+		"a sandwich", "some peanut butter", "some cashews", "some ramen",
+	}
 	return string(food[rand.Intn(len(food))])
 }
diff --git a/examples/sequence/main.go b/examples/sequence/main.go
new file mode 100644
index 0000000..79f7e23
--- /dev/null
+++ b/examples/sequence/main.go
@@ -0,0 +1,40 @@
+package main
+
+// A simple example illustrating how to run a series of commands in order.
+
+import (
+	"fmt"
+	"os"
+
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+type model struct{}
+
+func (m model) Init() tea.Cmd {
+	return tea.Sequence(
+		tea.Println("A"),
+		tea.Println("B"),
+		tea.Println("C"),
+		tea.Quit,
+	)
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg.(type) {
+	case tea.KeyMsg:
+		return m, tea.Quit
+	}
+	return m, nil
+}
+
+func (m model) View() string {
+	return ""
+}
+
+func main() {
+	if err := tea.NewProgram(model{}).Start(); err != nil {
+		fmt.Println("Uh oh:", err)
+		os.Exit(1)
+	}
+}
diff --git a/examples/spinner/main.go b/examples/spinner/main.go
index 229542d..c1095f6 100644
--- a/examples/spinner/main.go
+++ b/examples/spinner/main.go
@@ -52,7 +52,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.spinner, cmd = m.spinner.Update(msg)
 		return m, cmd
 	}
-
 }
 
 func (m model) View() string {
diff --git a/examples/textinput/main.go b/examples/textinput/main.go
index eac1c8a..af90208 100644
--- a/examples/textinput/main.go
+++ b/examples/textinput/main.go
@@ -19,8 +19,10 @@ func main() {
 	}
 }
 
-type tickMsg struct{}
-type errMsg error
+type (
+	tickMsg struct{}
+	errMsg  error
+)
 
 type model struct {
 	textInput textinput.Model
diff --git a/examples/textinputs/main.go b/examples/textinputs/main.go
index 1bd5594..587b8df 100644
--- a/examples/textinputs/main.go
+++ b/examples/textinputs/main.go
@@ -62,6 +62,7 @@ func initialModel() model {
 
 	return m
 }
+
 func (m model) Init() tea.Cmd {
 	return textinput.Blink
 }
@@ -134,7 +135,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
-	var cmds = make([]tea.Cmd, len(m.inputs))
+	cmds := make([]tea.Cmd, len(m.inputs))
 
 	// Only text inputs with Focus() set will respond, so it's safe to simply
 	// update all of them here without any further logic.
diff --git a/examples/views/main.go b/examples/views/main.go
index 1a934be..f27d699 100644
--- a/examples/views/main.go
+++ b/examples/views/main.go
@@ -46,8 +46,10 @@ func main() {
 	}
 }
 
-type tickMsg struct{}
-type frameMsg struct{}
+type (
+	tickMsg  struct{}
+	frameMsg struct{}
+)
 
 func tick() tea.Cmd {
 	return tea.Tick(time.Second, func(time.Time) tea.Msg {
diff --git a/exec.go b/exec.go
index 587d91a..53bf5d4 100644
--- a/exec.go
+++ b/exec.go
@@ -39,7 +39,7 @@ func Exec(c ExecCommand, fn ExecCallback) Cmd {
 //	c := exec.Command("vim", "file.txt")
 //
 //	cmd := ExecProcess(c, func(err error) Msg {
-//	    return VimFinishedMsg{err: error}
+//	    return VimFinishedMsg{err: err}
 //	})
 //
 // Or, if you don't care about errors, you could simply:
diff --git a/go.mod b/go.mod
index 628391f..d3a0c97 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/charmbracelet/bubbletea
 
-go 1.13
+go 1.16
 
 require (
 	github.com/containerd/console v1.0.3
@@ -9,8 +9,7 @@ require (
 	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
 	github.com/muesli/cancelreader v0.2.2
 	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
-	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
+	github.com/muesli/termenv v0.13.0
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 	golang.org/x/text v0.3.7 // indirect
 )
diff --git a/go.sum b/go.sum
index 270f8f5..76e0059 100644
--- a/go.sum
+++ b/go.sum
@@ -1,29 +1,30 @@
+github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
+github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
+github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/key.go b/key.go
index 07bc651..14dc10f 100644
--- a/key.go
+++ b/key.go
@@ -199,6 +199,7 @@ const (
 	KeyPgUp
 	KeyPgDown
 	KeyDelete
+	KeyInsert
 	KeySpace
 	KeyCtrlUp
 	KeyCtrlDown
@@ -284,6 +285,7 @@ var keyNames = map[KeyType]string{
 	KeyPgUp:           "pgup",
 	KeyPgDown:         "pgdown",
 	KeyDelete:         "delete",
+	KeyInsert:         "insert",
 	KeyCtrlUp:         "ctrl+up",
 	KeyCtrlDown:       "ctrl+down",
 	KeyCtrlRight:      "ctrl+right",
@@ -376,6 +378,8 @@ var sequences = map[string]Key{
 
 	// Miscellaneous keys
 	"\x1b[Z":      {Type: KeyShiftTab},
+	"\x1b[2~":     {Type: KeyInsert},
+	"\x1b[2;3~":   {Type: KeyInsert, Alt: true},
 	"\x1b[3~":     {Type: KeyDelete},
 	"\x1b[3;3~":   {Type: KeyDelete, Alt: true},
 	"\x1b[1~":     {Type: KeyHome},
@@ -388,12 +392,20 @@ var sequences = map[string]Key{
 	"\x1b[6;3~":   {Type: KeyPgDown, Alt: true},
 	"\x1b[7~":     {Type: KeyHome},              // urxvt
 	"\x1b[8~":     {Type: KeyEnd},               // urxvt
+	"\x1b\x1b[2~": {Type: KeyInsert, Alt: true}, // urxvt
 	"\x1b\x1b[3~": {Type: KeyDelete, Alt: true}, // urxvt
 	"\x1b\x1b[5~": {Type: KeyPgUp, Alt: true},   // urxvt
 	"\x1b\x1b[6~": {Type: KeyPgDown, Alt: true}, // urxvt
 	"\x1b\x1b[7~": {Type: KeyHome, Alt: true},   // urxvt
 	"\x1b\x1b[8~": {Type: KeyEnd, Alt: true},    // urxvt
 
+	// Function keys, Linux console
+	"\x1b[[A": {Type: KeyF1}, // linux console
+	"\x1b[[B": {Type: KeyF2}, // linux console
+	"\x1b[[C": {Type: KeyF3}, // linux console
+	"\x1b[[D": {Type: KeyF4}, // linux console
+	"\x1b[[E": {Type: KeyF5}, // linux console
+
 	// Function keys, X11
 	"\x1bOP":     {Type: KeyF1},  // vt100
 	"\x1bOQ":     {Type: KeyF2},  // vt100
@@ -543,28 +555,29 @@ func readInputs(input io.Reader) ([]Msg, error) {
 
 		// Is the alt key pressed? If so, the buffer will be prefixed with an
 		// escape.
+		alt := false
 		if len(runes) > 1 && runes[0] == 0x1b {
-			msgs = append(msgs, KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: runes[1:]}))
-			continue
+			alt = true
+			runes = runes[1:]
 		}
 
 		for _, v := range runes {
 			// Is the first rune a control character?
 			r := KeyType(v)
 			if r <= keyUS || r == keyDEL {
-				msgs = append(msgs, KeyMsg(Key{Type: r}))
+				msgs = append(msgs, KeyMsg(Key{Type: r, Alt: alt}))
 				continue
 			}
 
 			// If it's a space, override the type with KeySpace (but still include
 			// the rune).
 			if r == ' ' {
-				msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}}))
+				msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}, Alt: alt}))
 				continue
 			}
 
 			// Welp, just regular, ol' runes.
-			msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}}))
+			msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}, Alt: alt}))
 		}
 	}
 
diff --git a/logging.go b/logging.go
index 54a4350..97ffcb7 100644
--- a/logging.go
+++ b/logging.go
@@ -19,7 +19,7 @@ import (
 //	  }
 //	  defer f.Close()
 func LogToFile(path string, prefix string) (*os.File, error) {
-	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
 	if err != nil {
 		return nil, err
 	}
diff --git a/options.go b/options.go
index b60d23c..6ede540 100644
--- a/options.go
+++ b/options.go
@@ -1,6 +1,10 @@
 package tea
 
-import "io"
+import (
+	"io"
+
+	"github.com/muesli/termenv"
+)
 
 // ProgramOption is used to set options when initializing a Program. Program can
 // accept a variable number of options.
@@ -13,17 +17,17 @@ type ProgramOption func(*Program)
 // WithOutput sets the output which, by default, is stdout. In most cases you
 // won't need to use this.
 func WithOutput(output io.Writer) ProgramOption {
-	return func(m *Program) {
-		m.output = output
+	return func(p *Program) {
+		p.output = termenv.NewOutput(output, termenv.WithColorCache(true))
 	}
 }
 
 // WithInput sets the input which, by default, is stdin. In most cases you
 // won't need to use this.
 func WithInput(input io.Reader) ProgramOption {
-	return func(m *Program) {
-		m.input = input
-		m.startupOptions |= withCustomInput
+	return func(p *Program) {
+		p.input = input
+		p.startupOptions |= withCustomInput
 	}
 }
 
@@ -39,8 +43,8 @@ func WithInputTTY() ProgramOption {
 // unusable state after a panic because Bubble Tea will not perform its usual
 // cleanup on exit.
 func WithoutCatchPanics() ProgramOption {
-	return func(m *Program) {
-		m.CatchPanics = false
+	return func(p *Program) {
+		p.CatchPanics = false
 	}
 }
 
@@ -114,8 +118,8 @@ func WithMouseAllMotion() ProgramOption {
 // programs. For example, your program could behave like a daemon if output is
 // not a TTY.
 func WithoutRenderer() ProgramOption {
-	return func(m *Program) {
-		m.renderer = &nilRenderer{}
+	return func(p *Program) {
+		p.renderer = &nilRenderer{}
 	}
 }
 
diff --git a/screen.go b/screen.go
deleted file mode 100644
index 63db7ae..0000000
--- a/screen.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package tea
-
-import (
-	"fmt"
-	"io"
-
-	te "github.com/muesli/termenv"
-)
-
-func hideCursor(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.HideCursorSeq)
-}
-
-func showCursor(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.ShowCursorSeq)
-}
-
-func clearLine(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.EraseLineSeq, 2)
-}
-
-func cursorUp(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.CursorUpSeq, 1)
-}
-
-func cursorDown(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.CursorDownSeq, 1)
-}
-
-func insertLine(w io.Writer, numLines int) {
-	fmt.Fprintf(w, te.CSI+"%dL", numLines)
-}
-
-func moveCursor(w io.Writer, row, col int) {
-	fmt.Fprintf(w, te.CSI+te.CursorPositionSeq, row, col)
-}
-
-func changeScrollingRegion(w io.Writer, top, bottom int) {
-	fmt.Fprintf(w, te.CSI+te.ChangeScrollingRegionSeq, top, bottom)
-}
-
-func cursorBack(w io.Writer, n int) {
-	fmt.Fprintf(w, te.CSI+te.CursorBackSeq, n)
-}
-
-func enterAltScreen(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.AltScreenSeq)
-	moveCursor(w, 0, 0)
-}
-
-func exitAltScreen(w io.Writer) {
-	fmt.Fprintf(w, te.CSI+te.ExitAltScreenSeq)
-}
diff --git a/signals_windows.go b/signals_windows.go
index 71da4f0..c60ca57 100644
--- a/signals_windows.go
+++ b/signals_windows.go
@@ -11,6 +11,7 @@ import (
 // listenForResize is not available on windows because windows does not
 // implement syscall.SIGWINCH.
 func listenForResize(ctx context.Context, output *os.File, msgs chan Msg,
-	errs chan error, done chan struct{}) {
+	errs chan error, done chan struct{},
+) {
 	close(done)
 }
diff --git a/standard_renderer.go b/standard_renderer.go
index d2bc387..6c1c50b 100644
--- a/standard_renderer.go
+++ b/standard_renderer.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/muesli/ansi/compressor"
 	"github.com/muesli/reflow/truncate"
+	"github.com/muesli/termenv"
 )
 
 const (
@@ -24,7 +25,7 @@ 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                io.Writer
+	out                *termenv.Output
 	buf                bytes.Buffer
 	queuedMessageLines []string
 	framerate          time.Duration
@@ -49,7 +50,7 @@ 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 io.Writer, mtx *sync.Mutex, useANSICompressor bool) renderer {
+func newRenderer(out *termenv.Output, mtx *sync.Mutex, useANSICompressor bool) renderer {
 	r := &standardRenderer{
 		out:                out,
 		mtx:                mtx,
@@ -58,7 +59,7 @@ func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) rendere
 		queuedMessageLines: []string{},
 	}
 	if r.useANSICompressor {
-		r.out = &compressor.Writer{Forward: out}
+		r.out = termenv.NewOutput(&compressor.Writer{Forward: out})
 	}
 	return r
 }
@@ -75,13 +76,13 @@ func (r *standardRenderer) start() {
 // stop permanently halts the renderer, rendering the final frame.
 func (r *standardRenderer) stop() {
 	r.flush()
-	clearLine(r.out)
+	r.out.ClearLine()
 	r.once.Do(func() {
 		close(r.done)
 	})
 
 	if r.useANSICompressor {
-		if w, ok := r.out.(io.WriteCloser); ok {
+		if w, ok := r.out.TTY().(io.WriteCloser); ok {
 			_ = w.Close()
 		}
 	}
@@ -89,7 +90,7 @@ func (r *standardRenderer) stop() {
 
 // kill halts the renderer. The final frame will not be rendered.
 func (r *standardRenderer) kill() {
-	clearLine(r.out)
+	r.out.ClearLine()
 	r.once.Do(func() {
 		close(r.done)
 	})
@@ -122,7 +123,8 @@ func (r *standardRenderer) flush() {
 	}
 
 	// Output buffer
-	out := new(bytes.Buffer)
+	buf := &bytes.Buffer{}
+	out := termenv.NewOutput(buf)
 
 	newLines := strings.Split(r.buf.String(), "\n")
 	numLinesThisFlush := len(newLines)
@@ -145,10 +147,10 @@ func (r *standardRenderer) flush() {
 			if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) {
 				skipLines[i] = struct{}{}
 			} else if _, exists := r.ignoreLines[i]; !exists {
-				clearLine(out)
+				out.ClearLine()
 			}
 
-			cursorUp(out)
+			out.CursorUp(1)
 		}
 
 		if _, exists := r.ignoreLines[0]; !exists {
@@ -161,8 +163,8 @@ func (r *standardRenderer) flush() {
 			// standard (whereas others are proprietary to, say, VT100/VT52).
 			// If cursor previous line (ESC[ + <n> + F) were better supported
 			// we could use that above to eliminate this step.
-			cursorBack(out, r.width)
-			clearLine(out)
+			out.CursorBack(r.width)
+			out.ClearLine()
 		}
 	}
 
@@ -179,7 +181,7 @@ func (r *standardRenderer) flush() {
 		if _, skip := skipLines[i]; skip {
 			// Unless this is the last line, move the cursor down.
 			if i < len(newLines)-1 {
-				cursorDown(out)
+				out.CursorDown(1)
 			}
 		} else {
 			line := newLines[i]
@@ -195,10 +197,10 @@ func (r *standardRenderer) flush() {
 				line = truncate.String(line, uint(r.width))
 			}
 
-			_, _ = io.WriteString(out, line)
+			_, _ = out.WriteString(line)
 
 			if i < len(newLines)-1 {
-				_, _ = io.WriteString(out, "\r\n")
+				_, _ = out.WriteString("\r\n")
 			}
 		}
 	}
@@ -210,12 +212,12 @@ func (r *standardRenderer) flush() {
 		// This case fixes a bug in macOS terminal. In other terminals the
 		// other case seems to do the job regardless of whether or not we're
 		// using the full terminal window.
-		moveCursor(out, r.linesRendered, 0)
+		out.MoveCursor(r.linesRendered, 0)
 	} else {
-		cursorBack(out, r.width)
+		out.CursorBack(r.width)
 	}
 
-	_, _ = r.out.Write(out.Bytes())
+	_, _ = r.out.Write(buf.Bytes())
 	r.lastRender = r.buf.String()
 	r.buf.Reset()
 }
@@ -270,15 +272,17 @@ func (r *standardRenderer) setIgnoredLines(from int, to int) {
 
 	// Erase ignored lines
 	if r.linesRendered > 0 {
-		out := new(bytes.Buffer)
+		buf := &bytes.Buffer{}
+		out := termenv.NewOutput(buf)
+
 		for i := r.linesRendered - 1; i >= 0; i-- {
 			if _, exists := r.ignoreLines[i]; exists {
-				clearLine(out)
+				out.ClearLine()
 			}
-			cursorUp(out)
+			out.CursorUp(1)
 		}
-		moveCursor(out, r.linesRendered, 0) // put cursor back
-		_, _ = r.out.Write(out.Bytes())
+		out.MoveCursor(r.linesRendered, 0) // put cursor back
+		_, _ = r.out.Write(buf.Bytes())
 	}
 }
 
@@ -311,18 +315,19 @@ func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary
 	r.mtx.Lock()
 	defer r.mtx.Unlock()
 
-	b := new(bytes.Buffer)
+	buf := &bytes.Buffer{}
+	out := termenv.NewOutput(buf)
 
-	changeScrollingRegion(b, topBoundary, bottomBoundary)
-	moveCursor(b, topBoundary, 0)
-	insertLine(b, len(lines))
-	_, _ = io.WriteString(b, strings.Join(lines, "\r\n"))
-	changeScrollingRegion(b, 0, r.height)
+	out.ChangeScrollingRegion(topBoundary, bottomBoundary)
+	out.MoveCursor(topBoundary, 0)
+	out.InsertLines(len(lines))
+	_, _ = out.WriteString(strings.Join(lines, "\r\n"))
+	out.ChangeScrollingRegion(0, r.height)
 
 	// Move cursor back to where the main rendering routine expects it to be
-	moveCursor(b, r.linesRendered, 0)
+	out.MoveCursor(r.linesRendered, 0)
 
-	_, _ = r.out.Write(b.Bytes())
+	_, _ = r.out.Write(buf.Bytes())
 }
 
 // insertBottom effectively scrolls down. It inserts lines at the bottom of
@@ -338,17 +343,18 @@ func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBound
 	r.mtx.Lock()
 	defer r.mtx.Unlock()
 
-	b := new(bytes.Buffer)
+	buf := &bytes.Buffer{}
+	out := termenv.NewOutput(buf)
 
-	changeScrollingRegion(b, topBoundary, bottomBoundary)
-	moveCursor(b, bottomBoundary, 0)
-	_, _ = io.WriteString(b, "\r\n"+strings.Join(lines, "\r\n"))
-	changeScrollingRegion(b, 0, r.height)
+	out.ChangeScrollingRegion(topBoundary, bottomBoundary)
+	out.MoveCursor(bottomBoundary, 0)
+	_, _ = out.WriteString("\r\n" + strings.Join(lines, "\r\n"))
+	out.ChangeScrollingRegion(0, r.height)
 
 	// Move cursor back to where the main rendering routine expects it to be
-	moveCursor(b, r.linesRendered, 0)
+	out.MoveCursor(r.linesRendered, 0)
 
-	_, _ = r.out.Write(b.Bytes())
+	_, _ = r.out.Write(buf.Bytes())
 }
 
 // handleMessages handles internal messages for the renderer.
diff --git a/tea.go b/tea.go
index 446a2bc..23d8cb7 100644
--- a/tea.go
+++ b/tea.go
@@ -23,7 +23,7 @@ import (
 	"github.com/containerd/console"
 	isatty "github.com/mattn/go-isatty"
 	"github.com/muesli/cancelreader"
-	te "github.com/muesli/termenv"
+	"github.com/muesli/termenv"
 	"golang.org/x/term"
 )
 
@@ -89,9 +89,10 @@ type Program struct {
 	errs         chan error
 	readLoopDone chan struct{}
 
-	output       io.Writer // where to send output. this will usually be os.Stdout.
-	input        io.Reader // this will usually be os.Stdin.
-	cancelReader cancelreader.CancelReader
+	output        *termenv.Output // where to send output. this will usually be os.Stdout.
+	restoreOutput func() error
+	input         io.Reader // this will usually be os.Stdin.
+	cancelReader  cancelreader.CancelReader
 
 	renderer           renderer
 	altScreenActive    bool
@@ -253,7 +254,6 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
 	p := &Program{
 		mtx:          &sync.Mutex{},
 		initialModel: model,
-		output:       os.Stdout,
 		input:        os.Stdin,
 		msgs:         make(chan Msg),
 		CatchPanics:  true,
@@ -265,6 +265,16 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
 		opt(p)
 	}
 
+	// if no output was set, set it to stdout
+	if p.output == nil {
+		p.output = termenv.DefaultOutput()
+
+		// cache detected color values
+		termenv.WithColorCache(true)(p.output)
+	}
+
+	p.restoreOutput, _ = termenv.EnableVirtualTerminalProcessing(p.output)
+
 	return p
 }
 
@@ -429,7 +439,7 @@ func (p *Program) StartReturningModel() (Model, error) {
 	}
 	defer p.cancelReader.Close() // nolint:errcheck
 
-	if f, ok := p.output.(*os.File); ok && isatty.IsTerminal(f.Fd()) {
+	if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) {
 		// Get the initial terminal size and send it to the program.
 		go func() {
 			w, h, err := term.GetSize(int(f.Fd()))
@@ -527,11 +537,22 @@ func (p *Program) StartReturningModel() (Model, error) {
 				p.DisableMouseAllMotion()
 
 			case hideCursorMsg:
-				hideCursor(p.output)
+				p.output.HideCursor()
 
 			case execMsg:
 				// NB: this blocks.
 				p.exec(msg.cmd, msg.fn)
+
+			case sequenceMsg:
+				go func() {
+					// Execute commands one at a time, in order.
+					for _, cmd := range msg {
+						select {
+						case p.msgs <- cmd():
+						case <-p.ctx.Done():
+						}
+					}
+				}()
 			}
 
 			// Process internal messages for the renderer.
@@ -595,6 +616,10 @@ func (p *Program) shutdown(kill bool) {
 	p.DisableMouseCellMotion()
 	p.DisableMouseAllMotion()
 	_ = p.restoreTerminalState()
+
+	if p.restoreOutput != nil {
+		_ = p.restoreOutput()
+	}
 }
 
 // EnterAltScreen enters the alternate screen buffer, which consumes the entire
@@ -609,7 +634,8 @@ func (p *Program) EnterAltScreen() {
 		return
 	}
 
-	enterAltScreen(p.output)
+	p.output.AltScreen()
+	p.output.MoveCursor(1, 1)
 
 	p.altScreenActive = true
 	if p.renderer != nil {
@@ -628,7 +654,7 @@ func (p *Program) ExitAltScreen() {
 		return
 	}
 
-	exitAltScreen(p.output)
+	p.output.ExitAltScreen()
 
 	p.altScreenActive = false
 	if p.renderer != nil {
@@ -643,7 +669,8 @@ func (p *Program) ExitAltScreen() {
 func (p *Program) EnableMouseCellMotion() {
 	p.mtx.Lock()
 	defer p.mtx.Unlock()
-	fmt.Fprintf(p.output, te.CSI+te.EnableMouseCellMotionSeq)
+
+	p.output.EnableMouseCellMotion()
 }
 
 // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
@@ -653,7 +680,8 @@ func (p *Program) EnableMouseCellMotion() {
 func (p *Program) DisableMouseCellMotion() {
 	p.mtx.Lock()
 	defer p.mtx.Unlock()
-	fmt.Fprintf(p.output, te.CSI+te.DisableMouseCellMotionSeq)
+
+	p.output.DisableMouseCellMotion()
 }
 
 // EnableMouseAllMotion enables mouse click, release, wheel and motion events,
@@ -664,7 +692,8 @@ func (p *Program) DisableMouseCellMotion() {
 func (p *Program) EnableMouseAllMotion() {
 	p.mtx.Lock()
 	defer p.mtx.Unlock()
-	fmt.Fprintf(p.output, te.CSI+te.EnableMouseAllMotionSeq)
+
+	p.output.EnableMouseAllMotion()
 }
 
 // DisableMouseAllMotion disables All Motion mouse tracking. This will be
@@ -674,7 +703,8 @@ func (p *Program) EnableMouseAllMotion() {
 func (p *Program) DisableMouseAllMotion() {
 	p.mtx.Lock()
 	defer p.mtx.Unlock()
-	fmt.Fprintf(p.output, te.CSI+te.DisableMouseAllMotionSeq)
+
+	p.output.DisableMouseAllMotion()
 }
 
 // ReleaseTerminal restores the original terminal state and cancels the input
@@ -736,3 +766,14 @@ func (p *Program) Printf(template string, args ...interface{}) {
 		messageBody: fmt.Sprintf(template, args...),
 	}
 }
+
+// sequenceMsg is used interally to run the the given commands in order.
+type sequenceMsg []Cmd
+
+// Sequence runs the given commands one at a time, in order. Contrast this with
+// Batch, which runs commands concurrently.
+func Sequence(cmds ...Cmd) Cmd {
+	return func() Msg {
+		return sequenceMsg(cmds)
+	}
+}
diff --git a/tty.go b/tty.go
index 17e508b..e4f75cf 100644
--- a/tty.go
+++ b/tty.go
@@ -20,14 +20,14 @@ func (p *Program) initTerminal() error {
 		}
 	}
 
-	hideCursor(p.output)
+	p.output.HideCursor()
 	return nil
 }
 
 // restoreTerminalState restores the terminal to the state prior to running the
 // Bubble Tea program.
 func (p Program) restoreTerminalState() error {
-	showCursor(p.output)
+	p.output.ShowCursor()
 
 	if p.console != nil {
 		err := p.console.Reset()
diff --git a/tty_windows.go b/tty_windows.go
index 9cfc7d9..be415ae 100644
--- a/tty_windows.go
+++ b/tty_windows.go
@@ -4,11 +4,9 @@
 package tea
 
 import (
-	"io"
 	"os"
 
 	"github.com/containerd/console"
-	"golang.org/x/sys/windows"
 )
 
 func (p *Program) initInput() error {
@@ -26,8 +24,6 @@ func (p *Program) initInput() error {
 		p.console = c
 	}
 
-	enableAnsiColors(p.output)
-
 	return nil
 }
 
@@ -49,18 +45,3 @@ func openInputTTY() (*os.File, error) {
 	}
 	return f, nil
 }
-
-// enableAnsiColors enables support for ANSI color sequences in Windows
-// default console. Note that this only works with Windows 10.
-func enableAnsiColors(w io.Writer) {
-	f, ok := w.(*os.File)
-	if !ok {
-		return
-	}
-
-	stdout := windows.Handle(f.Fd())
-	var originalMode uint32
-
-	_ = windows.GetConsoleMode(stdout, &originalMode)
-	_ = windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-}
diff --git a/tutorials/basics/README.md b/tutorials/basics/README.md
index 3fcfa66..adec6aa 100644
--- a/tutorials/basics/README.md
+++ b/tutorials/basics/README.md
@@ -2,15 +2,16 @@ Bubble Tea Basics
 =================
 
 Bubble Tea is based on the functional design paradigms of [The Elm
-Architecture][elm], which happen to work nicely with Go. It's a delightful way to
-build applications.
-
-By the way, the non-annotated source code for this program is available
-[on GitHub](https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics).
+Architecture][elm], which happens to work nicely with Go. It's a delightful way
+to build applications.
 
 This tutorial assumes you have a working knowledge of Go.
 
+By the way, the non-annotated source code for this program is available
+[on GitHub][tut-source].
+
 [elm]: https://guide.elm-lang.org/architecture/
+[tut-source]:https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
 
 ## Enough! Let's get to it.
 
@@ -59,7 +60,7 @@ the initial model as a variable elsewhere, too.
 ```go
 func initialModel() model {
 	return model{
-		// Our shopping list is a grocery list
+		// Our to-do list is a grocery list
 		choices:  []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
 
 		// A map which indicates which choices are selected. We're using
diff --git a/tutorials/commands/README.md b/tutorials/commands/README.md
index a1275f1..2492058 100644
--- a/tutorials/commands/README.md
+++ b/tutorials/commands/README.md
@@ -2,7 +2,7 @@ Commands in Bubble Tea
 ======================
 
 This is the second tutorial for Bubble Tea covering commands, which deal with
-I/O. The tutorial assumes you have a working knowlege of Go and a decent
+I/O. The tutorial assumes you have a working knowledge of Go and a decent
 understanding of [the first tutorial][basics].
 
 You can find the non-annotated version of this program [on GitHub][source].
@@ -50,7 +50,7 @@ type model struct {
 `Cmd`s are functions that perform some I/O and then return a `Msg`. Checking the
 time, ticking a timer, reading from the disk, and network stuff are all I/O and
 should be run through commands. That might sound harsh, but it will keep your
-Bubble Tea program staightforward and simple.
+Bubble Tea program straightforward and simple.
 
 Anyway, let's write a `Cmd` that makes a request to a server and returns the
 result as a `Msg`.
@@ -87,7 +87,7 @@ First, let's write our initialization function.
 
 ## The Initialization Method
 
-The initilization method is very simple: we return the `Cmd` we made earlier.
+The initialization method is very simple: we return the `Cmd` we made earlier.
 Note that we don't call the function; the Bubble Tea runtime will do that when
 the time is right.
 
diff --git a/tutorials/go.mod b/tutorials/go.mod
index 6d00a45..9f0713f 100644
--- a/tutorials/go.mod
+++ b/tutorials/go.mod
@@ -1,6 +1,6 @@
 module tutorial
 
-go 1.14
+go 1.16
 
 require github.com/charmbracelet/bubbletea v0.21.0
 
diff --git a/tutorials/go.sum b/tutorials/go.sum
index 270f8f5..76e0059 100644
--- a/tutorials/go.sum
+++ b/tutorials/go.sum
@@ -1,29 +1,30 @@
+github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
+github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
+github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=


Test patch:
diff --git a/commands_test.go b/commands_test.go
index e0d5ff3..9caf18e 100644
--- a/commands_test.go
+++ b/commands_test.go
@@ -27,10 +27,10 @@ func TestTick(t *testing.T) {
 }
 
 func TestSequentially(t *testing.T) {
-	var expectedErrMsg = fmt.Errorf("some err")
-	var expectedStrMsg = "some msg"
+	expectedErrMsg := fmt.Errorf("some err")
+	expectedStrMsg := "some msg"
 
-	var nilReturnCmd = func() Msg {
+	nilReturnCmd := func() Msg {
 		return nil
 	}
 
diff --git a/key_test.go b/key_test.go
index b74f06b..869b32f 100644
--- a/key_test.go
+++ b/key_test.go
@@ -134,6 +134,24 @@ func TestReadInput(t *testing.T) {
 				},
 			},
 		},
+		"alt+enter": {
+			[]byte{'\x1b', '\r'},
+			[]Msg{
+				KeyMsg{
+					Type: KeyEnter,
+					Alt:  true,
+				},
+			},
+		},
+		"alt+ctrl+a": {
+			[]byte{'\x1b', byte(keySOH)},
+			[]Msg{
+				KeyMsg{
+					Type: KeyCtrlA,
+					Alt:  true,
+				},
+			},
+		},
 	} {
 		t.Run(out, func(t *testing.T) {
 			msgs, err := readInputs(bytes.NewReader(td.in))
diff --git a/options_test.go b/options_test.go
index 1b77977..6648ddd 100644
--- a/options_test.go
+++ b/options_test.go
@@ -9,8 +9,8 @@ func TestOptions(t *testing.T) {
 	t.Run("output", func(t *testing.T) {
 		var b bytes.Buffer
 		p := NewProgram(nil, WithOutput(&b))
-		if p.output != &b {
-			t.Errorf("expected output to custom, got %v", p.output)
+		if p.output.TTY() != nil {
+			t.Errorf("expected output to custom, got %v", p.output.TTY().Fd())
 		}
 	})
 
diff --git a/screen_test.go b/screen_test.go
deleted file mode 100644
index 7bfdf2e..0000000
--- a/screen_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package tea
-
-import (
-	"bytes"
-	"io"
-	"testing"
-)
-
-func TestScreen(t *testing.T) {
-	exercise := func(t *testing.T, fn func(io.Writer), expect []byte) {
-		var w bytes.Buffer
-		fn(&w)
-		if !bytes.Equal(w.Bytes(), expect) {
-			t.Errorf("expected %q, got %q", expect, w.Bytes())
-		}
-	}
-
-	t.Run("change scrolling region", func(t *testing.T) {
-		exercise(t, func(w io.Writer) {
-			changeScrollingRegion(w, 16, 22)
-		}, []byte("\x1b[16;22r"))
-	})
-
-	t.Run("line", func(t *testing.T) {
-		t.Run("clear", func(t *testing.T) {
-			exercise(t, clearLine, []byte("\x1b[2K"))
-		})
-
-		t.Run("insert", func(t *testing.T) {
-			exercise(t, func(w io.Writer) {
-				insertLine(w, 12)
-			}, []byte("\x1b[12L"))
-		})
-	})
-
-	t.Run("cursor", func(t *testing.T) {
-		t.Run("hide", func(t *testing.T) {
-			exercise(t, hideCursor, []byte("\x1b[?25l"))
-		})
-
-		t.Run("show", func(t *testing.T) {
-			exercise(t, showCursor, []byte("\x1b[?25h"))
-		})
-
-		t.Run("up", func(t *testing.T) {
-			exercise(t, cursorUp, []byte("\x1b[1A"))
-		})
-
-		t.Run("down", func(t *testing.T) {
-			exercise(t, cursorDown, []byte("\x1b[1B"))
-		})
-
-		t.Run("move", func(t *testing.T) {
-			exercise(t, func(w io.Writer) {
-				moveCursor(w, 10, 20)
-			}, []byte("\x1b[10;20H"))
-		})
-
-		t.Run("back", func(t *testing.T) {
-			exercise(t, func(w io.Writer) {
-				cursorBack(w, 15)
-			}, []byte("\x1b[15D"))
-		})
-	})
-
-}
diff --git a/tea_test.go b/tea_test.go
new file mode 100644
index 0000000..3c35074
--- /dev/null
+++ b/tea_test.go
@@ -0,0 +1,39 @@
+package tea
+
+import (
+	"bytes"
+	"testing"
+)
+
+type testModel struct{}
+
+func (m testModel) Init() Cmd {
+	return nil
+}
+
+func (m testModel) Update(msg Msg) (Model, Cmd) {
+	switch msg.(type) {
+	case KeyMsg:
+		return m, Quit
+	}
+	return m, nil
+}
+
+func (m testModel) View() string {
+	return "success\n"
+}
+
+func TestTeaModel(t *testing.T) {
+	var buf bytes.Buffer
+	var in bytes.Buffer
+	in.Write([]byte("q"))
+
+	p := NewProgram(testModel{}, WithInput(&in), WithOutput(&buf))
+	if err := p.Start(); err != nil {
+		t.Fatal(err)
+	}
+
+	if buf.Len() == 0 {
+		t.Fatal("no output")
+	}
+}