charmbracelet / bubbletea

A powerful little TUI framework 🏗
MIT License
27.47k stars 792 forks source link

Node-based rendering #79

Open ahmedkhalf opened 3 years ago

ahmedkhalf commented 3 years ago

Hello, I am trying to make a complex TUI application but unfortunately the single view model that bubbletea offers is severely limiting and only allows for the creation of linear CLI apps. Manipulating a string to add a popup window to the view has proven to be very difficult and inefficient. My proposed solution is to add child views.

meowgorithm commented 3 years ago

Hi! So this essentially comes down to writing a compositor and matching renderer, which is something we'd like to do, however we can't provide a timeline as it's a fairly large endeavor.

TheApeMachine commented 3 years ago

Hmm, not sure if I am either understanding the requirement wrong, or I am doing something I should not be doing. I was trying to figure out sort of similar behavior, where I want to have bubbles (I guess is what we call them?) dynamically load and unload. Just now I did a little experiment just using a goroutine for each bubble, and that seems to work fine. Is this bad? What I was thinking was to wrap each bubble in another object that contains an in and out channel (or something, I just discovered this project (which I love by the way, been looking for this) and I hardly know anything about it.) and use my "layout" object to orchestrate messaging between them, but that's just an idea...

layout.models = []tea.Model{
    NewStatus(),
    NewPager(),
}

for _, sub := range layout.models
    go func(sub tea.Model) {
        tea.NewProgram(sub).Start()
    }(sub)
}

ui := ""

for _, sub := range layout.models {
    ui += sub.View()
}

I suppose if you wanted to do this with string manipulation you could just have some sort of template variable substitution and redraw the entire screen? Not sure either...

76creates commented 2 years ago

Hmm, not sure if I am either understanding the requirement wrong, or I am doing something I should not be doing. I was trying to figure out sort of similar behavior, where I want to have bubbles (I guess is what we call them?) dynamically load and unload. Just now I did a little experiment just using a goroutine for each bubble, and that seems to work fine. Is this bad? What I was thinking was to wrap each bubble in another object that contains an in and out channel (or something, I just discovered this project (which I love by the way, been looking for this) and I hardly know anything about it.) and use my "layout" object to orchestrate messaging between them, but that's just an idea...

layout.models = []tea.Model{
    NewStatus(),
    NewPager(),
}

for _, sub := range layout.models
    go func(sub tea.Model) {
        tea.NewProgram(sub).Start()
    }(sub)
}

ui := ""

for _, sub := range layout.models {
    ui += sub.View()
}

I suppose if you wanted to do this with string manipulation you could just have some sort of template variable substitution and redraw the entire screen? Not sure either...

Nice hack, unfortunately listening for events becomes impossible ⚡

TheApeMachine commented 2 years ago

Why? It's been a while since I have worked on this so forgive me if I do not see the obvious, but there must be a way to collate all of the events over a shared channel right? Or better still, implement some pub/sub queue kind of thing?

TheApeMachine commented 2 years ago

Especially since in my previous example I did not halt the main goroutine yet, so the queue could do that. So something like:

eventBus := make(chan tea.Msg)
queue := NewQueue(eventBus)

layout.models = []tea.Model{
    NewStatus(queue, eventBus),
    NewPager(queue, eventBus),
}

for _, sub := range layout.models
    go func(sub tea.Model) {
        tea.NewProgram(sub).Start()
    }(sub)
}

ui := ""

for _, sub := range layout.models {
    ui += sub.View()
}

queue.Start()

Then inside the model, something like:

func (m model) Init() tea.Cmd {
    queue.registerListener('someEvent', someFunctor);

    // Just return `nil`, which means "no I/O right now, please."
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    m.eventBus <- msg // Or just do something locally, but this is event listening/sharing across components I think?
}

// Sort of like a callback.
func (m Model) someFunctor() {}

Really you don't even need to separate the eventBus, just have an exposed method on Queue to call to add an event to the stack and distribute it to all the listeners. Then inject the queue into all the components, or leave it an ambient context if that makes sense.

markhuge commented 2 years ago

For anyone reading this who just wants a popup overlay, here's a quick and dirty text compositor example.

If you're using lipgloss you'll probably need to tweak this so it plays nice with the ANSI control codes.

I invested about 15 mins into writing this so YMMV, no warranty, good luck, etc.

package main

import (
    "fmt"
    "strings"
)

func composite(main, overlay string, xoffset, yoffset int) string {
    doc := strings.Builder{}
    m := strings.Split(main, "\n")
    o := strings.Split(overlay, "\n")

    for i, row := range m {

        for j, char := range row {
            if j < xoffset || i < yoffset || i >= len(o)+yoffset || j >= len(o[i-yoffset])+xoffset {

                doc.WriteRune(char)
                continue
            }

            doc.WriteByte(o[i-yoffset][j-xoffset])

        }
        doc.WriteString("\n")

    }

    return doc.String()
}

const (
    A = `_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________
_______________________________________________________________`

    B = `OOOOOOOOOO
OOOOOOOOOO
OOOOOOOOOO
OOOOOOOOOO`
)

func main() {
    fmt.Println(composite(A, B, 10, 3))
}
meowgorithm commented 1 year ago

Just an update here: given that Bubblezone is a thing now, the solution here will will most likely only need to be implemented in Lip Gloss.