Code Monkey home page Code Monkey logo

Comments (26)

meowgorithm avatar meowgorithm commented on May 21, 2024 9

@curio77, @wcauchois et alia: this is now in master and will be included in the next release. It’s very simple to use:

p := tea.NewProgram(model)

go func() {
    if err := p.Start(); err != nil {
        fmt.Println("Uh oh", err)
        os.Exit(1)
    }
}()

p.Send(someMsg{value: "ta da!"});

We believe this is an important feature, so thanks for chiming in on this one!

from bubbletea.

curio77 avatar curio77 commented on May 21, 2024 7

One more thing… If in the tutorial you linked, say, you wanted to indicate the progress of the HTTP GET request (let's assume it's a big download). The way the example is set up, the checkServer() method only returns (a command) upon completion or failure. How would you go about reporting progress (and later on success/failure)? I think that would be reasonable real-world-related extension of that example. 😃 Especially as this would kinda loop back to my initial question as it would actually introduce the need to update the model during an ongoing external process.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024 5

@terrabitz This is cool. Do note that you can achieve something similar with just a channel and commands (and without explicit goroutines). The trick is that you have a command listen on a channel, returning a message after the receive. Then, once update receives the message, you have that command listen for the next message.

You can also run the spinner independently, so you don't need to manage its FPS. If you wanted to stop the spinner once a certain message arrived you could do that with a simple flag on the model and a conditional in the spinner portion of the type switch in Update. That is, if a condition is true don't return the usual spin command, effectively stopping the spinner.

Here's a generic example (see listenForActivity and waitForActivity specifically):

package main

import (
	"fmt"
	"math/rand"
	"os"
	"time"

	"github.com/charmbracelet/bubbles/spinner"
	tea "github.com/charmbracelet/bubbletea"
)

func main() {
	rand.Seed(time.Now().UTC().UnixNano())

	p := tea.NewProgram(model{
		sub:     make(chan struct{}),
		spinner: spinner.NewModel(),
	})

	if p.Start() != nil {
		fmt.Println("could not start program")
		os.Exit(1)
	}
}

// A message used to indicate that activity has occured. In the real world (for
// example, chat) this would probably contain actual data.
type responseMsg struct{}

// Simulate a process that sends events at an irregular interval. In this case,
// we'll send events on the channel at a random interval between 100 to 1000
// milliseconds. Bubble Tea will run this asyncronously.
func listenForActivity(sub chan struct{}) tea.Cmd {
	return func() tea.Msg {
		for {
			time.Sleep(time.Millisecond * time.Duration(rand.Int63n(900)+100))
			sub <- struct{}{}
		}
	}
}

// A command that waits for the activity on the channel.
func waitForActivity(sub chan struct{}) tea.Cmd {
	return func() tea.Msg {
		return responseMsg(<-sub)
	}
}

// Our Bubble Tea model.
type model struct {

	// Where we'll receive activity notifications
	sub chan struct{}

	// How many responses we've received
	responses int

	spinner spinner.Model
}

