charmbracelet/bubbletea

Cannot Init correctly when there are multiple Models

Charliego3 opened this issue · 2 comments

Describe the bug
As the title says, only tea.NewProgram(model) will call the Init() function when there are multiple Models, and the Model returned by the Update function has not been properly Init.
In the following example, secondModel returned by firstModel.Update does not call Init, when the return value of firstModel.Update is changed to return secondModel{s}, tea.Batch(tea.Println( f.input.Value()), s.Tick), spinner.Tick will be consumed correctly. While this allows the program to run normally, it shouldn't be.

Setup

  • OS: macOS 13.5
  • Shell: fish
  • Terminal Emulator: [kitty, Terminal]

Source Code

package main

import (
	"github.com/charmbracelet/bubbles/spinner"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"testing"
)

type firstModel struct {
	input textinput.Model
	done  bool
}

func (f firstModel) Init() tea.Cmd {
	return textinput.Blink
}

func (f firstModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch k := msg.(type) {
	case tea.KeyMsg:
		if k.String() == "enter" {
			f.done = true

			s := spinner.New(spinner.WithSpinner(spinner.Globe))
			//return secondModel{s}, tea.Batch(tea.Println(f.input.Value()), s.Tick)
			return secondModel{s}, tea.Println(f.input.Value())
		}
	}

	var cmd tea.Cmd
	f.input, cmd = f.input.Update(msg)
	return f, cmd
}

func (f firstModel) View() string {
	if f.done {
		return ""
	}
	return f.input.View() + "\n"
}

type secondModel struct {
	spinner spinner.Model
}

func (s secondModel) Init() tea.Cmd {
	return s.spinner.Tick
}

func (s secondModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch k := msg.(type) {
	case tea.KeyMsg:
		if k.String() == "q" {
			return s, tea.Quit
		}
	}

	var cmd tea.Cmd
	s.spinner, cmd = s.spinner.Update(msg)
	return s, cmd
}

func (s secondModel) View() string {
	return s.spinner.View() + " wait a moment\n"
}

func TestMultiModel(t *testing.T) {
	ti := textinput.New()
	ti.Placeholder = "please enter something"
	ti.Focus()
	ti.CharLimit = 200
	ti.Width = 60
	ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#870004"))
	ti.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#005a2c"))
	_, _ = tea.NewProgram(firstModel{input: ti}).Run()
}

Expected behavior
Hope Init can be called correctly. Instead of returning additional spinner.Tick Cmd when returning secondModel

Screenshots

2023-08-21.15.19.53.mov

Hey! I think there is misunderstanding going on
Purpose of tea.Model returned by Update is state updating, internally it replaces model it was called with. Notice firstModel isn't pointer type, but rather value type meaning fields won't be updated as it's effectively a copy of model and not "real" model.
What I'm implying here is that bubbletea doesn't have to and won't ever compare previous model with returned model, Init is called exactly once for root model, for child models it is your choice when, how and what do you call, bubbletea has no control over anything that isn't root model.

As a rule of a thumb, you should never return different tea.Model in Update than function receiver one.

Code that replaces (updates) model in bubbletea loop:

model, cmd = model.Update(msg) // run update

As of what you are trying to make you might want to try out my library for bubbletea called reactea, it supports multiple views and such, might be an overkill for what you are trying to do.
https://github.com/londek/reactea/tree/v0.4.2

Best regards

Thank you very much, you are right, although this can achieve different pages, but I should not do this, reactea is I want.