@@ -32,6 +32,9 @@ import (
32
32
"golang.org/x/sync/errgroup"
33
33
)
34
34
35
+ // ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
36
+ var ErrProgramPanic = errors .New ("program experienced a panic" )
37
+
35
38
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
36
39
var ErrProgramKilled = errors .New ("program was killed" )
37
40
@@ -210,6 +213,12 @@ type Program struct {
210
213
211
214
inputType inputType
212
215
216
+ // externalCtx is a context that was passed in via WithContext, otherwise defaulting
217
+ // to ctx.Background() (in case it was not), the internal context is derived from it.
218
+ externalCtx context.Context
219
+
220
+ // ctx is the programs's internal context for signalling internal teardown.
221
+ // It is built and derived from the externalCtx in NewProgram().
213
222
ctx context.Context
214
223
cancel context.CancelFunc
215
224
@@ -341,11 +350,11 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
341
350
342
351
// A context can be provided with a ProgramOption, but if none was provided
343
352
// we'll use the default background context.
344
- if p .ctx == nil {
345
- p .ctx = context .Background ()
353
+ if p .externalCtx == nil {
354
+ p .externalCtx = context .Background ()
346
355
}
347
356
// Initialize context and teardown channel.
348
- p .ctx , p .cancel = context .WithCancel (p .ctx )
357
+ p .ctx , p .cancel = context .WithCancel (p .externalCtx )
349
358
350
359
// if no output was set, set it to stdout
351
360
if p .output == nil {
@@ -470,7 +479,11 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
470
479
go func () {
471
480
// Recover from panics.
472
481
if ! p .startupOptions .has (withoutCatchPanics ) {
473
- defer p .recoverFromPanic ()
482
+ defer func () {
483
+ if r := recover (); r != nil {
484
+ p .recoverFromPanic (r )
485
+ }
486
+ }()
474
487
}
475
488
476
489
msg := cmd () // this can be long.
@@ -686,7 +699,11 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
686
699
687
700
case BatchMsg :
688
701
for _ , cmd := range msg {
689
- cmds <- cmd
702
+ select {
703
+ case <- p .ctx .Done ():
704
+ return model , nil
705
+ case cmds <- cmd :
706
+ }
690
707
}
691
708
continue
692
709
@@ -751,7 +768,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
751
768
752
769
var cmd Cmd
753
770
model , cmd = model .Update (msg ) // run update
754
- cmds <- cmd // process command (if any)
771
+
772
+ select {
773
+ case <- p .ctx .Done ():
774
+ return model , nil
775
+ case cmds <- cmd : // process command (if any)
776
+ }
755
777
756
778
p .render (model ) // render view
757
779
}
@@ -789,11 +811,15 @@ func (p *Program) render(model Model) {
789
811
// Run initializes the program and runs its event loops, blocking until it gets
790
812
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
791
813
// Returns the final model.
792
- func (p * Program ) Run () (Model , error ) {
814
+ func (p * Program ) Run () (returnModel Model , returnErr error ) {
793
815
p .handlers = channelHandlers {}
794
816
cmds := make (chan Cmd )
795
- p .errs = make (chan error )
796
- p .finished = make (chan struct {}, 1 )
817
+ p .errs = make (chan error , 1 )
818
+
819
+ p .finished = make (chan struct {})
820
+ defer func () {
821
+ close (p .finished )
822
+ }()
797
823
798
824
defer p .cancel ()
799
825
@@ -842,7 +868,12 @@ func (p *Program) Run() (Model, error) {
842
868
843
869
// Recover from panics.
844
870
if ! p .startupOptions .has (withoutCatchPanics ) {
845
- defer p .recoverFromPanic ()
871
+ defer func () {
872
+ if r := recover (); r != nil {
873
+ returnErr = fmt .Errorf ("%w: %w" , ErrProgramKilled , ErrProgramPanic )
874
+ p .recoverFromPanic (r )
875
+ }
876
+ }()
846
877
}
847
878
848
879
// Check if output is a TTY before entering raw mode, hiding the cursor and
@@ -986,11 +1017,27 @@ func (p *Program) Run() (Model, error) {
986
1017
987
1018
// Run event loop, handle updates and draw.
988
1019
model , err := p .eventLoop (model , cmds )
989
- killed := p . ctx . Err () != nil || err != nil
990
- if killed && err == nil {
991
- err = fmt . Errorf ( "%w: %s" , ErrProgramKilled , p . ctx . Err ())
1020
+
1021
+ if err == nil && len ( p . errs ) > 0 {
1022
+ err = <- p . errs // Drain a leftover error in case eventLoop crashed.
992
1023
}
993
- if err == nil {
1024
+
1025
+ killed := p .externalCtx .Err () != nil || p .ctx .Err () != nil || err != nil
1026
+ if killed {
1027
+ if err == nil && p .externalCtx .Err () != nil {
1028
+ // Return also as context error the cancellation of an external context.
1029
+ // This is the context the user knows about and should be able to act on.
1030
+ err = fmt .Errorf ("%w: %w" , ErrProgramKilled , p .externalCtx .Err ())
1031
+ } else if err == nil && p .ctx .Err () != nil {
1032
+ // Return only that the program was killed (not the internal mechanism).
1033
+ // The user does not know or need to care about the internal program context.
1034
+ err = ErrProgramKilled
1035
+ } else {
1036
+ // Return that the program was killed and also the error that caused it.
1037
+ err = fmt .Errorf ("%w: %w" , ErrProgramKilled , err )
1038
+ }
1039
+ } else {
1040
+ // Graceful shutdown of the program (not killed):
994
1041
// Ensure we rendered the final state of the model.
995
1042
p .render (model )
996
1043
}
@@ -1068,9 +1115,6 @@ func (p *Program) shutdown(kill bool) {
1068
1115
}
1069
1116
1070
1117
_ = p .restoreTerminalState ()
1071
- if ! kill {
1072
- p .finished <- struct {}{}
1073
- }
1074
1118
1075
1119
// Print a final newline to ensure the terminal prompt is on a new line.
1076
1120
p .execute ("\r \n " )
@@ -1079,12 +1123,15 @@ func (p *Program) shutdown(kill bool) {
1079
1123
1080
1124
// recoverFromPanic recovers from a panic, prints the stack trace, and restores
1081
1125
// the terminal to a usable state.
1082
- func (p * Program ) recoverFromPanic () {
1083
- if r := recover (); r != nil {
1084
- p .shutdown (true )
1085
- fmt .Printf ("Caught panic:\n \n %s\n \n Restoring terminal...\n \n " , r )
1086
- debug .PrintStack ()
1126
+ func (p * Program ) recoverFromPanic (r interface {}) {
1127
+ select {
1128
+ case p .errs <- ErrProgramPanic :
1129
+ default :
1087
1130
}
1131
+ p .cancel () // Just in case a previous shutdown has failed.
1132
+ p .shutdown (true )
1133
+ fmt .Printf ("Caught panic:\n \n %s\n \n Restoring terminal...\n \n " , r )
1134
+ debug .PrintStack ()
1088
1135
}
1089
1136
1090
1137
// ReleaseTerminal restores the original terminal state and cancels the input
0 commit comments