Merge branch '3cb-add-synchronization-channels'

This commit is contained in:
Oliver 2018-10-28 13:43:04 +01:00
commit 5c44db199f
5 changed files with 225 additions and 82 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,13 +46,27 @@ 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
// 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{}
return &Application{
events: make(chan tcell.Event, queueSize),
updates: make(chan func(), queueSize),
redraw: make(chan struct{}, queueSize),
suspendToken: make(chan struct{}, 1),
}
}
// SetInputCapture sets a function which captures all key events before they are
@ -134,65 +150,109 @@ 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.
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
}
// 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
}
}
}()
// Start event loop.
EventLoop:
for {
// Do not poll events during suspend mode
a.suspendMutex.Lock()
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
a.suspendMutex.Unlock()
break
}
select {
case event := <-a.events:
if event == nil {
break EventLoop
}
// Wait for next event.
event := a.screen.PollEvent()
a.suspendMutex.Unlock()
if event == nil {
// 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()
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.
// Intercept keys.
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
break EventLoop // 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()
// 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 *tcell.EventResize:
a.RLock()
screen := a.screen
a.RUnlock()
screen.Clear()
a.Draw()
// If we have updates, now is the time to execute them.
case updater := <-a.updates:
updater()
// If a redraw is requested, do it now.
case <-a.redraw:
a.draw()
}
}
a.running = false
close(a.suspendToken)
wg.Wait()
return nil
}
@ -200,12 +260,13 @@ func (a *Application) Run() error {
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
@ -216,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()
@ -255,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
@ -264,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()
@ -404,3 +468,24 @@ 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.
func (a *Application) QueueUpdate(f func()) *Application {
a.updates <- f
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
}

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()