package tview
import (
"fmt"
"os"
"sync"
"github.com/gdamore/tcell"
)
// 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.
type Application struct {
sync . RWMutex
// The application's screen.
screen tcell . Screen
// Indicates whether the application's screen is currently active.
running bool
// The primitive which currently has the keyboard focus.
focus Primitive
// The root primitive to be seen on the screen.
root Primitive
// Whether or not the application resizes the root primitive.
rootFullscreen bool
// An optional capture function which receives a key event and returns the
// event to be forwarded to the default input handler (nil if nothing should
// be forwarded).
inputCapture func ( event * tcell . EventKey ) * tcell . EventKey
// An optional callback function which is invoked just before the root
// primitive is drawn.
beforeDraw func ( screen tcell . Screen ) bool
// An optional callback function which is invoked after the root primitive
// was drawn.
afterDraw func ( screen tcell . Screen )
// Halts the event loop during suspended mode.
suspendMutex sync . Mutex
// Used to send screen events from separate goroutine to main event loop
events chan tcell . Event
// Used to send primitive updates from separate goroutines to the main event loop
updates chan func ( )
}
// NewApplication creates and returns a new application.
func NewApplication ( ) * Application {
return & Application {
events : make ( chan tcell . Event , 100 ) ,
updates : make ( chan func ( ) , 100 ) ,
}
}
// SetInputCapture sets a function which captures all key events before they are
// forwarded to the key event handler of the primitive which currently has
// focus. This function can then choose to forward that key event (or a
// different one) by returning it or stop the key event processing by returning
// nil.
//
// Note that this also affects the default event handling of the application
// itself: Such a handler can intercept the Ctrl-C event which closes the
// applicatoon.
func ( a * Application ) SetInputCapture ( capture func ( event * tcell . EventKey ) * tcell . EventKey ) * Application {
a . inputCapture = capture
return a
}
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func ( a * Application ) GetInputCapture ( ) func ( event * tcell . EventKey ) * tcell . EventKey {
return a . inputCapture
}
// 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 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 . 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 ( )
// Separate loop to wait for screen events
go func ( ) {
for {
// Do not poll events during suspend mode
a . suspendMutex . Lock ( )
a . RLock ( )
screen := a . screen
a . RUnlock ( )
if screen == nil {
a . suspendMutex . Unlock ( )
// send signal to stop main event loop
a . QueueEvent ( nil )
break
}
// Wait for next event.
a . QueueEvent ( screen . PollEvent ( ) )
a . suspendMutex . Unlock ( )
}
} ( )
// Start event loop.
loop :
for {
select {
case event := <- a . events :
if event == nil {
// The screen was finalized. Exit the loop.
break loop
}
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 loop // 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 ( )
}
}
case * tcell . EventResize :
a . RLock ( )
screen := a . screen
a . RUnlock ( )
screen . Clear ( )
a . Draw ( )
}
case updater := <- a . updates :
updater ( )
a . Draw ( )
}
}
return nil
}
// Stop stops the application, causing Run() to return.
func ( a * Application ) Stop ( ) {
a . Lock ( )
defer a . Unlock ( )
if a . screen == nil {
return
}
a . screen . Fini ( )
a . screen = nil
a . running = false
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
// invoking the provided function "f". When "f" returns, terminal UI mode is
// entered again and the application resumes.
//
// A return value of true indicates that the application was suspended and "f"
// 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 . RLock ( )
if a . screen == nil {
// Screen has not yet been initialized.
a . RUnlock ( )
return false
}
// Enter suspended mode.
a . suspendMutex . Lock ( )
defer a . suspendMutex . Unlock ( )
a . RUnlock ( )
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.
a . Lock ( )
var err error
a . 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 ( )
// 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.
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
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
return a
}
// Resize if requested.
if fullscreen && root != nil {
width , height := screen . Size ( )
root . SetRect ( 0 , 0 , width , height )
}
// Call before handler if there is one.
if before != nil {
if before ( screen ) {
screen . Show ( )
return a
}
}
// Draw all primitives.
root . Draw ( screen )
// Call after handler if there is one.
if after != nil {
after ( screen )
}
// Sync screen.
screen . Show ( )
return a
}
// SetBeforeDrawFunc installs a callback function which is invoked just before
// the root primitive is drawn during screen updates. If the function returns
// true, drawing will not continue, i.e. the root primitive will not be drawn
// (and an after-draw-handler will not be called).
//
// Note that the screen is not cleared by the application. To clear the screen,
// you may call screen.Clear().
//
// Provide nil to uninstall the callback function.
func ( a * Application ) SetBeforeDrawFunc ( handler func ( screen tcell . Screen ) bool ) * Application {
a . beforeDraw = handler
return a
}
// GetBeforeDrawFunc returns the callback function installed with
// SetBeforeDrawFunc() or nil if none has been installed.
func ( a * Application ) GetBeforeDrawFunc ( ) func ( screen tcell . Screen ) bool {
return a . beforeDraw
}
// SetAfterDrawFunc installs a callback function which is invoked after the root
// primitive was drawn during screen updates.
//
// Provide nil to uninstall the callback function.
func ( a * Application ) SetAfterDrawFunc ( handler func ( screen tcell . Screen ) ) * Application {
a . afterDraw = handler
return a
}
// GetAfterDrawFunc returns the callback function installed with
// SetAfterDrawFunc() or nil if none has been installed.
func ( a * Application ) GetAfterDrawFunc ( ) func ( screen tcell . Screen ) {
return a . afterDraw
}
// SetRoot sets the root primitive for this application. If "fullscreen" is set
// to true, the root primitive's position will be changed to fill the screen.
//
// This function must be called at least once or nothing will be displayed when
// the application starts.
//
// It also calls SetFocus() on the primitive.
func ( a * Application ) SetRoot ( root Primitive , fullscreen bool ) * Application {
a . Lock ( )
a . root = root
a . rootFullscreen = fullscreen
if a . screen != nil {
a . screen . Clear ( )
}
a . Unlock ( )
a . SetFocus ( root )
return a
}
// ResizeToFullScreen resizes the given primitive such that it fills the entire
// screen.
func ( a * Application ) ResizeToFullScreen ( p Primitive ) * Application {
a . RLock ( )
width , height := a . screen . Size ( )
a . RUnlock ( )
p . SetRect ( 0 , 0 , width , height )
return a
}
// SetFocus sets the focus on a new primitive. All key events will be redirected
// to that primitive. Callers must ensure that the primitive will handle key
// events.
//
// Blur() will be called on the previously focused primitive. Focus() will be
// called on the new primitive.
func ( a * Application ) SetFocus ( p Primitive ) * Application {
a . Lock ( )
if a . focus != nil {
a . focus . Blur ( )
}
a . focus = p
if a . screen != nil {
a . screen . HideCursor ( )
}
a . Unlock ( )
if p != nil {
p . Focus ( func ( p Primitive ) {
a . SetFocus ( p )
} )
}
return a
}
// GetFocus returns the primitive which has the current focus. If none has it,
// nil is returned.
func ( a * Application ) GetFocus ( ) Primitive {
a . RLock ( )
defer a . RUnlock ( )
return a . focus
}
// QueueUpdate is used to synchronize changes to primitives by carrying an update function from separate goroutine to the Application event loop via channel
func ( a * Application ) QueueUpdate ( f func ( ) ) * Application {
a . updates <- f
return a
}
// QueueEvent takes an Event instance and sends it to the Application event loop via channel
func ( a * Application ) QueueEvent ( e tcell . Event ) * Application {
a . events <- e
return a
}