diff --git a/tea.go b/tea.go index e249d71bba..64bbee012a 100644 --- a/tea.go +++ b/tea.go @@ -993,10 +993,6 @@ 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) @@ -1004,6 +1000,17 @@ func (p *Program) Run() (returnModel Model, returnErr error) { 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 { @@ -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. diff --git a/tea_test.go b/tea_test.go index c0b6f88cac..cbef5ec42a 100644 --- a/tea_test.go +++ b/tea_test.go @@ -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") + } + } +}