2
0
mirror of https://github.com/rivo/tview.git synced 2024-11-15 06:12:46 +00:00

Add logic to flush channels, and to safeguard adding of events, after main loop in Application.Run exited

This commit is contained in:
Brendan Bosman 2022-06-29 09:24:37 +02:00
parent 9994674d60
commit fc1e4b9f8f

View File

@ -1,6 +1,7 @@
package tview package tview
import ( import (
"context"
"sync" "sync"
"time" "time"
@ -66,6 +67,9 @@ type queuedUpdate struct {
type Application struct { type Application struct {
sync.RWMutex sync.RWMutex
runContext context.Context
runCancelFunc context.CancelFunc
// The application's screen. Apart from Run(), this variable should never be // The application's screen. Apart from Run(), this variable should never be
// set directly. Always use the screenReplacement channel after calling // set directly. Always use the screenReplacement channel after calling
// Fini(), to set a new screen (or nil to stop the application). // Fini(), to set a new screen (or nil to stop the application).
@ -120,9 +124,39 @@ type Application struct {
lastMouseButtons tcell.ButtonMask // The last mouse button state. lastMouseButtons tcell.ButtonMask // The last mouse button state.
} }
func (a *Application) Close() error {
a.runCancelFunc()
close(a.events)
close(a.screenReplacement)
close(a.updates)
// flush events channel
go func() {
for range a.events {
}
}()
// flush screenReplacement channel
go func() {
for range a.screenReplacement {
}
}()
// flush updates channel
go func() {
for up := range a.updates {
// important to set done for calling channel to be able to return
up.done <- struct{}{}
}
}()
return nil
}
// NewApplication creates and returns a new application. // NewApplication creates and returns a new application.
func NewApplication() *Application { func NewApplication() *Application {
cancelContext, cancelFunc := context.WithCancel(context.Background())
return &Application{ return &Application{
runContext: cancelContext,
runCancelFunc: cancelFunc,
events: make(chan tcell.Event, queueSize), events: make(chan tcell.Event, queueSize),
updates: make(chan queuedUpdate, queueSize), updates: make(chan queuedUpdate, queueSize),
screenReplacement: make(chan tcell.Screen, 1), screenReplacement: make(chan tcell.Screen, 1),
@ -188,7 +222,10 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
oldScreen := a.screen oldScreen := a.screen
a.Unlock() a.Unlock()
oldScreen.Fini() oldScreen.Fini()
a.screenReplacement <- screen // check to see if the Application.Run is still valid
if a.runContext.Err() == nil {
a.screenReplacement <- screen
}
return a return a
} }
@ -252,8 +289,15 @@ func (a *Application) Run() error {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
defer func() {
// call the runCancelFunc when exiting this function.
//This will stop the channels accepting any more events
a.runCancelFunc()
}()
defer wg.Done() defer wg.Done()
for {
// check to see if the Application.Run is still valid
for a.runContext.Err() == nil {
a.RLock() a.RLock()
screen := a.screen screen := a.screen
a.RUnlock() a.RUnlock()
@ -271,37 +315,47 @@ func (a *Application) Run() error {
continue continue
} }
// A screen was finalized (event is nil). Wait for a new scren. // A screen was finalized (event is nil). Wait for a new screen.
screen = <-a.screenReplacement var ok bool
if screen == nil { select {
// No new screen. We're done. // exit when runContext complete
a.QueueEvent(nil) case <-a.runContext.Done():
return return
} case screen, ok = <-a.screenReplacement:
if !ok || screen == nil {
// No new screen. We're done.
a.QueueEvent(nil)
return
}
// We have a new screen. Keep going. // We have a new screen. Keep going.
a.Lock() a.Lock()
a.screen = screen a.screen = screen
enableMouse := a.enableMouse enableMouse := a.enableMouse
a.Unlock() a.Unlock()
// Initialize and draw this screen. // Initialize and draw this screen.
if err := screen.Init(); err != nil { if err := screen.Init(); err != nil {
panic(err) panic(err)
}
if enableMouse {
screen.EnableMouse()
}
a.draw()
} }
if enableMouse {
screen.EnableMouse()
}
a.draw()
} }
}() }()
// Start event loop. // Start event loop.
EventLoop: EventLoop:
for { // check to see if the Application.Run is still valid
for a.runContext.Err() == nil {
select { select {
case event := <-a.events: // break loop when runContext complete
if event == nil { case <-a.runContext.Done():
break EventLoop
case event, ok := <-a.events:
if !ok || event == nil {
break EventLoop break EventLoop
} }
@ -348,9 +402,14 @@ EventLoop:
if redrawTimer != nil { if redrawTimer != nil {
redrawTimer.Stop() redrawTimer.Stop()
} }
redrawTimer = time.AfterFunc(redrawPause, func() { redrawTimer = time.AfterFunc(redrawPause,
a.events <- event func() {
}) // check to see if the Application.Run is still valid
if a.runContext.Err() == nil {
a.events <- event
}
},
)
} }
a.RLock() a.RLock()
screen := a.screen screen := a.screen
@ -376,13 +435,19 @@ EventLoop:
} }
// If we have updates, now is the time to execute them. // If we have updates, now is the time to execute them.
case update := <-a.updates: case update, ok := <-a.updates:
if !ok {
break EventLoop
}
update.f() update.f()
if update.done != nil { if update.done != nil {
update.done <- struct{}{} update.done <- struct{}{}
} }
} }
} }
// call the runCancelFunc when exiting eventLoop.
//This will stop the channels accepting any more events
a.runCancelFunc()
// Wait for the event loop to finish. // Wait for the event loop to finish.
wg.Wait() wg.Wait()
@ -500,7 +565,11 @@ func (a *Application) Stop() {
} }
a.screen = nil a.screen = nil
screen.Fini() screen.Fini()
a.screenReplacement <- nil
// check to see if the Application.Run is still valid
if a.runContext.Err() == nil {
a.screenReplacement <- nil
}
} }
// Suspend temporarily suspends the application by exiting terminal UI mode and // Suspend temporarily suspends the application by exiting terminal UI mode and
@ -618,15 +687,20 @@ func (a *Application) draw() *Application {
// corrupted so you may want to offer your users a keyboard shortcut to refresh // corrupted so you may want to offer your users a keyboard shortcut to refresh
// the screen. // the screen.
func (a *Application) Sync() *Application { func (a *Application) Sync() *Application {
a.updates <- queuedUpdate{f: func() { // check to see if the Application.Run is still valid
a.RLock() if a.runContext.Err() == nil {
screen := a.screen a.updates <- queuedUpdate{
a.RUnlock() f: func() {
if screen == nil { a.RLock()
return screen := a.screen
a.RUnlock()
if screen == nil {
return
}
screen.Sync()
},
} }
screen.Sync() }
}}
return a return a
} }
@ -741,9 +815,15 @@ func (a *Application) GetFocus() Primitive {
// //
// This function returns after f has executed. // This function returns after f has executed.
func (a *Application) QueueUpdate(f func()) *Application { func (a *Application) QueueUpdate(f func()) *Application {
ch := make(chan struct{}) // check to see if the Application.Run is still valid
a.updates <- queuedUpdate{f: f, done: ch} if a.runContext.Err() == nil {
<-ch ch := make(chan struct{})
a.updates <- queuedUpdate{
f: f,
done: ch,
}
<-ch
}
return a return a
} }
@ -761,6 +841,9 @@ func (a *Application) QueueUpdateDraw(f func()) *Application {
// //
// It is not recommended for event to be nil. // It is not recommended for event to be nil.
func (a *Application) QueueEvent(event tcell.Event) *Application { func (a *Application) QueueEvent(event tcell.Event) *Application {
a.events <- event // check to see if the Application.Run is still valid
if a.runContext.Err() == nil {
a.events <- event
}
return a return a
} }