Bugfixes/improvements to PR #172.

This commit is contained in:
Oliver 2018-10-28 13:42:49 +01:00
parent d830c42f6b
commit c22d5570be
5 changed files with 173 additions and 68 deletions

View File

@ -65,6 +65,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.19 (2018-10-28)
- Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
- v0.18 (2018-10-18)
- `InputField` elements can now be navigated freely.
- v0.17 (2018-06-20)

View File

@ -1,13 +1,14 @@
package tview
import (
"fmt"
"os"
"sync"
"github.com/gdamore/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
@ -19,7 +20,8 @@ type Application struct {
// The application's screen.
screen tcell.Screen
// Indicates whether the application's screen is currently active.
// 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.
@ -44,21 +46,26 @@ type Application struct {
// 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
// Functions queued from goroutines, used to serialize updates to primitives.
updates chan func()
// Redraw requests.
redraw chan struct{}
// 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{
events: make(chan tcell.Event, 100),
updates: make(chan func(), 100),
events: make(chan tcell.Event, queueSize),
updates: make(chan func(), queueSize),
redraw: make(chan struct{}, queueSize),
suspendToken: make(chan struct{}, 1),
}
}
@ -143,50 +150,70 @@ func (a *Application) Run() error {
// Draw the screen for the first time.
a.Unlock()
a.Draw()
a.draw()
// Separate loop to wait for screen events
// 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() {
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)
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
}
// 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.
a.QueueEvent(screen.PollEvent())
a.suspendMutex.Unlock()
}
}()
// Start event loop.
loop:
EventLoop:
for {
select {
case event := <-a.events:
if event == nil {
// The screen was finalized. Exit the loop.
break loop
break EventLoop
}
switch event := event.(type) {
case *tcell.EventKey:
a.RLock()
p := a.focus
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys.
if a.inputCapture != nil {
event = a.inputCapture(event)
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
break loop // Don't forward event.
break EventLoop // Don't forward event.
}
}
@ -201,7 +228,7 @@ loop:
handler(event, func(p Primitive) {
a.SetFocus(p)
})
a.Draw()
a.draw()
}
}
case *tcell.EventResize:
@ -209,16 +236,23 @@ loop:
screen := a.screen
a.RUnlock()
screen.Clear()
a.Draw()
a.draw()
}
// If we have updates, now is the time to execute them.
case updater := <-a.updates:
updater()
a.Draw()
}
// If a redraw is requested, do it now.
case <-a.redraw:
a.draw()
}
}
a.running = false
close(a.suspendToken)
wg.Wait()
return nil
}
@ -226,12 +260,13 @@ loop:
func (a *Application) Stop() {
a.Lock()
defer a.Unlock()
if a.screen == nil {
screen := a.screen
if screen == nil {
return
}
a.screen.Fini()
a.screen = nil
a.running = false
screen.Fini()
// a.running is still true, the main loop will clean up.
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
@ -242,32 +277,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.RLock()
a.Lock()
if a.screen == nil {
screen := a.screen
if screen == nil {
// Screen has not yet been initialized.
a.RUnlock()
a.Unlock()
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)
}
}()
// 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()
// 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()
@ -281,7 +310,9 @@ func (a *Application) Suspend(f func()) bool {
}
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
@ -290,6 +321,13 @@ func (a *Application) Suspend(f func()) bool {
// 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 {
// We actually just queue this draw.
a.redraw <- struct{}{}
return a
}
// draw actually does what Draw() promises to do.
func (a *Application) draw() *Application {
a.Lock()
defer a.Unlock()
@ -431,14 +469,23 @@ func (a *Application) GetFocus() Primitive {
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
// 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.
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
// 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
}

View File

@ -34,10 +34,13 @@ func TextView1(nextSlide func()) (title string, content tview.Primitive) {
textView := tview.NewTextView().
SetTextColor(tcell.ColorYellow).
SetScrollable(false).
SetChangedFunc(func() {
SetDoneFunc(func(key tcell.Key) {
nextSlide()
})
textView.SetChangedFunc(func() {
if textView.HasFocus() {
app.Draw()
}).SetDoneFunc(func(key tcell.Key) {
nextSlide()
}
})
go func() {
var n int

21
doc.go
View File

@ -137,6 +137,27 @@ Unicode Support
This package supports unicode characters including wide characters.
Concurrency
Many functions in this package are not thread-safe. For many applications, this
may not be an issue: If your code makes changes in response to key events, it
will execute in the main goroutine and thus will not cause any race conditions.
If you access your primitives from other goroutines, however, you will need to
synchronize execution. The easiest way to do this is to call
Application.QueueUpdate() (see its documentation for details):
go func() {
app.QueueUpdate(func() {
table.SetCellSimple(0, 0, "Foo bar")
app.Draw()
})
}()
One exception to this is the io.Writer interface implemented by TextView. You
can safely write to a TextView from any goroutine. See the TextView
documentation for details.
Type Hierarchy
All widgets listed above contain the Box type. All of Box's functions are

View File

@ -31,7 +31,7 @@ type textViewIndex struct {
// TextView is a box which displays text. It implements the io.Writer interface
// so you can stream text to it. This does not trigger a redraw automatically
// but if a handler is installed via SetChangedFunc(), you can cause it to be
// redrawn.
// redrawn. (See SetChangedFunc() for more details.)
//
// Navigation
//
@ -260,8 +260,20 @@ func (t *TextView) SetRegions(regions bool) *TextView {
}
// SetChangedFunc sets a handler function which is called when the text of the
// text view has changed. This is typically used to cause the application to
// redraw the screen.
// text view has changed. This is useful when text is written to this io.Writer
// in a separate goroutine. This does not automatically cause the screen to be
// refreshed so you may want to use the "changed" handler to redraw the screen.
//
// Note that to avoid race conditions or deadlocks, there are a few rules you
// should follow:
//
// - You can call Application.Draw() from this handler.
// - You can call TextView.HasFocus() from this handler.
// - During the execution of this handler, access to any other variables from
// this primitive or any other primitive should be queued using
// Application.QueueUpdate().
//
// See package description for details on dealing with concurrency.
func (t *TextView) SetChangedFunc(handler func()) *TextView {
t.changed = handler
return t
@ -441,13 +453,33 @@ func (t *TextView) GetRegionText(regionID string) string {
return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
}
// Focus is called when this primitive receives focus.
func (t *TextView) Focus(delegate func(p Primitive)) {
// Implemented here with locking because this is used by layout primitives.
t.Lock()
defer t.Unlock()
t.hasFocus = true
}
// HasFocus returns whether or not this primitive has focus.
func (t *TextView) HasFocus() bool {
// Implemented here with locking because this may be used in the "changed"
// callback.
t.Lock()
defer t.Unlock()
return t.hasFocus
}
// Write lets us implement the io.Writer interface. Tab characters will be
// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
// as a new line.
func (t *TextView) Write(p []byte) (n int, err error) {
// Notify at the end.
if t.changed != nil {
defer t.changed()
t.Lock()
changed := t.changed
t.Unlock()
if changed != nil {
defer changed() // Deadlocks may occur if we lock here.
}
t.Lock()