Technical Documentation: charmbracelet/bubbletea
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Bubble Tea
[](https://github.com/charmbracelet/bubbletea/releases)
[](https://pkg.go.dev/charm.land/bubbletea/v2?tab=doc)
[](https://github.com/charmbracelet/bubbletea/actions)The fun, functional and stateful way to build terminal apps. A Go framework
based on The Elm Architecture. Bubble Tea is well-suited for simple and
complex terminal applications, either inline, full-window, or a mix of both.
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 high-performance cell-based renderer, built-in color downsampling,
declarative views, high-fidelity keyboard and mouse handling, native clipboard
support, and more.
To get started, see the tutorial below, the examples, the
docs, and some common resources.
[!TIP]
Upgrading from v1? Check out the upgrade guide, or
point your LLM at it and let it go to town.
By the way
Be sure to check out Bubbles, a library of common UI components for Bubble Tea.
[](https://github.com/charmbracelet/bubbles)
[](https://github.com/charmbracelet/bubbles)Tutorial
Bubble Tea is based on the functional design paradigms of The Elm
Architecture, 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.
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.
package main
// These imports will be used later in the tutorial. If you save the file
// now, Go might complain they are unused, but that's fine.
// You may also need to run `go mod tidy` to download bubbletea and its
// dependencies.
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)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.
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. 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.”
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{}),
}
}After that, we’ll define our application’s initial state in 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."
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.KeyPressMsg messages, which are
automatically sent to the update function when keys are pressed.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Is it a key press?
case tea.KeyPressMsg:
// 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 space bar toggle the selected state
// for the item that the cursor is pointing at.
case "enter", "space":
_, 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 ctrl+c and q 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 build atea.View. The view declares our UI content and, optionally, terminal features
like alt screen mode, mouse tracking, cursor position, and more.
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.
func (m model) View() tea.View {
// 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 tea.NewView(s)
}All Together Now
The last step is to simply run our program. We pass our initial model totea.NewProgram and let it rip:
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); 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. It's pretty simple.
There are also several Bubble Tea examples available and, of course,
there are Go Docs.
Debugging
Debugging with Delve
Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run
delve in headless mode and then connect to it:
# Start the debugger
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
API server listening at: 127.0.0.1:43000
# Connect to it from another terminal
$ dlv connect 127.0.0.1:43000If you do not explicitly supply the --listen flag, the port used will vary
per run, so passing this in makes the debugger easier to use from a script
or your IDE of choice.
Additionally, we pass in --api-version=2 because delve defaults to version 1
for backwards compatibility reasons. However, delve recommends using version 2
for all new development and some clients may no longer work with version 1.
For more information, see the Delve documentation.
Logging Stuff
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:
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
}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
- Bubbles: Common Bubble Tea components such as text inputs, viewports, spinners and so on
- Lip Gloss: Style, format and layout tools for terminal applications
- Harmonica: A spring animation library for smooth, natural motion
- BubbleZone: Easy mouse event tracking for Bubble Tea components
- ntcharts: A terminal charting library built for Bubble Tea and Lip Gloss
Bubble Tea in the Wild
There are over 18,000 applications built with Bubble Tea! Here are a handful of ’em.
Staff favourites
- chezmoi: securely manage your dotfiles across multiple machines
- circumflex: read Hacker News in the terminal
- gh-dash: a GitHub CLI extension for PRs and issues
- Tetrigo: Tetris in the terminal
- Signls: a generative midi sequencer designed for composition and live performance
- Superfile: a super file manager
In Industry
- Microsoft Azure – Aztify: bring Microsoft Azure resources under Terraform
- Daytona – Daytona: an AI infrastructure platform
- Cockroach Labs – CockroachDB: a cloud-native, high-availability distributed SQL database
- Truffle Security Co. – Trufflehog: find leaked credentials
- NVIDIA – container-canary: a container validator
- AWS – eks-node-viewer: a tool for visualizing dynamic node usage within an EKS cluster
- MinIO – mc: the official MinIO client
- Ubuntu – Authd: an authentication daemon for cloud-based identity providers
Charm stuff
- Glow: a markdown reader, browser, and online markdown stash
- Huh?: an interactive prompt and form toolkit
- Mods: AI on the CLI, built for pipelines
- Wishlist: an SSH directory (and bastion!)
There’s so much more where that came from
For more applications built with Bubble Tea see Charm & Friends.
Is there something cool you made with Bubble Tea you want to share? PRs are
welcome!
Contributing
See contributing.
Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
Acknowledgments
Bubble Tea is based on the paradigms of The Elm Architecture by Evan
Czaplicki et alia and the excellent go-tea by TJ Holowaychuk. It’s
inspired by the many great Zeichenorientierte Benutzerschnittstellen
of days past.
License
Part of Charm.
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
Overview (`examples/README.md`)
Examples
Alt Screen Toggle
The altscreen-toggle example shows how to transition between the alternative
screen buffer and the normal screen buffer using Bubble Tea.
Chat
The chat examples shows a basic chat application with a multi-line textarea
input.
Composable Views
The composable-views example shows how to compose two bubble models (spinner
and timer) together in a single application and switch between them.
ISBN Book Form
The isbn-form example demonstrates how to build a multi-step form withtextinput bubbles and validation on the inputs.
Debounce
The debounce example shows how to throttle key presses to avoid overloading
your Bubble Tea application.
Exec
The exec example shows how to execute a running command during the execution
of a Bubble Tea application such as launching an EDITOR.
Full Screen
The fullscreen example shows how to make a Bubble Tea application fullscreen.
Glamour
The glamour example shows how to use Glamour inside a viewport bubble.
Help
The help example shows how to use the help bubble to display help to the
user of your application.
Http
The http example shows how to make an http call within your Bubble Tea
application.
Default List
The list-default example shows how to use the list bubble.
Fancy List
The list-fancy example shows how to use the list bubble with extra customizations.
Simple List
The list-simple example shows how to use the list and customize it to have a simpler, more compact, appearance.
Mouse
The mouse example shows how to receive mouse events in a Bubble Tea
application.
Package Manager
The package-manager example shows how to build an interface for a package
manager using the tea.Println feature.
Pager
The pager example shows how to build a simple pager application similar toless.
Paginator
The paginator example shows how to build a simple paginated list.
Pipe
The pipe example demonstrates using shell pipes to communicate with Bubble
Tea applications.
Animated Progress
The progress-animated example shows how to build a progress bar with an
animated progression.
Download Progress
The progress-download example demonstrates how to download a file while
indicating download progress through Bubble Tea.
Static Progress
The progress-static example shows a progress bar with static incrementation
of progress.
Real Time
The realtime example demonstrates the use of go channels to perform realtime
communication with a Bubble Tea application.
Result
The result example shows a choice menu with the ability to select an option.
Send Msg
The send-msg example demonstrates the usage of custom tea.Msgs.
Sequence
The sequence example demonstrates the tea.Sequence command.
Simple
The simple example shows a very simple Bubble Tea application.
Spinner
The spinner example demonstrates a spinner bubble being used to indicate loading.
Spinners
The spinner example shows various spinner types that are available.
Split Editors
The split-editors example shows multiple textareas being used in a single
application and being able to switch focus between them.
Stop Watch
The stopwatch example shows a sample stop watch built with Bubble Tea.
Table
The table example demonstrates the table bubble being used to display tabular
data.
Tabs
The tabs example demonstrates tabbed navigation styled with Lip Gloss.
Text Area
The textarea example demonstrates a simple Bubble Tea application using atextarea bubble.
Text Input
The textinput example demonstrates a simple Bubble Tea application using a textinput bubble.
Multiple Text Inputs
The textinputs example shows multiple textinputs and being able to switch
focus between them as well as changing the cursor mode.
Timer
The timer example shows a simple timer built with Bubble Tea.
TUI Daemon
The tui-daemon-combo demonstrates building a text-user interface along with a
daemon mode using Bubble Tea.
Views
The views example demonstrates how to build a Bubble Tea application with
multiple views and switch between them.
Overview (`examples/altscreen-toggle/README.md`)
Alt Screen Toggle
Overview (`examples/composable-views/README.md`)
Composable Views
Overview (`examples/debounce/README.md`)
Debounce
Overview (`examples/fullscreen/README.md`)
Full Screen
Overview (`examples/glamour/README.md`)
Glamour
Overview (`examples/list-default/README.md`)
Default List
Overview (`examples/list-fancy/README.md`)
Fancy List
Overview (`examples/list-simple/README.md`)
Simple List
Overview (`examples/package-manager/README.md`)
Package Manager
Overview (`examples/paginator/README.md`)
Paginator
Overview (`examples/progress-animated/README.md`)
Animated Progress
Overview (`examples/progress-download/README.md`)
Download Progress
This example demonstrates how to download a file from a URL and show its
progress with a Progress Bubble.
In this case we're getting download progress with an io.TeeReader and
sending progress Msgs to the Program with Program.Send().
How to Run
Build the application with go build ., then run with a --url argument
specifying the URL of the file to download. For example:
./progress-download --url="https://download.blender.org/demo/color_vortex.blend"Note that in this example a TUI will not be shown for URLs that do not respond
with a ContentLength header.
This example originally came from this discussion.
Charm热爱开源 • Charm loves open source
Overview (`examples/progress-static/README.md`)
Static Progress
Overview (`examples/realtime/README.md`)
Real Time
Overview (`examples/send-msg/README.md`)
Send Msg
Overview (`examples/sequence/README.md`)
Sequence
Overview (`examples/spinner/README.md`)
Spinner
Overview (`examples/spinners/README.md`)
Spinners
Overview (`examples/split-editors/README.md`)
Split Editors
Overview (`examples/stopwatch/README.md`)
Stopwatch
Overview (`examples/textarea/README.md`)
Text Area
Overview (`examples/textinput/README.md`)
Text Input
Overview (`examples/textinputs/README.md`)
Text Inputs
Overview (`examples/tui-daemon-combo/README.md`)
TUI Daemon
Overview (`tutorials/basics/README.md`)
Bubble Tea Basics
Bubble Tea is based on the functional design paradigms of The Elm
Architecture, 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.
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.
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)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.
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.
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{}),
}
}After that, we’ll 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."
func (m model) Init() tea.Cmd {
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.KeyPressMsg messages, which are
automatically sent to the update function when keys are pressed.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Is it a key press?
case tea.KeyPressMsg:
// 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 space bar toggle the selected state for the
// item that the cursor is pointing at.
case "enter", "space":
_, 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 ctrl+c and q 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 tea.View. The view declares our UI content and, optionally, terminal
features like full-window mode (aka, altscreen mode), mouse tracking, cursor
position, and more.
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.
func (m model) View() tea.View {
// 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 tea.NewView(s)
}All Together Now
The last step is to simply run our program. We pass our initial model totea.NewProgram and let it rip:
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); 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. It’s pretty simple.
There are also several Bubble Tea examples available and, of course,
there are Go Docs.
Additional Resources
Feedback
We’d love to hear your thoughts on this tutorial. Feel free to drop us a note!
Part of Charm.
Charm热爱开源 • Charm loves open source
Overview (`tutorials/commands/README.md`)
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 knowledge of Go and a decent
understanding of the first tutorial.
You can find the non-annotated version of this program on GitHub.
Let’s Go!
For this tutorial we’re building a very simple program that makes an HTTP
request to a server and reports the status code of the response.
We’ll import a few necessary packages and put the URL we’re going to check in
a const.
package main
import (
"fmt"
"net/http"
"os"
"time"
tea "charm.land/bubbletea/v2"
)
const url = "https://charm.sh/"The Model
Next we’ll define our model. The only things we need to store are the status
code of the HTTP response and a possible error.
type model struct {
status int
err error
}Commands and Messages
Cmds 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 straightforward and simple.
Anyway, let’s write a Cmd that makes a request to a server and returns the
result as a Msg.
func checkServer() tea.Msg {
// Create an HTTP client and make a GET request.
c := &http.Client{Timeout: 10 * time.Second}
res, err := c.Get(url)
if err != nil {
// There was an error making our request. Wrap the error we received
// in a message and return it.
return errMsg{err}
}
// We received a response from the server. Return the HTTP status code
// as a message.
return statusMsg(res.StatusCode)
}
type statusMsg int
type errMsg struct{ err error }
// For messages that contain errors it's often handy to also implement the
// error interface on the message.
func (e errMsg) Error() string { return e.err.Error() }And notice that we’ve defined two new Msg types. They can be any type, even
an empty struct. We’ll come back to them later in our update function.
First, let’s write our initialization function.
The Initialization Method
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.
func (m model) Init() tea.Cmd {
return checkServer
}The Update Method
Internally, Cmds run asynchronously in a goroutine. The Msg they return is
collected and sent to our update function for handling. Remember those message
types we made earlier when we were making the checkServer command? We handle
them here. This makes dealing with many asynchronous operations very easy.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case statusMsg:
// The server returned a status message. Save it to our model. Also
// tell the Bubble Tea runtime we want to exit because we have nothing
// else to do. We'll still be able to render a final view with our
// status message.
m.status = int(msg)
return m, tea.Quit
case errMsg:
// There was an error. Note it in the model. And tell the runtime
// we're done and want to quit.
m.err = msg
return m, tea.Quit
case tea.KeyPressMsg:
// Ctrl+c exits. Even with short running programs it's good to have
// a quit key, just in case your logic is off. Users will be very
// annoyed if they can't exit.
if msg.Mod == tea.ModCtrl && msg.Code == 'c' {
return m, tea.Quit
}
}
// If we happen to get any other messages, don't do anything.
return m, nil
}The View Function
Our view is very straightforward. We look at the current model and build atea.View accordingly:
func (m model) View() tea.View {
// If there's an error, print it out and don't do anything else.
if m.err != nil {
return tea.NewView(fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err))
}
// Tell the user we're doing something.
s := fmt.Sprintf("Checking %s ... ", url)
// When the server responds with a status, add it to the current line.
if m.status > 0 {
s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status))
}
// Send off whatever we came up with above for rendering.
return tea.NewView("\n" + s + "\n\n")
}Run the program
The only thing left to do is run the program, so let’s do that! Our initial
model doesn’t need any data at all in this case, we just initialize it with
a model struct with default values.
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Printf("Uh oh, there was an error: %v\n", err)
os.Exit(1)
}
}And that’s that. There’s one more thing that is helpful to know aboutCmds, though.
One More Thing About Commands
Cmds are defined in Bubble Tea as type Cmd func() Msg. So they’re just
functions that don’t take any arguments and return a Msg, which can be
any type. If you need to pass arguments to a command, you just make a function
that returns a command. For example:
func cmdWithArg(id int) tea.Cmd {
return func() tea.Msg {
return someMsg{id: id}
}
}A more real-world example looks like:
func checkSomeUrl(url string) tea.Cmd {
return func() tea.Msg {
c := &http.Client{Timeout: 10 * time.Second}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
return statusMsg(res.StatusCode)
}
}Anyway, just make sure you do as much stuff as you can in the innermost
function, because that’s the one that runs asynchronously.
Now What?
After doing this tutorial and the previous one you should be ready to
build a Bubble Tea program of your own. We also recommend that you look at the
Bubble Tea example programs as well as Bubbles,
a component library for Bubble Tea.
And, of course, check out the Go Docs.
Additional Resources
Feedback
We’d love to hear your thoughts on this tutorial. Feel free to drop us a note!
Part of Charm.
Charm热爱开源 • Charm loves open source
--- METRICS ---
- Files Extracted: 28
- Estimated Token Budget: ~10260 tokens
- Canonical Reference: https://codewiki.google/github.com/charmbracelet/bubbletea