Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,9 @@ type Program struct {
// be capped at 120.
fps int

// initialModel is the initial model for the program and is the only
// required field when creating a new program.
initialModel Model
// model stores the last updated model as an atomic pointer for safe
// concurrent access.
model Model

// disableRenderer prevents the program from rendering to the terminal.
// This can be useful for running daemon-like programs that don't require a
Expand Down Expand Up @@ -458,6 +458,9 @@ type Program struct {
// rendererDone is used to stop the renderer.
rendererDone chan struct{}

// rendererModel is used to synchronize model updates with the renderer.
rendererModel chan Model

// Initial window size. Mainly used for testing.
width, height int

Expand Down Expand Up @@ -512,7 +515,7 @@ func Interrupt() Msg {
// NewProgram creates a new [Program].
func NewProgram(model Model, opts ...ProgramOption) *Program {
p := &Program{
initialModel: model,
model: model,
msgs: make(chan Msg),
errs: make(chan error, 1),
rendererDone: make(chan struct{}),
Expand Down Expand Up @@ -776,16 +779,18 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
case cmds <- cmd: // process command (if any)
}

p.render(model) // render view
select {
case <-p.ctx.Done():
return model, nil
case p.rendererModel <- model: // send model to render loop
}
}
}
}

// render renders the given view to the renderer.
func (p *Program) render(model Model) {
if p.renderer != nil {
p.renderer.render(model.View()) // send view to renderer
}
p.renderer.render(model.View()) // send view to renderer
}

func (p *Program) execSequenceMsg(msg sequenceMsg) {
Expand Down Expand Up @@ -860,14 +865,16 @@ func (p *Program) execBatchMsg(msg BatchMsg) {
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
// Returns the final model.
func (p *Program) Run() (returnModel Model, returnErr error) {
if p.initialModel == nil {
model := p.model
if model == nil {
return nil, errors.New("bubbletea: InitialModel cannot be nil")
}

// Initialize context and teardown channel.
p.handlers = channelHandlers{}
cmds := make(chan Cmd)

p.rendererModel = make(chan Model)
p.finished = make(chan struct{})
defer func() {
close(p.finished)
Expand Down Expand Up @@ -899,7 +906,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
// Check if output is a TTY before entering raw mode, hiding the cursor and
// so on.
if err := p.initTerminal(); err != nil {
return p.initialModel, err
return model, err
}

// Get the initial window size.
Expand All @@ -908,7 +915,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
// Set the initial size of the terminal.
w, h, err := term.GetSize(p.ttyOutput.Fd())
if err != nil {
return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
return model, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
}

width, height = w, h
Expand Down Expand Up @@ -952,7 +959,6 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
go p.Send(EnvMsg(p.environ))

// Init the input reader and initial model.
model := p.initialModel
if p.input != nil {
if err := p.initInputReader(false); err != nil {
return model, err
Expand Down Expand Up @@ -1257,13 +1263,17 @@ func (p *Program) startRenderer() {
// Start the renderer.
p.renderer.start()
go func() {
model := p.model
for {
select {
case <-p.rendererDone:
p.ticker.Stop()
return

case model = <-p.rendererModel:

case <-p.ticker.C:
p.render(model) // send view to renderer
_ = p.flush()
_ = p.renderer.flush(false)
}
Expand Down
27 changes: 26 additions & 1 deletion tea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ func (m *testModel) Update(msg Msg) (Model, Cmd) {
}

case KeyPressMsg:
return m, Quit
switch msg.String() {
case "q", "ctrl+c":
return m, Quit
}

case panicMsg:
panic("testing panic behavior")
Expand Down Expand Up @@ -586,3 +589,25 @@ func TestTeaGoroutinePanic(t *testing.T) {
t.Fatalf("Expected %v, got %v", ErrProgramKilled, err)
}
}

func BenchmarkTeaRun(b *testing.B) {
for b.Loop() {
var buf bytes.Buffer
var in bytes.Buffer

m := &testModel{}
p := NewProgram(m,
WithInput(&in),
WithOutput(&buf),
WithWindowSize(80, 24),
)

in.Reset()
in.Write([]byte("abc123q"))
buf.Reset()

if _, err := p.Run(); err != nil {
b.Errorf("Run failed: %v", err)
}
}
}
Loading