Msgs can now be sent to Programs with Program.Send(Msg)

This commit is contained in:
Christian Rocha
2021-06-16 21:20:56 -04:00
parent b957c32c18
commit 0886ee26b0
2 changed files with 169 additions and 8 deletions

141
examples/send-msg/main.go Normal file
View File

@@ -0,0 +1,141 @@
package main
// A simple example that shows how to send messages to a Bubble Tea program
// from outside the program using Program.Send(Msg).
import (
"fmt"
"math/rand"
"os"
"strings"
"time"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
dotStyle = helpStyle.Copy().UnsetMargins()
foodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("36"))
durationStyle = dotStyle.Copy()
appStyle = lipgloss.NewStyle().Margin(1, 2, 0, 2)
)
type resultMsg struct {
duration time.Duration
food string
}
func (r resultMsg) String() string {
if r.duration == 0 {
return dotStyle.Render(strings.Repeat(".", 30))
}
return fmt.Sprintf("🍔 Ate %s %s", r.food,
durationStyle.Render(r.duration.String()))
}
type model struct {
spinner spinner.Model
results []resultMsg
quitting bool
}
func newModel() model {
const numLastResults = 5
s := spinner.NewModel()
s.Style = spinnerStyle
return model{
spinner: s,
results: make([]resultMsg, numLastResults),
}
}
func (m model) Init() tea.Cmd {
return spinner.Tick
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
m.quitting = true
return m, tea.Quit
case resultMsg:
m.results = append(m.results[1:], msg)
return m, nil
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
default:
return m, nil
}
}
func (m model) View() string {
var s string
if m.quitting {
s += "Thats all for today!"
} else {
s += m.spinner.View() + " Eating food..."
}
s += "\n\n"
for _, res := range m.results {
s += res.String() + "\n"
}
if !m.quitting {
s += helpStyle.Render("Press any key to exit")
}
if m.quitting {
s += "\n"
}
return appStyle.Render(s)
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
done := make(chan struct{})
p := tea.NewProgram(newModel())
go func() {
if err := p.Start(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
close(done)
}()
// Simulate activity
go func() {
for {
pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond
time.Sleep(pause)
// Send the Bubble Tea program a message from outside the program.
p.Send(resultMsg{food: randomFood(), duration: pause})
}
}()
<-done
}
func randomEmoji() string {
emojis := []rune("🍦🧋🍡🤠👾😭🦊🐯🦆🥨🎏🍔🍒🍥🎮📦🦁🐶🐸🍕🥐🧲🚒🥇🏆🌽")
return string(emojis[rand.Intn(len(emojis))])
}
func randomFood() string {
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"}
return string(food[rand.Intn(len(food))])
}

36
tea.go
View File

@@ -215,6 +215,8 @@ type Program struct {
mtx *sync.Mutex
done chan struct{}
msgs chan Msg
output io.Writer // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
renderer renderer
@@ -371,7 +373,6 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
initialModel: model,
output: os.Stdout,
input: os.Stdin,
done: make(chan struct{}),
CatchPanics: true,
}
@@ -385,9 +386,11 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
// Start initializes the program.
func (p *Program) Start() error {
p.msgs = make(chan Msg)
p.done = make(chan struct{})
var (
cmds = make(chan Cmd)
msgs = make(chan Msg)
errs = make(chan error)
// If output is a file (e.g. os.Stdout) then this will be set
@@ -435,7 +438,7 @@ func (p *Program) Start() error {
select {
case <-ctx.Done():
case <-sig:
msgs <- quitMsg{}
p.msgs <- quitMsg{}
}
}()
@@ -502,7 +505,7 @@ func (p *Program) Start() error {
}
errs <- err
}
msgs <- msg
p.msgs <- msg
}
}()
}
@@ -514,11 +517,11 @@ func (p *Program) Start() error {
if err != nil {
errs <- err
}
msgs <- WindowSizeMsg{w, h}
p.msgs <- WindowSizeMsg{w, h}
}()
// Listen for window resizes
go listenForResize(outputAsFile, msgs, errs)
go listenForResize(outputAsFile, p.msgs, errs)
}
// Process commands
@@ -530,7 +533,7 @@ func (p *Program) Start() error {
case cmd := <-cmds:
if cmd != nil {
go func() {
msgs <- cmd()
p.msgs <- cmd()
}()
}
}
@@ -543,7 +546,7 @@ func (p *Program) Start() error {
case err := <-errs:
p.shutdown(false)
return err
case msg := <-msgs:
case msg := <-p.msgs:
// Handle special internal messages
switch msg := msg.(type) {
@@ -593,6 +596,21 @@ func (p *Program) Start() error {
}
}
// Send sends a message to the main update function, effectively allowing
// messages to be injected from outside the program for interoperability
// purposes.
//
// If the program is not running this this will be a no-op, so it's safe to
// send messages if the program is unstarted, or has exited.
//
// This method is currently provisional. The method signature may alter
// slightly, or it may be removed in a future version of this package.
func (p *Program) Send(msg Msg) {
if p.msgs != nil {
p.msgs <- msg
}
}
// shutdown performs operations to free up resources and restore the terminal
// to its original state.
func (p *Program) shutdown(kill bool) {
@@ -602,6 +620,8 @@ func (p *Program) shutdown(kill bool) {
p.renderer.stop()
}
close(p.done)
close(p.msgs)
p.msgs = nil
p.ExitAltScreen()
p.DisableMouseCellMotion()
p.DisableMouseAllMotion()