Repository: charmbracelet/bubbletea
Pull request #419 (2022-08-25 16:21:59)

docs: added got to Bubble Tea in the Wild


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..4e68c8b 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -1,12 +1,12 @@
 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
diff --git a/examples/go.sum b/examples/go.sum
index ab7a16e..8b5d157 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=
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..fedb6b0 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
diff --git a/key.go b/key.go
index 07bc651..135571b 100644
--- a/key.go
+++ b/key.go
@@ -394,6 +394,13 @@ var sequences = map[string]Key{
 	"\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 +550,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/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/tea.go b/tea.go
index 446a2bc..547e011 100644
--- a/tea.go
+++ b/tea.go
@@ -532,6 +532,17 @@ func (p *Program) StartReturningModel() (Model, error) {
 			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.
@@ -736,3 +747,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/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/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
 


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/screen_test.go b/screen_test.go
index 7bfdf2e..93cb50d 100644
--- a/screen_test.go
+++ b/screen_test.go
@@ -62,5 +62,4 @@ func TestScreen(t *testing.T) {
 			}, []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")
+	}
+}