Skip to content

Commit

Permalink
feat: add example graphics, clean
Browse files Browse the repository at this point in the history
  • Loading branch information
bashbunni committed Mar 12, 2024
1 parent e8da519 commit d1ffded
Showing 1 changed file with 175 additions and 10 deletions.
185 changes: 175 additions & 10 deletions tutorials/patterns/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Common Patterns in Bubble Tea

So you've started building your app, but now you're not sure if you're doing
You've started building your app, but now you're not sure if you're doing
things the "right way".

Well thankfully, we have some common patterns that we see when building Bubble
Tea applications that should help to simplify your decision-making.
Thankfully, there are some common patterns that you'll come across when
building Bubble Tea applications that should help to simplify your
decision-making.

## Managing multiple components in one model

<img width="800" src="https://github.com/charmbracelet/bubbletea/blob/master/examples/composable-views/composable-views.gif" />

If you have a composite view, then you have multiple components on one screen
that you want to be able to switch between. To handle this in Bubble Tea you'll
want your parent component to house a `state` field that dictates which element
on the screen is focused and receiving key presses.

You can see a [basic example][basic] of this on our GitHub.
You can see a [basic example][basic] of this in the repo where we switch focus
between a timer and spinner.

```go
switch m.state {
Expand All @@ -37,21 +41,43 @@ To figure out whether a component should process the message or not, simply
include an ID in the message. The ID will match the ID field of your child
model and can be handled in that child model's `Update`.

We use this pattern in our [spinner example][spinner]
This pattern is used in the [spinner bubble][spinner]:

These spots in particular:
https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go#L145-L149
<img width="800" src="https://github.com/charmbracelet/bubbletea/blob/patterns/examples/spinner/spinner.gif" />

```go
// Update is the Tea update function.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
// If an ID is set, and the ID doesn't belong to this spinner, reject
// the message.
if msg.ID > 0 && msg.ID != m.id {
return m, nil
}

// If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and
// thus spinning too fast.
if msg.tag > 0 && msg.tag != m.tag {
return m, nil
}

m.frame++
if m.frame >= len(m.Spinner.Frames) {
m.frame = 0
}

m.tag++
// include the ID of the model that triggered the msg
return m, m.tick(m.id, m.tag)
default:
return m, nil
}
}
```

https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go#L164
https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go#L195-L203l
This is what that `tick` function does:

```go
func (m Model) tick(id, tag int) tea.Cmd {
Expand All @@ -65,6 +91,145 @@ func (m Model) tick(id, tag int) tea.Cmd {
}
```

[Source](https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go#L195-L203l)

## I want my Bubble Tea program to display external processes

How do I send information to my Bubble Tea app? There are a couple of examples
on how to handle this behavior in the Bubble Tea Repo:
- [downloading a file and feeding the progress to Bubble Tea][progress-download]
- [a `p.Send` example that simulates a message from outside the program][send-msg].

<img width="800" src="https://github.com/charmbracelet/bubbletea/blob/master/examples/send-msg/send-msg.gif" />

The goal here is to have the external process run in a [Goroutine][goroutine].
1. Create a new `tea.Program` with your model.
2. Start a Goroutine for the external process you want to document in your
Bubble Tea program.
3. Use [`p.Send`][psend] to send the data to your Bubble Tea application.
4. Run your `tea.Program` outside the Goroutine.
5. Handle that message type in your `Update` function.

In the simpler `p.Send` example, it looks like this:
```go
func main() {
p := tea.NewProgram(newModel())

// Simulate activity
go func() {
for {
pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond // nolint:gosec
time.Sleep(pause)

// Send the Bubble Tea program a message from outside the
// tea.Program. This will block until it is ready to receive
// messages.
p.Send(resultMsg{food: randomFood(), duration: pause})
}
}()

if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// ...
case resultMsg:
m.results = append(m.results[1:], msg)
return m, nil
}
}
```
[Source][send-msg]

In the more specific download example, it looks like this:

```go
func (pw *progressWriter) Start() {
// TeeReader calls pw.Write() each time a new response is received
_, err := io.Copy(pw.file, io.TeeReader(pw.reader, pw))
if err != nil {
p.Send(progressErrMsg{err})
}
}

// ...

func main() {
// ...
pw := &progressWriter{
total: int(resp.ContentLength),
file: file,
reader: resp.Body,
onProgress: func(ratio float64) {
p.Send(progressMsg(ratio))
},
}

m := model{
pw: pw,
progress: progress.New(progress.WithDefaultGradient()),
}
// Start Bubble Tea
p = tea.NewProgram(m)

// Start the download
go pw.Start()

if _, err := p.Run(); err != nil {
fmt.Println("error running program:", err)
os.Exit(1)
}
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// ...
case progressMsg:
var cmds []tea.Cmd

if msg >= 1.0 {
cmds = append(cmds, tea.Sequence(finalPause(), tea.Quit))
}

cmds = append(cmds, m.progress.SetPercent(float64(msg)))
return m, tea.Batch(cmds...)
}
}
```
[Source][progress-download]

Let us know in Discussions if there are other patterns that you'd like to see!
If there's enough interest we can certainly include it in these docs.

## Additional Resources

* [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)

### 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.social/@charmcli)
* [Discord](https://charm.sh/chat)

***

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

[psend]: https://pkg.go.dev/github.com/charmbracelet/bubbletea#Program.Send
[goroutine]: https://go.dev/doc/effective_go#goroutines
[send-msg]: https://github.com/charmbracelet/bubbletea/blob/master/examples/send-msg/main.go
[progress-download]: https://github.com/charmbracelet/bubbletea/blob/master/examples/progress-download/main.go
[basic]: https://github.com/charmbracelet/bubbletea/blob/master/examples/composable-views/main.go
[glow]: https://github.com/charmbracelet/glow/blob/f0734709f0be19a34e648caaf63340938a50caa2/ui/ui.go#L434
[spinner]: https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go
[spinner]: https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go#L142-L168

0 comments on commit d1ffded

Please sign in to comment.