@@ -27,6 +27,9 @@ import (
2727 "golang.org/x/sync/errgroup"
2828)
2929
30+ // ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
31+ var ErrProgramPanic = errors .New ("program experienced a panic" )
32+
3033// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
3134var ErrProgramKilled = errors .New ("program was killed" )
3235
@@ -147,6 +150,12 @@ type Program struct {
147150
148151 inputType inputType
149152
153+ // externalCtx is a context that was passed in via WithContext, otherwise defaulting
154+ // to ctx.Background() (in case it was not), the internal context is derived from it.
155+ externalCtx context.Context
156+
157+ // ctx is the programs's internal context for signalling internal teardown.
158+ // It is built and derived from the externalCtx in NewProgram().
150159 ctx context.Context
151160 cancel context.CancelFunc
152161
@@ -243,11 +252,11 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
243252
244253 // A context can be provided with a ProgramOption, but if none was provided
245254 // we'll use the default background context.
246- if p .ctx == nil {
247- p .ctx = context .Background ()
255+ if p .externalCtx == nil {
256+ p .externalCtx = context .Background ()
248257 }
249258 // Initialize context and teardown channel.
250- p .ctx , p .cancel = context .WithCancel (p .ctx )
259+ p .ctx , p .cancel = context .WithCancel (p .externalCtx )
251260
252261 // if no output was set, set it to stdout
253262 if p .output == nil {
@@ -346,7 +355,11 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
346355 go func () {
347356 // Recover from panics.
348357 if ! p .startupOptions .has (withoutCatchPanics ) {
349- defer p .recoverFromPanic ()
358+ defer func () {
359+ if r := recover (); r != nil {
360+ p .recoverFromGoPanic (r )
361+ }
362+ }()
350363 }
351364
352365 msg := cmd () // this can be long.
@@ -460,7 +473,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
460473
461474 case BatchMsg :
462475 for _ , cmd := range msg {
463- cmds <- cmd
476+ select {
477+ case <- p .ctx .Done ():
478+ return model , nil
479+ case cmds <- cmd :
480+ }
464481 }
465482 continue
466483
@@ -506,7 +523,13 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
506523
507524 var cmd Cmd
508525 model , cmd = model .Update (msg ) // run update
509- cmds <- cmd // process command (if any)
526+
527+ select {
528+ case <- p .ctx .Done ():
529+ return model , nil
530+ case cmds <- cmd : // process command (if any)
531+ }
532+
510533 p .renderer .write (model .View ()) // send view to renderer
511534 }
512535 }
@@ -515,11 +538,15 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
515538// Run initializes the program and runs its event loops, blocking until it gets
516539// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
517540// Returns the final model.
518- func (p * Program ) Run () (Model , error ) {
541+ func (p * Program ) Run () (returnModel Model , returnErr error ) {
519542 p .handlers = channelHandlers {}
520543 cmds := make (chan Cmd )
521- p .errs = make (chan error )
522- p .finished = make (chan struct {}, 1 )
544+ p .errs = make (chan error , 1 )
545+
546+ p .finished = make (chan struct {})
547+ defer func () {
548+ close (p .finished )
549+ }()
523550
524551 defer p .cancel ()
525552
@@ -568,7 +595,12 @@ func (p *Program) Run() (Model, error) {
568595
569596 // Recover from panics.
570597 if ! p .startupOptions .has (withoutCatchPanics ) {
571- defer p .recoverFromPanic ()
598+ defer func () {
599+ if r := recover (); r != nil {
600+ returnErr = fmt .Errorf ("%w: %w" , ErrProgramKilled , ErrProgramPanic )
601+ p .recoverFromPanic (r )
602+ }
603+ }()
572604 }
573605
574606 // If no renderer is set use the standard one.
@@ -645,11 +677,27 @@ func (p *Program) Run() (Model, error) {
645677
646678 // Run event loop, handle updates and draw.
647679 model , err := p .eventLoop (model , cmds )
648- killed := p . ctx . Err () != nil || err != nil
649- if killed && err == nil {
650- err = fmt . Errorf ( "%w: %s" , ErrProgramKilled , p . ctx . Err ())
680+
681+ if err == nil && len ( p . errs ) > 0 {
682+ err = <- p . errs // Drain a leftover error in case eventLoop crashed
651683 }
652- if err == nil {
684+
685+ killed := p .externalCtx .Err () != nil || p .ctx .Err () != nil || err != nil
686+ if killed {
687+ if err == nil && p .externalCtx .Err () != nil {
688+ // Return also as context error the cancellation of an external context.
689+ // This is the context the user knows about and should be able to act on.
690+ err = fmt .Errorf ("%w: %w" , ErrProgramKilled , p .externalCtx .Err ())
691+ } else if err == nil && p .ctx .Err () != nil {
692+ // Return only that the program was killed (not the internal mechanism).
693+ // The user does not know or need to care about the internal program context.
694+ err = ErrProgramKilled
695+ } else {
696+ // Return that the program was killed and also the error that caused it.
697+ err = fmt .Errorf ("%w: %w" , ErrProgramKilled , err )
698+ }
699+ } else {
700+ // Graceful shutdown of the program (not killed):
653701 // Ensure we rendered the final state of the model.
654702 p .renderer .write (model .View ())
655703 }
@@ -704,11 +752,11 @@ func (p *Program) Quit() {
704752 p .Send (Quit ())
705753}
706754
707- // Kill stops the program immediately and restores the former terminal state.
755+ // Kill signals the program to stop immediately and restore the former terminal state.
708756// The final render that you would normally see when quitting will be skipped.
709757// [program.Run] returns a [ErrProgramKilled] error.
710758func (p * Program ) Kill () {
711- p .shutdown ( true )
759+ p .cancel ( )
712760}
713761
714762// Wait waits/blocks until the underlying Program finished shutting down.
@@ -717,7 +765,11 @@ func (p *Program) Wait() {
717765}
718766
719767// shutdown performs operations to free up resources and restore the terminal
720- // to its original state.
768+ // to its original state. It is called once at the end of the program's lifetime.
769+ //
770+ // This method should not be called to signal the program to be killed/shutdown.
771+ // Doing so can lead to race conditions with the eventual call at the program's end.
772+ // As alternatives, the [Quit] or [Kill] convenience methods should be used instead.
721773func (p * Program ) shutdown (kill bool ) {
722774 p .cancel ()
723775
@@ -744,19 +796,30 @@ func (p *Program) shutdown(kill bool) {
744796 }
745797
746798 _ = p .restoreTerminalState ()
747- if ! kill {
748- p .finished <- struct {}{}
749- }
750799}
751800
752801// recoverFromPanic recovers from a panic, prints the stack trace, and restores
753802// the terminal to a usable state.
754- func (p * Program ) recoverFromPanic () {
755- if r := recover (); r != nil {
756- p .shutdown (true )
757- fmt .Printf ("Caught panic:\n \n %s\n \n Restoring terminal...\n \n " , r )
758- debug .PrintStack ()
803+ func (p * Program ) recoverFromPanic (r interface {}) {
804+ select {
805+ case p .errs <- ErrProgramPanic :
806+ default :
759807 }
808+ p .shutdown (true ) // Ok to call here, p.Run() cannot do it anymore.
809+ fmt .Printf ("Caught panic:\n \n %s\n \n Restoring terminal...\n \n " , r )
810+ debug .PrintStack ()
811+ }
812+
813+ // recoverFromGoPanic recovers from a goroutine panic, prints a stack trace and
814+ // signals for the program to be killed and terminal restored to a usable state.
815+ func (p * Program ) recoverFromGoPanic (r interface {}) {
816+ select {
817+ case p .errs <- ErrProgramPanic :
818+ default :
819+ }
820+ p .cancel ()
821+ fmt .Printf ("Caught goroutine panic:\n \n %s\n \n Restoring terminal...\n \n " , r )
822+ debug .PrintStack ()
760823}
761824
762825// ReleaseTerminal restores the original terminal state and cancels the input
0 commit comments