chore: break up Start into several, more maintainable methods

This commit is contained in:
Christian Muehlhaeuser 2022-10-06 18:39:57 +02:00
parent 0e76ba142a
commit 2696b2f339
2 changed files with 165 additions and 138 deletions

297
tea.go
View File

@ -154,68 +154,8 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
return p return p
} }
// StartReturningModel initializes the program. Returns the final model. func (p *Program) handleSignals() chan struct{} {
func (p *Program) StartReturningModel() (Model, error) { ch := make(chan struct{})
cmds := make(chan Cmd)
p.errs = make(chan error)
// Channels for managing goroutine lifecycles.
var (
sigintLoopDone = make(chan struct{})
cmdLoopDone = make(chan struct{})
resizeLoopDone = make(chan struct{})
initSignalDone = make(chan struct{})
waitForGoroutines = func(withReadLoop bool) {
if withReadLoop {
p.waitForReadLoop()
}
<-cmdLoopDone
<-resizeLoopDone
<-sigintLoopDone
<-initSignalDone
}
)
var cancelContext context.CancelFunc
p.ctx, cancelContext = context.WithCancel(context.Background())
defer cancelContext()
switch {
case p.startupOptions.has(withInputTTY):
// Open a new TTY, by request
f, err := openInputTTY()
if err != nil {
return p.initialModel, err
}
defer f.Close() //nolint:errcheck
p.input = f
case !p.startupOptions.has(withCustomInput):
// If the user hasn't set a custom input, and input's not a terminal,
// open a TTY so we can capture input as normal. This will allow things
// to "just work" in cases where data was piped or redirected into this
// application.
f, isFile := p.input.(*os.File)
if !isFile {
break
}
if isatty.IsTerminal(f.Fd()) {
break
}
f, err := openInputTTY()
if err != nil {
return p.initialModel, err
}
defer f.Close() //nolint:errcheck
p.input = f
}
// Listen for SIGINT and SIGTERM. // Listen for SIGINT and SIGTERM.
// //
@ -230,7 +170,7 @@ func (p *Program) StartReturningModel() (Model, error) {
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
defer func() { defer func() {
signal.Stop(sig) signal.Stop(sig)
close(sigintLoopDone) close(ch)
}() }()
for { for {
@ -246,67 +186,12 @@ func (p *Program) StartReturningModel() (Model, error) {
} }
}() }()
if p.CatchPanics { return ch
defer func() {
if r := recover(); r != nil {
p.shutdown(true)
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
return
}
}()
} }
// If no renderer is set use the standard one. // handleResize handles terminal resize events.
if p.renderer == nil { func (p *Program) handleResize() chan struct{} {
p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor)) ch := make(chan struct{})
}
// Check if output is a TTY before entering raw mode, hiding the cursor and
// so on.
if err := p.initTerminal(); err != nil {
return p.initialModel, err
}
// Honor program startup options.
if p.startupOptions&withAltScreen != 0 {
p.renderer.enterAltScreen()
}
if p.startupOptions&withMouseCellMotion != 0 {
p.renderer.enableMouseCellMotion()
} else if p.startupOptions&withMouseAllMotion != 0 {
p.renderer.enableMouseAllMotion()
}
// Initialize the program.
model := p.initialModel
if initCmd := model.Init(); initCmd != nil {
go func() {
defer close(initSignalDone)
select {
case cmds <- initCmd:
case <-p.ctx.Done():
}
}()
} else {
close(initSignalDone)
}
// Start the renderer.
p.renderer.start()
// Render the initial view.
p.renderer.write(model.View())
// Subscribe to user input.
if p.input != nil {
if err := p.initCancelReader(); err != nil {
return model, err
}
} else {
defer close(p.readLoopDone)
}
defer p.cancelReader.Close() //nolint:errcheck
if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) { if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) {
// Get the initial terminal size and send it to the program. // Get the initial terminal size and send it to the program.
@ -323,20 +208,27 @@ func (p *Program) StartReturningModel() (Model, error) {
}() }()
// Listen for window resizes. // Listen for window resizes.
go listenForResize(p.ctx, f, p.msgs, p.errs, resizeLoopDone) go listenForResize(p.ctx, f, p.msgs, p.errs, ch)
} else { } else {
close(resizeLoopDone) close(ch)
} }
// Process commands. return ch
}
// handleCommands runs commands in a goroutine and sends the result to the
// program's message channel.
func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
ch := make(chan struct{})
go func() { go func() {
defer close(cmdLoopDone) defer close(ch)
for { for {
select { select {
case <-p.ctx.Done(): case <-p.ctx.Done():
return return
case cmd := <-cmds: case cmd := <-cmds:
if cmd == nil { if cmd == nil {
continue continue
@ -357,25 +249,24 @@ func (p *Program) StartReturningModel() (Model, error) {
} }
}() }()
// Handle updates and draw. return ch
}
// eventLoop is the central message loop. It receives and handles the default
// Bubble Tea messages, update the model and triggers redraws.
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
for { for {
select { select {
case <-p.killc: case <-p.killc:
return nil, nil return nil, nil
case err := <-p.errs: case err := <-p.errs:
cancelContext()
waitForGoroutines(p.cancelReader.Cancel())
p.shutdown(false)
return model, err return model, err
case msg := <-p.msgs: case msg := <-p.msgs:
// Handle special internal messages. // Handle special internal messages.
switch msg := msg.(type) { switch msg := msg.(type) {
case quitMsg: case quitMsg:
cancelContext()
waitForGoroutines(p.cancelReader.Cancel())
p.shutdown(false)
return model, nil return model, nil
case clearScreenMsg: case clearScreenMsg:
@ -438,6 +329,142 @@ func (p *Program) StartReturningModel() (Model, error) {
} }
} }
// StartReturningModel initializes the program. Returns the final model.
func (p *Program) StartReturningModel() (Model, error) {
cmds := make(chan Cmd)
p.errs = make(chan error)
var cancelContext context.CancelFunc
p.ctx, cancelContext = context.WithCancel(context.Background())
defer cancelContext()
switch {
case p.startupOptions.has(withInputTTY):
// Open a new TTY, by request
f, err := openInputTTY()
if err != nil {
return p.initialModel, err
}
defer f.Close() //nolint:errcheck
p.input = f
case !p.startupOptions.has(withCustomInput):
// If the user hasn't set a custom input, and input's not a terminal,
// open a TTY so we can capture input as normal. This will allow things
// to "just work" in cases where data was piped or redirected into this
// application.
f, isFile := p.input.(*os.File)
if !isFile {
break
}
if isatty.IsTerminal(f.Fd()) {
break
}
f, err := openInputTTY()
if err != nil {
return p.initialModel, err
}
defer f.Close() //nolint:errcheck
p.input = f
}
// Handle signals.
sigintLoopDone := p.handleSignals()
if p.CatchPanics {
defer func() {
if r := recover(); r != nil {
p.shutdown(true)
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
return
}
}()
}
// If no renderer is set use the standard one.
if p.renderer == nil {
p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
}
// Check if output is a TTY before entering raw mode, hiding the cursor and
// so on.
if err := p.initTerminal(); err != nil {
return p.initialModel, err
}
// Honor program startup options.
if p.startupOptions&withAltScreen != 0 {
p.renderer.enterAltScreen()
}
if p.startupOptions&withMouseCellMotion != 0 {
p.renderer.enableMouseCellMotion()
} else if p.startupOptions&withMouseAllMotion != 0 {
p.renderer.enableMouseAllMotion()
}
// Initialize the program.
initSignalDone := make(chan struct{})
model := p.initialModel
if initCmd := model.Init(); initCmd != nil {
go func() {
defer close(initSignalDone)
select {
case cmds <- initCmd:
case <-p.ctx.Done():
}
}()
} else {
close(initSignalDone)
}
// Start the renderer.
p.renderer.start()
// Render the initial view.
p.renderer.write(model.View())
// Subscribe to user input.
if p.input != nil {
if err := p.initCancelReader(); err != nil {
return model, err
}
} else {
defer close(p.readLoopDone)
}
defer p.cancelReader.Close() //nolint:errcheck
// Handle resize events.
resizeLoopDone := p.handleResize()
// Process commands.
cmdLoopDone := p.handleCommands(cmds)
// Run event loop, handle updates and draw.
model, err := p.eventLoop(model, cmds)
// Tear down.
cancelContext()
// Wait for input loop to finish.
if p.cancelReader.Cancel() {
p.waitForReadLoop()
}
<-cmdLoopDone
<-resizeLoopDone
<-sigintLoopDone
<-initSignalDone
p.shutdown(false)
return model, err
}
// Start initializes the program. Ignores the final model. // Start initializes the program. Ignores the final model.
func (p *Program) Start() error { func (p *Program) Start() error {
_, err := p.StartReturningModel() _, err := p.StartReturningModel()

4
tty.go
View File

@ -51,12 +51,12 @@ func (p *Program) initCancelReader() error {
} }
p.readLoopDone = make(chan struct{}) p.readLoopDone = make(chan struct{})
go p.eventLoop() go p.readLoop()
return nil return nil
} }
func (p *Program) eventLoop() { func (p *Program) readLoop() {
defer close(p.readLoopDone) defer close(p.readLoopDone)
for { for {