package tui import ( "context" "errors" tea "charm.land/bubbletea/v2" ) type shutdownMsg struct{} // Run owns operations until their output, terminal result and metadata have // settled, including when a signal or terminal failure ends the program. func (m *Model) Run(ctx context.Context, options ...tea.ProgramOption) error { defer m.stopObservations() options = append(options, tea.WithContext(context.Background()), tea.WithoutSignalHandler(), tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg { switch msg.(type) { case tea.QuitMsg, tea.InterruptMsg: if m.operationActive || m.preparedManagerUpdate != nil || (m.state == StateInstalling && !m.installComplete) { return shutdownMsg{} } } return msg })) p := tea.NewProgram(m, options...) finished := make(chan struct{}) watcherDone := make(chan struct{}) go func() { defer close(watcherDone) select { case <-ctx.Done(): m.cancelObservations() p.Send(shutdownMsg{}) case <-finished: } }() _, err := p.Run() close(finished) <-watcherDone // A renderer/input failure can bypass Update. Pending reads are retained by // the model, so drain the same operation without starting another reader. if m.operationActive { m.requestOperationCancellation(false, true) for m.operationActive { m.Update(m.nextOperationStreamCmd()()) } } if m.preparedManagerUpdate != nil { err = errors.Join(err, m.preparedManagerUpdate.Abort()) m.preparedManagerUpdate = nil } return errors.Join(err, ctx.Err()) }