diff --git a/examples/send-msg/main.go b/examples/send-msg/main.go new file mode 100644 index 0000000..f61c036 --- /dev/null +++ b/examples/send-msg/main.go @@ -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 += "Thatโ€™s 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))]) +} diff --git a/tea.go b/tea.go index 6303052..c0d1bbd 100644 --- a/tea.go +++ b/tea.go @@ -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()