Skip to content
Draft
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
17 changes: 12 additions & 5 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -993,17 +993,24 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
return nil, errors.New("bubbletea: InitialModel cannot be nil")
}

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

p.finished = make(chan struct{})
defer func() {
close(p.finished)
}()

defer p.cancel()

// If the context has already been cancelled (e.g. Kill() was
// called before Run()), return early without initializing the
// terminal.
if p.ctx.Err() != nil {
return p.initialModel, ErrProgramKilled
}

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

if p.disableInput {
p.input = nil
} else if p.input == nil {
Expand Down Expand Up @@ -1202,7 +1209,7 @@ func (p *Program) Quit() {
// The final render that you would normally see when quitting will be skipped.
// [program.Run] returns a [ErrProgramKilled] error.
func (p *Program) Kill() {
p.shutdown(true)
p.cancel()
}

// Wait waits/blocks until the underlying Program finished shutting down.
Expand Down
24 changes: 24 additions & 0 deletions tea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,27 @@ func BenchmarkTeaRun(b *testing.B) {
_ = r.CloseWithError(io.EOF)
}
}

func TestKillDuringStartupRace(t *testing.T) {
for range 100 {
p := NewProgram(&testModel{},
WithInput(bytes.NewBuffer(nil)),
WithOutput(io.Discard),
WithoutSignals(),
)

done := make(chan struct{})
go func() {
_, _ = p.Run()
close(done)
}()

p.Kill()

select {
case <-done:
case <-time.After(time.Second):
t.Fatal("program did not stop")
}
}
}