|
|
|
@ -24,13 +24,11 @@ const queueSize = 100
|
|
|
|
|
type Application struct {
|
|
|
|
|
sync.RWMutex
|
|
|
|
|
|
|
|
|
|
// The application's screen.
|
|
|
|
|
// The application's screen. Apart from Run(), this variable should never be
|
|
|
|
|
// set directly. Always use the screenReplacement channel after calling
|
|
|
|
|
// Fini(), to set a new screen (or nil to stop the application).
|
|
|
|
|
screen tcell.Screen
|
|
|
|
|
|
|
|
|
|
// Indicates whether the application's screen is currently active. This is
|
|
|
|
|
// false during suspended mode.
|
|
|
|
|
running bool
|
|
|
|
|
|
|
|
|
|
// The primitive which currently has the keyboard focus.
|
|
|
|
|
focus Primitive
|
|
|
|
|
|
|
|
|
@ -59,16 +57,19 @@ type Application struct {
|
|
|
|
|
// Functions queued from goroutines, used to serialize updates to primitives.
|
|
|
|
|
updates chan func()
|
|
|
|
|
|
|
|
|
|
// A channel which signals the end of the suspended mode.
|
|
|
|
|
suspendToken chan struct{}
|
|
|
|
|
// An object that the screen variable will be set to after Fini() was called.
|
|
|
|
|
// Use this channel to set a new screen object for the application
|
|
|
|
|
// (screen.Init() and draw() will be called implicitly). A value of nil will
|
|
|
|
|
// stop the application.
|
|
|
|
|
screenReplacement chan tcell.Screen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewApplication creates and returns a new application.
|
|
|
|
|
func NewApplication() *Application {
|
|
|
|
|
return &Application{
|
|
|
|
|
events: make(chan tcell.Event, queueSize),
|
|
|
|
|
updates: make(chan func(), queueSize),
|
|
|
|
|
suspendToken: make(chan struct{}, 1),
|
|
|
|
|
events: make(chan tcell.Event, queueSize),
|
|
|
|
|
updates: make(chan func(), queueSize),
|
|
|
|
|
screenReplacement: make(chan tcell.Screen, 1),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -94,29 +95,29 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
|
|
|
|
|
|
|
|
|
|
// SetScreen allows you to provide your own tcell.Screen object. For most
|
|
|
|
|
// applications, this is not needed and you should be familiar with
|
|
|
|
|
// tcell.Screen when using this function. Run() will call Init() and Fini() on
|
|
|
|
|
// the provided screen object.
|
|
|
|
|
//
|
|
|
|
|
// This function is typically called before calling Run(). Calling it while an
|
|
|
|
|
// application is running will switch the application to the new screen. Fini()
|
|
|
|
|
// will be called on the old screen and Init() on the new screen (errors
|
|
|
|
|
// returned by Init() will lead to a panic).
|
|
|
|
|
// tcell.Screen when using this function.
|
|
|
|
|
//
|
|
|
|
|
// Note that calling Suspend() will invoke Fini() on your screen object and it
|
|
|
|
|
// will not be restored when suspended mode ends. Instead, a new default screen
|
|
|
|
|
// object will be created.
|
|
|
|
|
// This function is typically called before the first call to Run(). Init() need
|
|
|
|
|
// not be called on the screen.
|
|
|
|
|
func (a *Application) SetScreen(screen tcell.Screen) *Application {
|
|
|
|
|
a.Lock()
|
|
|
|
|
defer a.Unlock()
|
|
|
|
|
if a.running {
|
|
|
|
|
a.screen.Fini()
|
|
|
|
|
if screen == nil {
|
|
|
|
|
return a // Invalid input. Do nothing.
|
|
|
|
|
}
|
|
|
|
|
a.screen = screen
|
|
|
|
|
if a.running {
|
|
|
|
|
if err := a.screen.Init(); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.Lock()
|
|
|
|
|
if a.screen == nil {
|
|
|
|
|
// Run() has not been called yet.
|
|
|
|
|
a.screen = screen
|
|
|
|
|
a.Unlock()
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run() is already in progress. Exchange screen.
|
|
|
|
|
oldScreen := a.screen
|
|
|
|
|
a.Unlock()
|
|
|
|
|
oldScreen.Fini()
|
|
|
|
|
a.screenReplacement <- screen
|
|
|
|
|
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -133,12 +134,11 @@ func (a *Application) Run() error {
|
|
|
|
|
a.Unlock()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err = a.screen.Init(); err != nil {
|
|
|
|
|
a.Unlock()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err = a.screen.Init(); err != nil {
|
|
|
|
|
a.Unlock()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
a.running = true
|
|
|
|
|
|
|
|
|
|
// We catch panics to clean up because they mess up the terminal.
|
|
|
|
|
defer func() {
|
|
|
|
@ -146,7 +146,6 @@ func (a *Application) Run() error {
|
|
|
|
|
if a.screen != nil {
|
|
|
|
|
a.screen.Fini()
|
|
|
|
|
}
|
|
|
|
|
a.running = false
|
|
|
|
|
panic(p)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
@ -158,42 +157,44 @@ func (a *Application) Run() error {
|
|
|
|
|
// Separate loop to wait for screen events.
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
a.suspendToken <- struct{}{} // We need this to get started.
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
for range a.suspendToken {
|
|
|
|
|
for {
|
|
|
|
|
a.RLock()
|
|
|
|
|
screen := a.screen
|
|
|
|
|
a.RUnlock()
|
|
|
|
|
if screen == nil {
|
|
|
|
|
// We have no screen. Let's stop.
|
|
|
|
|
a.QueueEvent(nil)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
a.RLock()
|
|
|
|
|
screen := a.screen
|
|
|
|
|
a.RUnlock()
|
|
|
|
|
if screen == nil {
|
|
|
|
|
// We have no screen. Let's stop.
|
|
|
|
|
a.QueueEvent(nil)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for next event and queue it.
|
|
|
|
|
event := screen.PollEvent()
|
|
|
|
|
if event != nil {
|
|
|
|
|
// Regular event. Queue.
|
|
|
|
|
a.QueueEvent(event)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Wait for next event and queue it.
|
|
|
|
|
event := screen.PollEvent()
|
|
|
|
|
if event != nil {
|
|
|
|
|
// Regular event. Queue.
|
|
|
|
|
a.QueueEvent(event)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A screen was finalized (event is nil).
|
|
|
|
|
a.RLock()
|
|
|
|
|
running := a.running
|
|
|
|
|
a.RUnlock()
|
|
|
|
|
if running {
|
|
|
|
|
// The application was stopped. End the event loop.
|
|
|
|
|
a.QueueEvent(nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// A screen was finalized (event is nil). Wait for a new scren.
|
|
|
|
|
screen = <-a.screenReplacement
|
|
|
|
|
if screen == nil {
|
|
|
|
|
// No new screen. We're done.
|
|
|
|
|
a.QueueEvent(nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We're in suspended mode (running is false). Pause and wait for new
|
|
|
|
|
// token.
|
|
|
|
|
break
|
|
|
|
|
// We have a new screen. Keep going.
|
|
|
|
|
a.Lock()
|
|
|
|
|
a.screen = screen
|
|
|
|
|
a.Unlock()
|
|
|
|
|
|
|
|
|
|
// Initialize and draw this screen.
|
|
|
|
|
if err := screen.Init(); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
a.draw()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
@ -252,9 +253,9 @@ EventLoop:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.running = false
|
|
|
|
|
close(a.suspendToken)
|
|
|
|
|
// Wait for the event loop to finish.
|
|
|
|
|
wg.Wait()
|
|
|
|
|
a.screen = nil
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
@ -269,7 +270,7 @@ func (a *Application) Stop() {
|
|
|
|
|
}
|
|
|
|
|
a.screen = nil
|
|
|
|
|
screen.Fini()
|
|
|
|
|
// a.running is still true, the main loop will clean up.
|
|
|
|
|
a.screenReplacement <- nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Suspend temporarily suspends the application by exiting terminal UI mode and
|
|
|
|
@ -280,41 +281,26 @@ func (a *Application) Stop() {
|
|
|
|
|
// was called. If false is returned, the application was already suspended,
|
|
|
|
|
// terminal UI mode was not exited, and "f" was not called.
|
|
|
|
|
func (a *Application) Suspend(f func()) bool {
|
|
|
|
|
a.Lock()
|
|
|
|
|
|
|
|
|
|
a.RLock()
|
|
|
|
|
screen := a.screen
|
|
|
|
|
a.RUnlock()
|
|
|
|
|
if screen == nil {
|
|
|
|
|
// Screen has not yet been initialized.
|
|
|
|
|
a.Unlock()
|
|
|
|
|
return false
|
|
|
|
|
return false // Screen has not yet been initialized.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enter suspended mode. Make a new screen here already so our event loop can
|
|
|
|
|
// continue.
|
|
|
|
|
a.screen = nil
|
|
|
|
|
a.running = false
|
|
|
|
|
// Enter suspended mode.
|
|
|
|
|
screen.Fini()
|
|
|
|
|
a.Unlock()
|
|
|
|
|
|
|
|
|
|
// Wait for "f" to return.
|
|
|
|
|
f()
|
|
|
|
|
|
|
|
|
|
// Initialize our new screen and draw the contents.
|
|
|
|
|
a.Lock()
|
|
|
|
|
// Make a new screen.
|
|
|
|
|
var err error
|
|
|
|
|
a.screen, err = tcell.NewScreen()
|
|
|
|
|
screen, err = tcell.NewScreen()
|
|
|
|
|
if err != nil {
|
|
|
|
|
a.Unlock()
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
if err = a.screen.Init(); err != nil {
|
|
|
|
|
a.Unlock()
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
a.running = true
|
|
|
|
|
a.Unlock()
|
|
|
|
|
a.draw()
|
|
|
|
|
a.suspendToken <- struct{}{}
|
|
|
|
|
a.screenReplacement <- screen
|
|
|
|
|
// One key event will get lost, see https://github.com/gdamore/tcell/issues/194
|
|
|
|
|
|
|
|
|
|
// Continue application loop.
|
|
|
|
|