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:
parent
0e620cf3ca
commit
7ceec7a572
51
termdash.go
51
termdash.go
@ -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 {
|
||||
|
261
termdash_test.go
261
termdash_test.go
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user