func (m model) Init() tea.Cmd {
	return tea.Batch(
		spinner.Tick,
		listenForActivity(m.sub), // generate activity
		waitForActivity(m.sub),   // wait for activity
	)
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg.(type) {
	case tea.KeyMsg:
		return m, tea.Quit
	case responseMsg:
		m.responses++                    // record external activity
		return m, waitForActivity(m.sub) // wait for next event
	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 {
	return fmt.Sprintf("%s Events received: %d\n\nPress any key to exit\n", m.spinner.View(), m.responses)
}

from bubbletea.

wcauchois avatar wcauchois commented on May 21, 2024 4

Hey, I just wanted to chime in that I think this feature would be useful as well. I was investigating using Bubble Tea for a chat app, and I think it would be clean to be able to send messages from a handler that's listening for real time message updates.

I think that if I could send messages from outside the framework, that would be a clean way to do that, although I could be misunderstanding it all.

I was looking at the source and I think other internal components kind of do this anyway, like the way that keyboard events are injected. Would there be any harm in exposing the chan Msg to users of this library?

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024 3

Hey! You do this in Bubble Tea with commands. Here's a simple example where we make an HTTP request and send the result to Update. I also recommend having a look at the command tutorial.

Basically in Update you return a tea.Cmd (which is just a func() tea.Msg) which performs some IO. The Bubble Tea runtime puts that function into a queue, processes it in a goroutine, and then calls Update with a Msg argument which contains the result of your operation.

Let me know if you if you have any more questions about this.

from bubbletea.

terrabitz avatar terrabitz commented on May 21, 2024 3

@meowgorithm I think that's exactly what I'm looking for. I didn't know about the tea.Batch command, which seems to check all my boxes.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024 2

It's worth exploring; I can see some interesting applications for it. We made an exception for keyboard events as you noticed, namely because handling keyboard input seemed like a common enough need that it should simply be enabled by default. Nothing else sends to channel msgs directly yet.

Anyway, though it's not immediately obvious, it's totally possible to handle realtime, channel-based, stuff in Bubble Tea right now (Glow searches for local files in realtime this way). You basically store a channel in the model and then receive activity in a command. The benefit here is that you can coordinate receives with the state of the model. Here's roughly how you'd do it:

type connectionMsg *chan activity
type connErrMsg struct{ error }
type chatActivityMsg activity

// Connect to server
func connectCmd() tea.Msg {
	conn, err := getConnection()
	if err != nil {
		return errMsg(err)
	}
	return connectionMsg(conn)
}

// Listen for chat activity
func listenCmd(m model) tea.Cmd {
	return func() {
		return activityMsg(<-m.conn)
	}
}

type chatActivity struct {
	username string
	message  string
}

type model struct {
	conn *chan chatActivity
}

func (m model) Init() tea.Cmd {
	return connectCmd
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := m.(type) {
	case connectionMsg:
		m.conn = msg
		return m, listenCmd(m) // listen for events
	case connErrMsg:
		// respond to connection error
	case chatActivityMsg:
		// handle to chat events here
		return m, listenCmd(m) // listen next event
	}
}

from bubbletea.

anaseto avatar anaseto commented on May 21, 2024 2

Here's the definition I use for effects: I use an Effect interface with unexported method as a sum-type-like for Cmd and Sub types. The drawback in practice is the explicit Cmd conversion needed at times, like in the Batch definition, because Go cannot directly accept func () Msg as an Effect. The other alternative I considered, was a more Elm-like approach, expanding the Model interface with a Subscriptions method, but it didn't feel right for simple models that did not need it.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024 2

Cool, this makes sense. Is there a way to cancel the subscription from within the application, or do they only cancel when the application cancels its main context?

from bubbletea.

anaseto avatar anaseto commented on May 21, 2024 2

I would like to add that, after thoroughly thinking about it, I'm now convinced too that it's not only always possible, but also easy enough (with a bit of experience) to replace a subscription with chained commands (with a channel in the model for communication if needed). Even for animations, I finally decided against subscriptions in my turn-based game, because I do not have animations running all the time (only on specific events and actions), so a tight coupling with Update was natural in the end.

That's not to say subscriptions are completely useless: this thread alone is proof that simulating them with commands is not always that obvious. But they're by no means necessary: the difference resides in a different kind of code coupling with Update. Commands require a tighter coupling with Update, which may or may not be more intuitive depending on the cases.

from bubbletea.

reitzig avatar reitzig commented on May 21, 2024 2

Wait, this is less convoluted and avoids the block in Update:

type Notes struct {
	current string
	rest    chan string
}

func (n Notes) awaitNext() Notes {
	return Notes{current: <-n.rest, rest: n.rest}
}

func (m model) Init() tea.Cmd {
	channelOut := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		for i := 0; i < 10; i++ {
			channelOut <- string(rune(100 + i))
			time.Sleep(1 * time.Second)
		}
		close(channelOut)
	}()

	return func() tea.Msg {
		return Notes{<-channelOut, channelOut}
	}
}

type model struct {
	currentNote string
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {

	// ...

	case Notes:
		m.currentNote = msg.current
		return m, func() tea.Msg { return msg.awaitNext() }
	}

	return m, nil
}

func (m model) View() string {
	return m.currentNote
}

(My tired brain used "notes" meaning notifications or something; it could be "progress" as well, referring to the OP.)

Wrapper type Notes for convenience; the proposed handling of channels in the middleware would remove the need for it.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024 1

Ah, I see what you're saying now!

To your first point, I've been wondering if Bubble Tea programs should exit with a value. Maybe even the final model. The not-so-glamorous part about that is that it probably involves a type assertion. Anyway, #7 was asking something similar and in retrospect solving this with a channel feels a bit like a hack as you mention.

To your second point about progress feedback, that's totally valid and totally needed example. It's still doable with commands; I'll let you know when I have an example.

Thanks for the great feedback, by the way!

from bubbletea.

curio77 avatar curio77 commented on May 21, 2024

I had a bit of a different point of view, but I'm sure I'm kinda abusing the concept with this. In what I'm currently doing, I'm evaluating using Bubble Tea for things like progress indications for steps in a complex console application doing various kinds of consecutive processing. As the application is, for the most part, not concerned about interactive user input (aside from interruption) and it does too many different things, I don't intend to wrap a Bubble Tea program around everything but currently rather just use programs to render progress in various situations during individual steps, updating their models externally. This (abuse? 😉) is BTW also how I came across what I posted in #24.

from bubbletea.

mritd avatar mritd commented on May 21, 2024

I’m not sure if I understand it correctly. Regarding the progress bar, I try to add a stage field to the Model to indicate the progress; after each job is completed, adjust the stage value, and the view method is only responsible for displaying it according to the stage value;

This is an example of a continuous progress bar used to read and write from etcd database, hope it can be helpful to you:

https://github.com/etcdhosts/dnsctl/blob/3dfd95e28c33834d96a67da426bddbb5f0abfb1a/cmd_add.go#L102

2020-12-27 11 59 40

from bubbletea.

anaseto avatar anaseto commented on May 21, 2024

If I'm not mistaken, this issue actually asks for the so called subscriptions in Elm. I had the same issue recently for a project of mine: I'm currently working on a library strongly inspired by Elm and bubbletea (great idea BTW!) but only for full-window grid-based applications (with games as a priority) supporting too sdl and the browser. While it's possible to simulate that with commands, sometimes the subscription feels more natural (for example for a long running ticker or listening on a socket). I finally chose to allow Update to return either a command or a subscription (with msgs and context) by defining an Effect interface that is either a Cmd or a Subscription. It could be done in bubbletea too in an almost backward-compatible way, except sometimes for the need of an explicit type conversion from func () Msg to Cmd.

Otherwise, as subscriptions are mostly useful for the main model of an application in my experience, maybe they could just be added directly to the Program.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024

@anaseto Bubble Tea had a notion of subscriptions early on but we ultimately removed it because the implementation we came up with it didn't feel quite right. I'd love to see your version.

from bubbletea.

meowgorithm avatar meowgorithm commented on May 21, 2024

@anaseto This is really interesting; thanks for sending it over. Do you happen to have any examples of how subscriptions look on the implementation side?

from bubbletea.

anaseto avatar anaseto commented on May 21, 2024

I don't have code that uses subscriptions among the published examples, but the use I have currently for them is to produce ticker timed messages to synchronize game animation drawings and control fps (my library does not handle fps by default), something that looks like:

type MsgAnimation time.Time

func SubMsgDraw(ctx context.Context, msgs chan<- Msg) {
    t := time.NewTicker(time.Second / 60)
    for {
        select {
        case <-t.C:
            select {
            case msgs <- MsgAnimation(time.Now()):
            case <-ctx.Done(): // avoid blocking
            }
        case <-ctx.Done():
            t.Stop()
            return
        }
    }
}

And the subscription is returned on the first Update (or when appropriate).

from bubbletea.

anaseto avatar anaseto commented on May 21, 2024

They cancel when the application cancels its main context, either on the Model's request or external. It is be possible to cancel them from within the application by sending another command to communicate later with it on a custom channel that the subscription would have to check in addition to ctx.Done(), though I haven't tried that: I'm currently in the early stages of porting a game of mine to this architecture, but it's far from done yet. The old code used the pre-historical approach of manually waiting for input, update some stuff then ad hoc drawing with hacky time.Sleep added as needed for animations. I'm still unsure about how much I will be using subscriptions :-)

An additional natural application for subscriptions I have in mind, but have not tried yet, is to report progress of a long command, such as when loading or saving a game, loading assets or generating a new game (similar to what curio77 had in mind with http requests).

from bubbletea.

terrabitz avatar terrabitz commented on May 21, 2024

Just wanted to add my own take here. I was experimenting with this framework and was in a similar situation to the OP: I had a background channel continually listening for HTTP requests, and I needed an update to occur signalling that a new request had come in so it could be displayed. Simultaneously, I wanted a spinner to indicate that no requests had come in yet. I modified @anaseto's subscriptions solution and came up with something similar:

type model struct {
	msgs           chan tea.Msg
	requestChan    <-chan *http.Request
	currentRequest *http.Request
	spinner        spinner.Model
}

func (m *model) Init() tea.Cmd {
	ctx := context.Background()
	subs := []Sub{
		m.listenSub,
		m.tickSub,
	}

	for _, sub := range subs {
		go sub(ctx, m.msgs)
	}

	return m.anyMsg
}

type requestMessage *http.Request

func (m *model) listenSub(ctx context.Context, msgs chan<- tea.Msg) {
	for {
		select {
		case msgs <- requestMessage(<-m.requestChan):
		case <-ctx.Done():
		}
	}
}

func (m *model) tickSub(ctx context.Context, msgs chan<- tea.Msg) {
	t := time.NewTicker(time.Second / 6)

	for {
		select {
		case <-t.C:
			select {
			case msgs <- spinner.Tick():
			case <-ctx.Done():
			}
		case <-ctx.Done():
			t.Stop()
			return
		}
	}
}

func (m *model) anyMsg() tea.Msg {
	return <-m.msgs
}

Essentially, this solution creates its own message channel that it continually pops messages from. From what I understand from reading the above comments, this mimics how bubbletea works internally (i.e. using a message channel that can be written to concurrently). The one downside of this solution is that I have to control FPS of the spinner, since I can't rely on the spinner's own internal ticking mechanism. But that's not a huge deal in my opinion.

Full example code here: https://gist.github.com/terrabitz/b6781bbd2fb366c52d408f737fc84a5c

from bubbletea.

reitzig avatar reitzig commented on May 21, 2024

Having a channel in the model that might only be used during some phases of the lifetime feels wrong.

Not sure whether p.Send helps with messages coming from within the program; wouldn't I have to store p in the model (or make it global)?

But you say, anything can be a Msg ? 🤔 So how about returning a channel from the Cmd?

Here's a silly but working example (disclaimer: beginner, so I may be missing something obvious):

func (m model) Init() tea.Cmd {
	channelOut := make(chan tea.KeyMsg)

	go func() {
		for i := 0; i < 10; i++ {
			channelOut <- tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{rune(100 + i)}}
			time.Sleep(1 * time.Second)
		}
		close(channelOut)
	}()

	return func() tea.Msg {
		return channelOut
	}
}

