2020-07-23 22:04:53 -04:00
|
|
|
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
|
|
|
|
understanding of [the first tutorial][basics].
|
|
|
|
|
|
|
|
You can find the non-annotated version of this program [on GitHub][source].
|
|
|
|
|
2021-12-29 16:33:05 -05:00
|
|
|
[basics]: https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics
|
2022-01-25 20:08:16 -05:00
|
|
|
[source]: https://github.com/charmbracelet/bubbletea/blob/master/tutorials/commands/main.go
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
## Let's Go!
|
|
|
|
|
|
|
|
For this tutorial we're building a very simple program that makes an HTTP
|
2020-07-23 22:14:00 -04:00
|
|
|
request to a server and reports the status code of the response.
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
We'll import a few necessary packages and put the URL we're going to check in
|
|
|
|
a `const`.
|
|
|
|
|
|
|
|
```go
|
2020-08-26 15:29:52 -04:00
|
|
|
package main
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"time"
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
)
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
const url = "https://charm.sh/"
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
## The Model
|
|
|
|
|
2020-07-23 22:14:00 -04:00
|
|
|
Next we'll define our model. The only things we need to store are the status
|
2020-07-23 22:35:25 -04:00
|
|
|
code of the HTTP response and a possible error.
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
```go
|
2020-08-26 15:29:52 -04:00
|
|
|
type model struct {
|
|
|
|
status int
|
|
|
|
err error
|
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
## Commands and Messages
|
|
|
|
|
|
|
|
`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.
|
|
|
|
|
|
|
|
Anyway, let's write a `Cmd` that makes a request to a server and returns the
|
|
|
|
result as a `Msg`.
|
|
|
|
|
|
|
|
```go
|
2020-08-26 15:29:52 -04:00
|
|
|
func checkServer() tea.Msg {
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
// Create an HTTP client and make a GET request.
|
|
|
|
c := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
res, err := c.Get(url)
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
if err != nil {
|
|
|
|
// There was an error making our request. Wrap the error we received
|
|
|
|
// in a message and return it.
|
|
|
|
return errMsg{err}
|
2020-07-23 22:04:53 -04:00
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
// We received a response from the server. Return the HTTP status code
|
|
|
|
// as a message.
|
|
|
|
return statusMsg(res.StatusCode)
|
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
type statusMsg int
|
2020-08-26 15:28:18 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
type errMsg struct{ err error }
|
2020-08-26 15:28:18 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
// 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() }
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
2020-07-23 22:14:00 -04:00
|
|
|
And notice that we've defined two new `Msg` types. They can be any type, even
|
2021-11-21 14:14:00 -05:00
|
|
|
an empty struct. We'll come back to them later in our update function.
|
2020-07-23 22:04:53 -04:00
|
|
|
First, let's write our initialization function.
|
|
|
|
|
2020-10-15 20:26:02 -04:00
|
|
|
## The Initialization Method
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-10-15 20:26:02 -04:00
|
|
|
The initilization 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.
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
```go
|
2020-10-15 20:26:02 -04:00
|
|
|
func (m model) Init() (tea.Cmd) {
|
|
|
|
return checkServer
|
2020-08-26 15:29:52 -04:00
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
2020-10-15 20:26:02 -04:00
|
|
|
## The Update Method
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
Internally, `Cmd`s 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.
|
|
|
|
|
|
|
|
```go
|
2020-10-15 20:26:02 -04:00
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
2020-08-26 15:29:52 -04:00
|
|
|
switch msg := msg.(type) {
|
|
|
|
|
|
|
|
case statusMsg:
|
|
|
|
// The server returned a status message. Save it to our model. Also
|
2020-10-05 13:42:19 -04:00
|
|
|
// tell the Bubble Tea runtime we want to exit because we have nothing
|
2020-08-26 15:39:30 -04:00
|
|
|
// else to do. We'll still be able to render a final view with our
|
|
|
|
// status message.
|
2020-08-26 15:29:52 -04:00
|
|
|
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.KeyMsg:
|
|
|
|
// Ctrl+c exits. Even with short running programs it's good to have
|
|
|
|
// a quit key, just incase your logic is off. Users will be very
|
|
|
|
// annoyed if they can't exit.
|
|
|
|
if msg.Type == tea.KeyCtrlC {
|
2020-07-23 22:04:53 -04:00
|
|
|
return m, tea.Quit
|
|
|
|
}
|
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
|
|
|
|
// If we happen to get any other messages, don't do anything.
|
|
|
|
return m, nil
|
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
## The View Function
|
|
|
|
|
|
|
|
Our view is very straightforward. We look at the current model and build a
|
|
|
|
string accordingly:
|
|
|
|
|
|
|
|
```go
|
2020-10-15 20:26:02 -04:00
|
|
|
func (m model) View() string {
|
2020-08-26 15:29:52 -04:00
|
|
|
// If there's an error, print it out and don't do anything else.
|
|
|
|
if m.err != nil {
|
|
|
|
return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err)
|
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
// Tell the user we're doing something.
|
|
|
|
s := fmt.Sprintf("Checking %s ... ", url)
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-08-26 15:29:52 -04:00
|
|
|
// 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))
|
2020-07-23 22:04:53 -04:00
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
|
|
|
|
// Send off whatever we came up with above for rendering.
|
|
|
|
return "\n" + s + "\n\n"
|
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
## Run the program
|
|
|
|
|
2020-10-15 20:26:02 -04:00
|
|
|
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
|
|
|
|
as a `struct` with defaults.
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
```go
|
2020-08-26 15:29:52 -04:00
|
|
|
func main() {
|
2020-10-15 20:26:02 -04:00
|
|
|
if err := tea.NewProgram(model{}).Start(); err != nil {
|
2020-08-26 15:29:52 -04:00
|
|
|
fmt.Printf("Uh oh, there was an error: %v\n", err)
|
|
|
|
os.Exit(1)
|
2020-07-23 22:04:53 -04:00
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
2020-07-23 22:35:25 -04:00
|
|
|
And that's that. There's one more thing you that is helpful to know about
|
2020-07-23 22:04:53 -04:00
|
|
|
`Cmd`s, though.
|
|
|
|
|
|
|
|
## One More Thing About Commands
|
|
|
|
|
|
|
|
`Cmd`s 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
|
2021-09-04 14:36:49 -04:00
|
|
|
any type. If you need to pass arguments to a command, you just make a function
|
2020-07-23 22:04:53 -04:00
|
|
|
that returns a command. For example:
|
|
|
|
|
2021-09-04 14:36:49 -04:00
|
|
|
```go
|
|
|
|
func cmdWithArg(id int) tea.Cmd {
|
|
|
|
return func() tea.Msg {
|
|
|
|
return someMsg{id: int}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
A more real-world example looks like:
|
|
|
|
|
2020-07-23 22:04:53 -04:00
|
|
|
```go
|
2020-08-26 15:29:52 -04:00
|
|
|
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 {
|
2022-02-28 16:28:26 -05:00
|
|
|
return errMsg{err}
|
2020-07-23 22:04:53 -04:00
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
return statusMsg(res.StatusCode)
|
2020-07-23 22:04:53 -04:00
|
|
|
}
|
2020-08-26 15:29:52 -04:00
|
|
|
}
|
2020-07-23 22:04:53 -04:00
|
|
|
```
|
|
|
|
|
2021-09-04 14:36:49 -04:00
|
|
|
Anyway, just make sure you do as much stuff as you can in the innermost
|
|
|
|
function, because that's the one that runs asynchronously.
|
2020-07-23 22:35:25 -04:00
|
|
|
|
2021-09-04 14:36:49 -04:00
|
|
|
## Now What?
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2020-07-23 22:14:00 -04:00
|
|
|
After doing this tutorial and [the previous one][basics] you should be ready to
|
2021-09-04 14:36:49 -04:00
|
|
|
build a Bubble Tea program of your own. We also recommend that you look at the
|
2020-07-23 22:14:00 -04:00
|
|
|
Bubble Tea [example programs][examples] as well as [Bubbles][bubbles],
|
|
|
|
a component library for Bubble Tea.
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
And, of course, check out the [Go Docs][docs].
|
|
|
|
|
2021-12-29 16:33:05 -05:00
|
|
|
[bubbles]: https://github.com/charmbracelet/bubbles
|
|
|
|
[docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc
|
|
|
|
[examples]: https://github.com/charmbracelet/bubbletea/tree/master/examples
|
|
|
|
|
2021-09-04 14:36:49 -04:00
|
|
|
## Additional Resources
|
2020-07-23 22:04:53 -04:00
|
|
|
|
2021-09-04 14:36:49 -04:00
|
|
|
* [Libraries we use with Bubble Tea](https://github.com/charmbracelet/bubbletea/#libraries-we-use-with-bubble-tea)
|
|
|
|
* [Bubble Tea in the Wild](https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild)
|
2020-07-23 22:04:53 -04:00
|
|
|
|
|
|
|
### Feedback
|
|
|
|
|
|
|
|
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)
|
2022-07-19 19:23:37 -04:00
|
|
|
* [Slack](https://charm.sh/slack)
|
2021-09-04 14:36:49 -04:00
|
|
|
|
|
|
|
***
|
|
|
|
|
|
|
|
Part of [Charm](https://charm.sh).
|
|
|
|
|
2022-07-19 19:23:37 -04:00
|
|
|
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
2021-09-04 14:36:49 -04:00
|
|
|
|
|
|
|
Charm热爱开源 • Charm loves open source
|