aboutsummaryrefslogtreecommitdiff
path: root/vendor/maunium.net/go/tview/application.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/tview/application.go')
-rw-r--r--vendor/maunium.net/go/tview/application.go376
1 files changed, 260 insertions, 116 deletions
diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go
index 7e6da3a..ae4d7f8 100644
--- a/vendor/maunium.net/go/tview/application.go
+++ b/vendor/maunium.net/go/tview/application.go
@@ -1,24 +1,36 @@
package tview
import (
- "fmt"
- "os"
"sync"
"maunium.net/go/tcell"
)
+// The size of the event/update/redraw channels.
+const queueSize = 100
+
// Application represents the top node of an application.
//
// It is not strictly required to use this class as none of the other classes
// depend on it. However, it provides useful tools to set up an application and
// plays nicely with all widgets.
+//
+// The following command displays a primitive p on the screen until Ctrl-C is
+// pressed:
+//
+// if err := tview.NewApplication().SetRoot(p, true).Run(); err != nil {
+// panic(err)
+// }
type Application struct {
sync.RWMutex
// The application's screen.
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
@@ -48,13 +60,23 @@ type Application struct {
// was drawn.
afterDraw func(screen tcell.Screen)
- // If this value is true, the application has entered suspended mode.
- suspended bool
+ // Used to send screen events from separate goroutine to main event loop
+ events chan tcell.Event
+
+ // 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{}
}
// NewApplication creates and returns a new application.
func NewApplication() *Application {
- return &Application{}
+ return &Application{
+ events: make(chan tcell.Event, queueSize),
+ updates: make(chan func(), queueSize),
+ suspendToken: make(chan struct{}, 1),
+ }
}
// SetInputCapture sets a function which captures all key events before they are
@@ -97,140 +119,222 @@ func (a *Application) GetScreen() tcell.Screen {
return a.screen
}
+// 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).
+//
+// 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.
+func (a *Application) SetScreen(screen tcell.Screen) *Application {
+ a.Lock()
+ defer a.Unlock()
+ if a.running {
+ a.screen.Fini()
+ }
+ a.screen = screen
+ if a.running {
+ if err := a.screen.Init(); err != nil {
+ panic(err)
+ }
+ }
+ return a
+}
+
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
var err error
a.Lock()
- // Make a screen.
- a.screen, err = tcell.NewScreen()
- if err != nil {
- a.Unlock()
- return err
+ // Make a screen if there is none yet.
+ if a.screen == nil {
+ a.screen, err = tcell.NewScreen()
+ if err != nil {
+ a.Unlock()
+ return err
+ }
}
if err = a.screen.Init(); err != nil {
a.Unlock()
return err
}
a.screen.EnableMouse()
+ a.running = true
+
+ // We catch panics to clean up because they mess up the terminal.
+ defer func() {
+ if p := recover(); p != nil {
+ if a.screen != nil {
+ a.screen.Fini()
+ }
+ a.running = false
+ panic(p)
+ }
+ }()
// Draw the screen for the first time.
a.Unlock()
- a.Draw()
+ a.draw()
+
+ // 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. We might need to stop.
+ break
+ }
- // Start event loop.
- for {
- a.Lock()
- screen := a.screen
- if a.suspended {
- a.suspended = false // Clear previous suspended flag.
- }
- a.Unlock()
- if screen == nil {
- break
+ // 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
+ }
+
+ // We're in suspended mode (running is false). Pause and wait for new
+ // token.
+ break
+ }
}
+ }()
- // Wait for next event.
- event := a.screen.PollEvent()
- if event == nil {
- a.Lock()
- if a.suspended {
- // This screen was renewed due to suspended mode.
- a.suspended = false
- a.Unlock()
- continue // Resume.
+ // Start event loop.
+EventLoop:
+ for {
+ select {
+ case event := <-a.events:
+ if event == nil {
+ break EventLoop
}
- a.Unlock()
- // The screen was finalized. Exit the loop.
- break
- }
+ switch event := event.(type) {
+ case *tcell.EventKey:
+ a.RLock()
+ p := a.focus
+ inputCapture := a.inputCapture
+ a.RUnlock()
+
+ // Intercept keys.
+ if inputCapture != nil {
+ event = inputCapture(event)
+ if event == nil {
+ continue // Don't forward event.
+ }
+ }
- switch event := event.(type) {
- case *tcell.EventKey:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- // Intercept keys.
- if a.inputCapture != nil {
- event = a.inputCapture(event)
- if event == nil {
- break // Don't forward event.
+ // Ctrl-C closes the application.
+ if event.Key() == tcell.KeyCtrlC {
+ a.Stop()
}
- }
- // Pass other key events to the currently focused primitive.
- if p != nil {
- if handler := p.InputHandler(); handler != nil {
- handler(event, func(p Primitive) {
- a.SetFocus(p)
- })
- a.Draw()
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.InputHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ a.draw()
+ }
}
- }
- case *tcell.EventMouse:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- // Intercept keys.
- if a.mouseCapture != nil {
- event = a.mouseCapture(event)
- if event == nil {
- break // Don't forward event.
+
+ case *tcell.EventMouse:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ // Intercept keys.
+ if a.mouseCapture != nil {
+ event = a.mouseCapture(event)
+ if event == nil {
+ break // Don't forward event.
+ }
}
- }
- // Pass other key events to the currently focused primitive.
- if p != nil {
- if handler := p.MouseHandler(); handler != nil {
- handler(event, func(p Primitive) {
- a.SetFocus(p)
- })
- //a.Draw()
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.MouseHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ //a.Draw()
+ }
}
- }
- case *tcell.EventPaste:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- if a.pasteCapture != nil {
- event = a.pasteCapture(event)
- if event == nil {
- break
+ case *tcell.EventPaste:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ if a.pasteCapture != nil {
+ event = a.pasteCapture(event)
+ if event == nil {
+ break
+ }
}
- }
- if p != nil {
- if handler := p.PasteHandler(); handler != nil {
- handler(event)
- a.Draw()
+ if p != nil {
+ if handler := p.PasteHandler(); handler != nil {
+ handler(event)
+ a.Draw()
+ }
}
+ case *tcell.EventResize:
+ a.RLock()
+ screen := a.screen
+ a.RUnlock()
+ screen.Clear()
+ a.draw()
}
- case *tcell.EventResize:
- a.Lock()
- screen := a.screen
- a.Unlock()
- screen.Clear()
- a.Draw()
+
+ // If we have updates, now is the time to execute them.
+ case updater := <-a.updates:
+ updater()
}
}
+ a.running = false
+ close(a.suspendToken)
+ wg.Wait()
+
return nil
}
// Stop stops the application, causing Run() to return.
func (a *Application) Stop() {
- a.RLock()
- defer a.RUnlock()
- if a.screen == nil {
+ a.Lock()
+ defer a.Unlock()
+ screen := a.screen
+ if screen == nil {
return
}
- a.screen.Fini()
a.screen = nil
+ screen.Fini()
+ // a.running is still true, the main loop will clean up.
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
@@ -243,29 +347,24 @@ func (a *Application) Stop() {
func (a *Application) Suspend(f func()) bool {
a.Lock()
- if a.suspended || a.screen == nil {
- // Application is already suspended.
+ screen := a.screen
+ if screen == nil {
+ // Screen has not yet been initialized.
a.Unlock()
return false
}
- // Enter suspended mode.
- a.suspended = true
+ // Enter suspended mode. Make a new screen here already so our event loop can
+ // continue.
+ a.screen = nil
+ a.running = false
+ screen.Fini()
a.Unlock()
- a.Stop()
-
- // Deal with panics during suspended mode. Exit the program.
- defer func() {
- if p := recover(); p != nil {
- fmt.Println(p)
- os.Exit(1)
- }
- }()
// Wait for "f" to return.
f()
- // Make a new screen and redraw.
+ // Initialize our new screen and draw the contents.
a.Lock()
var err error
a.screen, err = tcell.NewScreen()
@@ -278,23 +377,36 @@ func (a *Application) Suspend(f func()) bool {
panic(err)
}
a.screen.EnableMouse()
+ a.running = true
a.Unlock()
- a.Draw()
+ a.draw()
+ a.suspendToken <- struct{}{}
+ // One key event will get lost, see https://github.com/gdamore/tcell/issues/194
// Continue application loop.
return true
}
-// Draw refreshes the screen. It calls the Draw() function of the application's
-// root primitive and then syncs the screen buffer.
+// Draw refreshes the screen (during the next update cycle). It calls the Draw()
+// function of the application's root primitive and then syncs the screen
+// buffer.
func (a *Application) Draw() *Application {
- a.RLock()
+ a.QueueUpdate(func() {
+ a.draw()
+ })
+ return a
+}
+
+// draw actually does what Draw() promises to do.
+func (a *Application) draw() *Application {
+ a.Lock()
+ defer a.Unlock()
+
screen := a.screen
root := a.root
fullscreen := a.rootFullscreen
before := a.beforeDraw
after := a.afterDraw
- a.RUnlock()
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
@@ -427,3 +539,35 @@ func (a *Application) GetFocus() Primitive {
defer a.RUnlock()
return a.focus
}
+
+// QueueUpdate is used to synchronize access to primitives from non-main
+// goroutines. The provided function will be executed as part of the event loop
+// and thus will not cause race conditions with other such update functions or
+// the Draw() function.
+//
+// Note that Draw() is not implicitly called after the execution of f as that
+// may not be desirable. You can call Draw() from f if the screen should be
+// refreshed after each update. Alternatively, use QueueUpdateDraw() to follow
+// up with an immediate refresh of the screen.
+func (a *Application) QueueUpdate(f func()) *Application {
+ a.updates <- f
+ return a
+}
+
+// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen
+// immediately after executing f.
+func (a *Application) QueueUpdateDraw(f func()) *Application {
+ a.QueueUpdate(func() {
+ f()
+ a.draw()
+ })
+ return a
+}
+
+// QueueEvent sends an event to the Application event loop.
+//
+// It is not recommended for event to be nil.
+func (a *Application) QueueEvent(event tcell.Event) *Application {
+ a.events <- event
+ return a
+}