1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Support manually triggered redraw.

This can be a more viable option for some users as it is less resource
intensive. The user knows when the data in any of the widget changed and
thus can trigger a redraw. External events still redraw the terminal
immediately.

Done here:
- Added a controller for the manually triggered redraws.
- Added an example of using termdash with triggered redraw.
- Removing couple of races in the test by waiting for the event queue to
empty before comparing values and by moving the test of terminal resize
into the test that triggers redraws.
This commit is contained in:
Jakub Sobon 2018-06-18 20:13:20 +01:00
parent 0e620cf3ca
commit 7ceec7a572
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
2 changed files with 290 additions and 22 deletions

View File

@ -25,6 +25,7 @@ package termdash
import (
"context"
"errors"
"fmt"
"sync"
"time"
@ -51,7 +52,8 @@ func (o option) set(td *termdash) {
}
// RedrawInterval sets how often termdash redraws the container and all the widgets.
// Defaults to DefaultRedrawInterval.
// Defaults to DefaultRedrawInterval. Use the controller to disable the
// periodic redraw.
func RedrawInterval(t time.Duration) Option {
return option(func(td *termdash) {
td.redrawInterval = t
@ -88,6 +90,8 @@ func MouseSubscriber(f func(*terminalapi.Mouse)) Option {
}
// Run runs the terminal dashboard with the provided container on the terminal.
// Redraws the terminal periodically. If you prefer a manual redraw, use the
// Controller instead.
// Blocks until the context expires.
func Run(ctx context.Context, t terminalapi.Terminal, c *container.Container, opts ...Option) error {
td := newTermdash(t, c, opts...)
@ -96,6 +100,51 @@ func Run(ctx context.Context, t terminalapi.Terminal, c *container.Container, op
return td.start(ctx)
}
// Controller controls a termdash instance.
// The controller instance is only valid until Close() is called.
// The controller is not thread-safe.
type Controller struct {
td *termdash
cancel context.CancelFunc
}
// NewController initializes termdash and returns an instance of the controller.
// Periodic redrawing is disabled when using the controller, the RedrawInterval
// option is ignored.
// Close the controller when it isn't needed anymore.
func NewController(t terminalapi.Terminal, c *container.Container, opts ...Option) (*Controller, error) {
ctx, cancel := context.WithCancel(context.Background())
ctrl := &Controller{
td: newTermdash(t, c, opts...),
cancel: cancel,
}
// stops when Close() is called.
go ctrl.td.processEvents(ctx)
if err := ctrl.td.periodicRedraw(); err != nil {
return nil, err
}
return ctrl, nil
}
// Redraw triggers redraw of the terminal.
func (c *Controller) Redraw() error {
if c.td == nil {
return errors.New("the termdash instance is no longer running, this controller is now invalid")
}
c.td.mu.Lock()
defer c.td.mu.Unlock()
return c.td.redraw()
}
// Close closes the Controller and its termdash instance.
func (c *Controller) Close() {
c.cancel()
c.td.stop()
c.td = nil
}
// termdash is a terminal based dashboard.
// This object is thread-safe.
type termdash struct {

View File

@ -34,7 +34,7 @@ import (
"github.com/mum4k/termdash/widgets/fakewidget"
)
// Example shows how to setup and run termdash.
// Example shows how to setup and run termdash with periodic redraw.
func Example() {
// Create the terminal.
t, err := termbox.New()
@ -70,6 +70,41 @@ func Example() {
}
}
// Example shows how to setup and run termdash with manually triggered redraw.
func Example_triggered() {
// Create the terminal.
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()
wOpts := widgetapi.Options{
MinimumSize: fakewidget.MinimumSize,
WantKeyboard: true,
WantMouse: true,
}
// Create the container with a widget.
c := container.New(
t,
container.PlaceWidget(fakewidget.New(wOpts)),
)
// Create the controller and disable periodic redraw.
ctrl, err := NewController(t, c)
if err != nil {
panic(err)
}
// Close the controller and termdash once it isn't required anymore.
defer ctrl.Close()
// Redraw the terminal manually.
if err := ctrl.Redraw(); err != nil {
panic(err)
}
}
// errorHandler just stores the last error received.
type errorHandler struct {
err error
@ -143,26 +178,6 @@ func TestRun(t *testing.T) {
},
wantErr: true,
},
{
desc: "resizes the terminal",
size: image.Point{60, 10},
opts: []Option{
RedrawInterval(1),
},
events: []terminalapi.Event{
&terminalapi.Resize{Size: image.Point{70, 10}},
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(image.Point{70, 10})
fakewidget.MustDraw(
ft,
testcanvas.MustNew(ft.Area()),
widgetapi.Options{},
)
return ft
},
},
{
desc: "forwards mouse events to container",
size: image.Point{60, 10},
@ -340,9 +355,213 @@ func TestRun(t *testing.T) {
t.Errorf("after => unexpected error: %v", err)
}
}
if err := untilEmpty(5*time.Second, eq); err != nil {
t.Fatalf("untilEmpty => %v", err)
}
if diff := faketerm.Diff(tc.want(got.Size()), got); diff != "" {
t.Errorf("Run => %v", diff)
}
})
}
}
func TestController(t *testing.T) {
tests := []struct {
desc string
size image.Point
opts []Option
events []terminalapi.Event
apiEvents func(*fakewidget.Mirror) // Calls to the API of the widget.
controls func(*Controller) error
want func(size image.Point) *faketerm.Terminal
wantErr bool
}{
{
desc: "event triggers a redraw",
size: image.Point{60, 10},
events: []terminalapi.Event{
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
fakewidget.MustDraw(
ft,
testcanvas.MustNew(ft.Area()),
widgetapi.Options{
WantKeyboard: true,
WantMouse: true,
},
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
)
return ft
},
},
{
desc: "controller triggers redraw",
size: image.Point{60, 10},
apiEvents: func(mi *fakewidget.Mirror) {
mi.Text("hello")
},
controls: func(ctrl *Controller) error {
return ctrl.Redraw()
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
mirror := fakewidget.New(widgetapi.Options{})
mirror.Text("hello")
fakewidget.MustDrawWithMirror(
mirror,
ft,
testcanvas.MustNew(ft.Area()),
)
return ft
},
},
{
desc: "ignores periodic redraw via the controller",
size: image.Point{60, 10},
opts: []Option{
RedrawInterval(1),
},
apiEvents: func(mi *fakewidget.Mirror) {
mi.Text("hello")
},
controls: func(ctrl *Controller) error {
return nil
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
fakewidget.MustDraw(
ft,
testcanvas.MustNew(ft.Area()),
widgetapi.Options{},
)
return ft
},
},
{
desc: "does not redraw unless triggered when periodic disabled",
size: image.Point{60, 10},
apiEvents: func(mi *fakewidget.Mirror) {
mi.Text("hello")
},
controls: func(ctrl *Controller) error {
return nil
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
fakewidget.MustDraw(
ft,
testcanvas.MustNew(ft.Area()),
widgetapi.Options{},
)
return ft
},
},
{
desc: "fails when redraw fails",
size: image.Point{1, 1},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
desc: "resizes the terminal",
size: image.Point{60, 10},
events: []terminalapi.Event{
&terminalapi.Resize{Size: image.Point{70, 10}},
},
controls: func(ctrl *Controller) error {
return ctrl.Redraw()
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(image.Point{70, 10})
fakewidget.MustDraw(
ft,
testcanvas.MustNew(ft.Area()),
widgetapi.Options{},
)
return ft
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
eq := eventqueue.New()
for _, ev := range tc.events {
eq.Push(ev)
}
got, err := faketerm.New(tc.size, faketerm.WithEventQueue(eq))
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
mi := fakewidget.New(widgetapi.Options{
WantKeyboard: true,
WantMouse: true,
})
cont := container.New(
got,
container.PlaceWidget(mi),
)
ctrl, err := NewController(got, cont, tc.opts...)
if (err != nil) != tc.wantErr {
t.Errorf("NewController => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
defer ctrl.Close()
if tc.apiEvents != nil {
tc.apiEvents(mi)
}
if tc.controls != nil {
if err := tc.controls(ctrl); err != nil {
t.Errorf("controls => unexpected error: %v", err)
}
}
if err := untilEmpty(5*time.Second, eq); err != nil {
t.Fatalf("untilEmpty => %v", err)
}
if diff := faketerm.Diff(tc.want(got.Size()), got); diff != "" {
t.Errorf("Run => %v", diff)
}
})
}
}
// untilEmpty waits until the queue empties.
// Waits at most the specified duration.
func untilEmpty(timeout time.Duration, q *eventqueue.Unbound) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
tick := time.NewTimer(5 * time.Millisecond)
defer tick.Stop()
for {
select {
case <-tick.C:
if q.Empty() {
return nil
}
case <-ctx.Done():
return fmt.Errorf("while waiting for the event queue to empty: %v", ctx.Err())
}
}
}