type model struct {
	currentRune rune
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {

	// Is it a key press?
	case tea.KeyMsg:

		// Cool, what was the actual key pressed?
		switch msg.String() {

		// These keys should exit the program.
		case "ctrl+c", "q":
			return m, tea.Quit
		}

	case chan tea.KeyMsg:
		keyMsg, ok := <-msg // this may block?
		if ok {
			m.currentRune = keyMsg.Runes[0]
			return m, func() tea.Msg { return msg }
		}
	}

	return m, nil
}

func (m model) View() string {
	return string(m.currentRune)
}

This feels better. 😄

Looking at this, I would ask: can't the middleware handle channels of messages returned from a command? Consume the channel "in the background" and call Update with each singular message, as they appear. That would result in, arguably, natural behaviour, and would remove the need for any special handling in either model or update function. (And remove the potential block in Update which I think I introduced.) 🤔

from bubbletea.

guy4261 avatar guy4261 commented on May 21, 2024

Looks like if I had a program and now I want to add some bubbletea, then I would have to move my formerly main function inside func (m model) Init() tea.Cmd:

(like https://github.com/charmbracelet/bubbletea/blob/master/examples/http/main.go#L34-L36))

AKA old main:

func main(){
	c := &http.Client{
		Timeout: 10 * time.Second,
	}
	res, err := c.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
}

New main:

# code goes into function returning tea.Msg
func checkServer() tea.Msg {
	c := &http.Client{
	...

# this will run the former main	
func (m model) Init() tea.Cmd {
	return checkServer
}

# this will init the model.
func main() {
	p := tea.NewProgram(model{})
    p.Run()
}

from bubbletea.

guy4261 avatar guy4261 commented on May 21, 2024

Compare with Python's tqdm (if that's not too rude!). You get it via $ pip install tqdm.

# vanilla
for i in range(10):
    pass
    
# with tqdm
from tqdm.auto import tqdm
for i in tqdm(range(10)):
    pass

Notice how I don't have to enslave my entire codebase just to get a progress bar. And I really want to do it with bubbletea nonetheless ❤️

from bubbletea.

guy4261 avatar guy4261 commented on May 21, 2024

Compare with Python's tqdm (if that's not too rude!). You get it via $ pip install tqdm.

# vanilla
for i in range(10):
    pass
    
# with tqdm
from tqdm.auto import tqdm
for i in tqdm(range(10)):
    pass

Notice how I don't have to enslave my entire codebase just to get a progress bar. And I really want to do it with bubbletea nonetheless ❤️

from bubbletea.

3goats avatar 3goats commented on May 21, 2024

Hi hope somebody can help me here. I have the following for loop in a go app

for {

        line, err := reader.ReadBytes('\n')
        if err != nil {
            if err == io.EOF {
                break
            }
            fmt.Printf("Error reading streamed bytes %v", err)
        }
        rb = append(rb, line...)
        err = json.Unmarshal([]byte(rb), &h)
        if err == nil {
            err = json.Unmarshal([]byte(h.HTTPBufferedTrace.Request.Body.AsString), &r)
            log.Info(fmt.Sprintf("Status: [%s]\n", r.Request))
            
            p.Send(resultMsg{food: r.Request, duration: 0}) // trying to send to Bubbletea queue here

}

Basically it listens to a HTTP event stream and prints the output. I'm following this example and was hoping that p.Send would be whats needed to inject my events into the Bubbletea app, but it doesnt work.

from bubbletea.

caozhuozi avatar caozhuozi commented on May 21, 2024

So cool!

from bubbletea.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.