forked from Mirrors/bubbletea
chore: break up Start into several, more maintainable methods
This commit is contained in:
parent
0e76ba142a
commit
2696b2f339
297
tea.go
297
tea.go
|
@ -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
4
tty.go
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue