Fix a race where artifacts could print when exiting a program

This commit also consolidates the exit operations for consistency's
sake, and adds a kill() method to renderers for stopping them without
performing any final rendering.
This commit is contained in:
Christian Rocha 2021-05-19 21:35:36 -04:00
parent 85ab476698
commit 1f773e8f20
4 changed files with 46 additions and 16 deletions

View File

@ -4,6 +4,7 @@ type nilRenderer struct{}
func (n nilRenderer) start() {}
func (n nilRenderer) stop() {}
func (n nilRenderer) kill() {}
func (n nilRenderer) write(v string) {}
func (n nilRenderer) repaint() {}
func (n nilRenderer) altScreen() bool { return false }

View File

@ -2,10 +2,26 @@ package tea
// renderer is the interface for Bubble Tea renderers.
type renderer interface {
// Start the renderer.
start()
// Stop the renderer, but render the final frame in the buffer, if any.
stop()
// Stop the renderer without doing any final rendering.
kill()
// Write a frame to the renderer. The renderer can write this data to ouput
// at its discretion.
write(string)
// Request a full re-render.
repaint()
// Whether or not the alternate screen buffer is enabled.
altScreen() bool
// Record internally that the alternate screen buffer is enabled. This does
// should not actually toggle the alternate screen buffer.
setAltScreen(bool)
}

View File

@ -61,13 +61,19 @@ func (r *standardRenderer) start() {
go r.listen()
}
// stop permanently halts the renderer.
// stop permanently halts the renderer, rendering the final frame.
func (r *standardRenderer) stop() {
r.flush()
clearLine(r.out)
close(r.done)
}
// kill halts the renderer. The final frame will not be rendered.
func (r *standardRenderer) kill() {
clearLine(r.out)
close(r.done)
}
// listen waits for ticks on the ticker, or a signal to stop the renderer.
func (r *standardRenderer) listen() {
for {

37
tea.go
View File

@ -212,7 +212,8 @@ type Program struct {
// treated as bits. These options can be set via various ProgramOptions.
startupOptions startupOptions
mtx *sync.Mutex
mtx *sync.Mutex
done chan struct{}
output io.Writer // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
@ -370,6 +371,7 @@ func NewProgram(model Model, opts ...ProgramOption) *Program {
initialModel: model,
output: os.Stdout,
input: os.Stdin,
done: make(chan struct{}),
CatchPanics: true,
}
@ -387,7 +389,6 @@ func (p *Program) Start() error {
cmds = make(chan Cmd)
msgs = make(chan Msg)
errs = make(chan error)
done = make(chan struct{})
// If output is a file (e.g. os.Stdout) then this will be set
// accordingly. Most of the time you should refer to p.outputIsTTY
@ -441,9 +442,7 @@ func (p *Program) Start() error {
if p.CatchPanics {
defer func() {
if r := recover(); r != nil {
p.ExitAltScreen()
p.DisableMouseCellMotion()
p.DisableMouseAllMotion()
p.shutdown(true)
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
return
@ -458,9 +457,6 @@ func (p *Program) Start() error {
if err != nil {
return err
}
defer func() {
_ = p.restoreTerminal()
}()
}
// If no renderer is set use the standard one.
@ -529,7 +525,7 @@ func (p *Program) Start() error {
go func() {
for {
select {
case <-done:
case <-p.done:
return
case cmd := <-cmds:
if cmd != nil {
@ -545,18 +541,14 @@ func (p *Program) Start() error {
for {
select {
case err := <-errs:
close(done)
p.shutdown(false)
return err
case msg := <-msgs:
// Handle special internal messages
switch msg := msg.(type) {
case quitMsg:
p.ExitAltScreen()
p.DisableMouseCellMotion()
p.DisableMouseAllMotion()
p.renderer.stop()
close(done)
p.shutdown(false)
return nil
case batchMsg:
@ -601,6 +593,21 @@ func (p *Program) Start() error {
}
}
// shutdown performs operations to free up resources and restore the terminal
// to its original state.
func (p *Program) shutdown(kill bool) {
if kill {
p.renderer.kill()
} else {
p.renderer.stop()
}
close(p.done)
p.ExitAltScreen()
p.DisableMouseCellMotion()
p.DisableMouseAllMotion()
_ = p.restoreTerminal()
}
// EnterAltScreen enters the alternate screen buffer, which consumes the entire
// terminal window. ExitAltScreen will return the terminal to its former state.
//