mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
Implementing the infrastructure layer.
This commit is contained in:
parent
0199ffbb9a
commit
6fe129c940
@ -65,7 +65,8 @@ guidelines](doc/design_guidelines.md) before contributing.
|
|||||||
- [x] Implement the input event pre-processing.
|
- [x] Implement the input event pre-processing.
|
||||||
- [x] Add support for tracking mouse and keyboard focus.
|
- [x] Add support for tracking mouse and keyboard focus.
|
||||||
- [x] Implement the first widget.
|
- [x] Implement the first widget.
|
||||||
- [ ] Implement the infrastructure layer.
|
- [x] Implement the infrastructure layer.
|
||||||
- [ ] Write the design guidelines document.
|
- [ ] Write the design guidelines document.
|
||||||
- [ ] Documentation and tooling for widget development.
|
- [ ] Documentation and tooling for widget development.
|
||||||
|
- [ ] Implement the first widget.
|
||||||
- [ ] Launch and iterate.
|
- [ ] Launch and iterate.
|
||||||
|
@ -54,12 +54,10 @@ func Example() {
|
|||||||
),
|
),
|
||||||
Right(
|
Right(
|
||||||
Border(draw.LineStyleLight),
|
Border(draw.LineStyleLight),
|
||||||
|
PlaceWidget(fakewidget.New(widgetapi.Options{})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(mum4k): Allow splits on different ratios.
|
|
||||||
// TODO(mum4k): Include an example with a widget.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mum4k/termdash"
|
||||||
"github.com/mum4k/termdash/container"
|
"github.com/mum4k/termdash/container"
|
||||||
"github.com/mum4k/termdash/draw"
|
"github.com/mum4k/termdash/draw"
|
||||||
"github.com/mum4k/termdash/terminal/termbox"
|
"github.com/mum4k/termdash/terminal/termbox"
|
||||||
@ -45,21 +46,6 @@ func inputEvents(ctx context.Context, t terminalapi.Terminal, c *container.Conta
|
|||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// redraw redraws the containers on the terminal.
|
|
||||||
func redraw(t terminalapi.Terminal, c *container.Container) error {
|
|
||||||
//if err := t.Clear(); err != nil {
|
|
||||||
// return err
|
|
||||||
//}
|
|
||||||
if err := c.Draw(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.Flush(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
t, err := termbox.New()
|
t, err := termbox.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,6 +54,7 @@ func main() {
|
|||||||
defer t.Close()
|
defer t.Close()
|
||||||
|
|
||||||
wOpts := widgetapi.Options{
|
wOpts := widgetapi.Options{
|
||||||
|
MinimumSize: fakewidget.MinimumSize,
|
||||||
WantKeyboard: true,
|
WantKeyboard: true,
|
||||||
WantMouse: true,
|
WantMouse: true,
|
||||||
}
|
}
|
||||||
@ -79,6 +66,7 @@ func main() {
|
|||||||
container.Top(
|
container.Top(
|
||||||
container.Border(draw.LineStyleLight),
|
container.Border(draw.LineStyleLight),
|
||||||
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
||||||
|
MinimumSize: fakewidget.MinimumSize,
|
||||||
WantKeyboard: true,
|
WantKeyboard: true,
|
||||||
WantMouse: true,
|
WantMouse: true,
|
||||||
Ratio: image.Point{5, 1},
|
Ratio: image.Point{5, 1},
|
||||||
@ -102,6 +90,7 @@ func main() {
|
|||||||
container.Border(draw.LineStyleLight),
|
container.Border(draw.LineStyleLight),
|
||||||
container.VerticalAlignMiddle(),
|
container.VerticalAlignMiddle(),
|
||||||
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
||||||
|
MinimumSize: fakewidget.MinimumSize,
|
||||||
WantKeyboard: true,
|
WantKeyboard: true,
|
||||||
WantMouse: true,
|
WantMouse: true,
|
||||||
Ratio: image.Point{2, 1},
|
Ratio: image.Point{2, 1},
|
||||||
@ -126,36 +115,8 @@ func main() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := redraw(t, c); err != nil {
|
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
if err := termdash.Run(ctx, t, c); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
events := inputEvents(context.Background(), t, c)
|
|
||||||
redrawTimer := time.NewTicker(100 * time.Millisecond)
|
|
||||||
defer redrawTimer.Stop()
|
|
||||||
|
|
||||||
const exitTime = 10 * time.Second
|
|
||||||
exitTimer := time.NewTicker(exitTime)
|
|
||||||
|
|
||||||
for {
|
|
||||||
defer exitTimer.Stop()
|
|
||||||
select {
|
|
||||||
case ev := <-events:
|
|
||||||
switch e := ev.(type) {
|
|
||||||
case *terminalapi.Mouse:
|
|
||||||
c.Mouse(e)
|
|
||||||
case *terminalapi.Keyboard:
|
|
||||||
c.Keyboard(e)
|
|
||||||
}
|
|
||||||
exitTimer.Stop()
|
|
||||||
exitTimer = time.NewTicker(exitTime)
|
|
||||||
|
|
||||||
case <-redrawTimer.C:
|
|
||||||
if err := redraw(t, c); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
case <-exitTimer.C:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
278
termdash.go
Normal file
278
termdash.go
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
Package termdash implements a terminal based dashboard.
|
||||||
|
|
||||||
|
While running, the terminal dashboard performs the following:
|
||||||
|
- Periodic redrawing of the canvas and all the widgets.
|
||||||
|
- Event based redrawing of the widgets (i.e. on Keyboard or Mouse events).
|
||||||
|
- Forwards input events to widgets and optional subscribers.
|
||||||
|
- Handles terminal resize events.
|
||||||
|
*/
|
||||||
|
package termdash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mum4k/termdash/container"
|
||||||
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultRedrawInterval is the default for the RedrawInterval option.
|
||||||
|
const DefaultRedrawInterval = 250 * time.Millisecond
|
||||||
|
|
||||||
|
// Option is used to provide options.
|
||||||
|
type Option interface {
|
||||||
|
// set sets the provided option.
|
||||||
|
set(td *termdash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// option implements Option.
|
||||||
|
type option func(td *termdash)
|
||||||
|
|
||||||
|
// set implements Option.set.
|
||||||
|
func (o option) set(td *termdash) {
|
||||||
|
o(td)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedrawInterval sets how often termdash redraws the container and all the widgets.
|
||||||
|
// Defaults to DefaultRedrawInterval.
|
||||||
|
func RedrawInterval(t time.Duration) Option {
|
||||||
|
return option(func(td *termdash) {
|
||||||
|
td.redrawInterval = t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputErrorHandler is used to provide a function that will be called with all
|
||||||
|
// input event errors. If not provided, input event errors are discarded.
|
||||||
|
func InputErrorHandler(f func(error)) Option {
|
||||||
|
return option(func(td *termdash) {
|
||||||
|
td.inputErrorHandler = f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardSubscriber registers a subscriber for Keyboard events. Each
|
||||||
|
// keyboard event is forwarded to the container and the registered subscriber.
|
||||||
|
// The provided function must be non-blocking, ideally just storing the value
|
||||||
|
// and returning as termdash blocks on each subscriber.
|
||||||
|
func KeyboardSubscriber(f func(*terminalapi.Keyboard)) Option {
|
||||||
|
return option(func(td *termdash) {
|
||||||
|
td.keyboardSubscriber = f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseSubscriber registers a subscriber for Mouse events. Each mouse event
|
||||||
|
// is forwarded to the container and the registered subscriber.
|
||||||
|
// The provided function must be non-blocking, ideally just storing the value
|
||||||
|
// and returning as termdash blocks on each subscriber.
|
||||||
|
func MouseSubscriber(f func(*terminalapi.Mouse)) Option {
|
||||||
|
return option(func(td *termdash) {
|
||||||
|
td.mouseSubscriber = f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs the terminal dashboard with the provided container on the terminal.
|
||||||
|
// Blocks until the context expires.
|
||||||
|
func Run(ctx context.Context, t terminalapi.Terminal, c *container.Container, opts ...Option) error {
|
||||||
|
td := newTermdash(t, c, opts...)
|
||||||
|
defer td.stop()
|
||||||
|
|
||||||
|
return td.start(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// termdash is a terminal based dashboard.
|
||||||
|
// This object is thread-safe.
|
||||||
|
type termdash struct {
|
||||||
|
// term is the terminal the dashboard runs on.
|
||||||
|
term terminalapi.Terminal
|
||||||
|
|
||||||
|
// container maintains terminal splits and places widgets.
|
||||||
|
container *container.Container
|
||||||
|
|
||||||
|
// closeCh gets closed when Stop() is called, which tells the event
|
||||||
|
// collecting goroutine to exit.
|
||||||
|
closeCh chan struct{}
|
||||||
|
// exitCh gets closed when the event collecting goroutine actually exits.
|
||||||
|
exitCh chan struct{}
|
||||||
|
|
||||||
|
// clearNeeded indicates if the terminal needs to be cleared next time
|
||||||
|
// we're drawing it. Terminal needs to be cleared if its sized changed.
|
||||||
|
clearNeeded bool
|
||||||
|
|
||||||
|
// mu protects termdash.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// Options.
|
||||||
|
redrawInterval time.Duration
|
||||||
|
inputErrorHandler func(error)
|
||||||
|
mouseSubscriber func(*terminalapi.Mouse)
|
||||||
|
keyboardSubscriber func(*terminalapi.Keyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTermdash creates a new termdash.
|
||||||
|
func newTermdash(t terminalapi.Terminal, c *container.Container, opts ...Option) *termdash {
|
||||||
|
td := &termdash{
|
||||||
|
term: t,
|
||||||
|
container: c,
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
exitCh: make(chan struct{}),
|
||||||
|
redrawInterval: DefaultRedrawInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.set(td)
|
||||||
|
}
|
||||||
|
return td
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputError forwards the input error to the error handler if one was
|
||||||
|
// provided.
|
||||||
|
func (td *termdash) inputError(err error) {
|
||||||
|
if td.inputErrorHandler != nil {
|
||||||
|
td.inputErrorHandler(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setClearNeeded flags that the terminal needs to be cleared next time we're
|
||||||
|
// drawing it.
|
||||||
|
func (td *termdash) setClearNeeded() {
|
||||||
|
td.mu.Lock()
|
||||||
|
defer td.mu.Unlock()
|
||||||
|
td.clearNeeded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// redraw redraws the container and its widgets.
|
||||||
|
// The caller must hold td.mu.
|
||||||
|
func (td *termdash) redraw() error {
|
||||||
|
if td.clearNeeded {
|
||||||
|
if err := td.term.Clear(); err != nil {
|
||||||
|
return fmt.Errorf("term.Clear => error: %v", err)
|
||||||
|
}
|
||||||
|
td.clearNeeded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := td.container.Draw(); err != nil {
|
||||||
|
return fmt.Errorf("container.Draw => error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := td.term.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("term.Flush => error: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyEvRedraw forwards the keyboard event and redraws the container and its
|
||||||
|
// widgets.
|
||||||
|
func (td *termdash) keyEvRedraw(ev *terminalapi.Keyboard) error {
|
||||||
|
td.mu.Lock()
|
||||||
|
defer td.mu.Unlock()
|
||||||
|
|
||||||
|
if err := td.container.Keyboard(ev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if td.keyboardSubscriber != nil {
|
||||||
|
td.keyboardSubscriber(ev)
|
||||||
|
}
|
||||||
|
return td.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mouseEvRedraw forwards the mouse event and redraws the container and its
|
||||||
|
// widgets.
|
||||||
|
func (td *termdash) mouseEvRedraw(ev *terminalapi.Mouse) error {
|
||||||
|
td.mu.Lock()
|
||||||
|
defer td.mu.Unlock()
|
||||||
|
|
||||||
|
if err := td.container.Mouse(ev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if td.mouseSubscriber != nil {
|
||||||
|
td.mouseSubscriber(ev)
|
||||||
|
}
|
||||||
|
return td.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodicRedraw is called once each RedrawInterval.
|
||||||
|
func (td *termdash) periodicRedraw() error {
|
||||||
|
td.mu.Lock()
|
||||||
|
defer td.mu.Unlock()
|
||||||
|
return td.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// processEvents processes terminal input events.
|
||||||
|
// This is the body of the event collecting goroutine.
|
||||||
|
func (td *termdash) processEvents(ctx context.Context) {
|
||||||
|
defer close(td.exitCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
event := td.term.Event(ctx)
|
||||||
|
switch ev := event.(type) {
|
||||||
|
case *terminalapi.Keyboard:
|
||||||
|
if err := td.keyEvRedraw(ev); err != nil {
|
||||||
|
td.inputError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *terminalapi.Mouse:
|
||||||
|
if err := td.mouseEvRedraw(ev); err != nil {
|
||||||
|
td.inputError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *terminalapi.Resize:
|
||||||
|
td.setClearNeeded()
|
||||||
|
|
||||||
|
case *terminalapi.Error:
|
||||||
|
// Don't forward the error if the context is closed.
|
||||||
|
// It just says that the context expired.
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
td.inputError(ev.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start starts the terminal dashboard. Blocks until the context expires or
|
||||||
|
// until stop() is called.
|
||||||
|
func (td *termdash) start(ctx context.Context) error {
|
||||||
|
redrawTimer := time.NewTicker(td.redrawInterval)
|
||||||
|
defer redrawTimer.Stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := td.periodicRedraw(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// stops when stop() is called or the context expires.
|
||||||
|
go td.processEvents(ctx)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-redrawTimer.C:
|
||||||
|
if err := td.periodicRedraw(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case <-td.closeCh:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop tells the event collecting goroutine to stop.
|
||||||
|
// Blocks until it exits.
|
||||||
|
func (td *termdash) stop() {
|
||||||
|
close(td.closeCh)
|
||||||
|
<-td.exitCh
|
||||||
|
}
|
335
termdash_test.go
Normal file
335
termdash_test.go
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package termdash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kylelemons/godebug/pretty"
|
||||||
|
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||||
|
"github.com/mum4k/termdash/container"
|
||||||
|
"github.com/mum4k/termdash/eventqueue"
|
||||||
|
"github.com/mum4k/termdash/keyboard"
|
||||||
|
"github.com/mum4k/termdash/mouse"
|
||||||
|
"github.com/mum4k/termdash/terminal/faketerm"
|
||||||
|
"github.com/mum4k/termdash/terminal/termbox"
|
||||||
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
|
"github.com/mum4k/termdash/widgetapi"
|
||||||
|
"github.com/mum4k/termdash/widgets/fakewidget"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example shows how to setup and run termdash.
|
||||||
|
func Example() error {
|
||||||
|
// 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 two fake widgets.
|
||||||
|
c := container.New(
|
||||||
|
t,
|
||||||
|
container.SplitVertical(
|
||||||
|
container.Left(
|
||||||
|
container.PlaceWidget(fakewidget.New(wOpts)),
|
||||||
|
),
|
||||||
|
container.Right(
|
||||||
|
container.PlaceWidget(fakewidget.New(wOpts)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Termdash runs until the context expires.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := Run(ctx, t, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorHandler just stores the last error received.
|
||||||
|
type errorHandler struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eh *errorHandler) handle(err error) {
|
||||||
|
eh.err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// keySubscriber just stores the last pressed key.
|
||||||
|
type keySubscriber struct {
|
||||||
|
received terminalapi.Keyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks *keySubscriber) receive(k *terminalapi.Keyboard) {
|
||||||
|
ks.received = *k
|
||||||
|
}
|
||||||
|
|
||||||
|
// mouseSubscriber just stores the last mouse event.
|
||||||
|
type mouseSubscriber struct {
|
||||||
|
received terminalapi.Mouse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *mouseSubscriber) receive(m *terminalapi.Mouse) {
|
||||||
|
ms.received = *m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
var (
|
||||||
|
handler errorHandler
|
||||||
|
keySub keySubscriber
|
||||||
|
mouseSub mouseSubscriber
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
opts []Option
|
||||||
|
events []terminalapi.Event
|
||||||
|
// function to execute after the test case, can do additional comparison.
|
||||||
|
after func() error
|
||||||
|
want func(size image.Point) *faketerm.Terminal
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "draws the dashboard until closed",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(ft.Area()),
|
||||||
|
widgetapi.Options{},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "resizes the terminal",
|
||||||
|
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",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(ft.Area()),
|
||||||
|
widgetapi.Options{
|
||||||
|
WantMouse: true,
|
||||||
|
},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "forwards keyboard events to container",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
},
|
||||||
|
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: "ignores input errors without error handler",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
terminalapi.NewError("input error"),
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(ft.Area()),
|
||||||
|
widgetapi.Options{},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "forwards input errors to the error handler",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
InputErrorHandler(handler.handle),
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
terminalapi.NewError("input error"),
|
||||||
|
},
|
||||||
|
after: func() error {
|
||||||
|
if want := "input error"; handler.err.Error() != want {
|
||||||
|
return fmt.Errorf("errorHandler got %v, want %v", handler.err, want)
|
||||||
|
}
|
||||||
|
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: "forwards keyboard events to the subscriber",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
KeyboardSubscriber(keySub.receive),
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyF1},
|
||||||
|
},
|
||||||
|
after: func() error {
|
||||||
|
want := terminalapi.Keyboard{Key: keyboard.KeyF1}
|
||||||
|
if diff := pretty.Compare(want, keySub.received); diff != "" {
|
||||||
|
return fmt.Errorf("keySubscriber got unexpected value, diff (-want, +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(ft.Area()),
|
||||||
|
widgetapi.Options{
|
||||||
|
WantKeyboard: true,
|
||||||
|
},
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyF1},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "forwards mouse events to the subscriber",
|
||||||
|
opts: []Option{
|
||||||
|
RedrawInterval(1),
|
||||||
|
MouseSubscriber(mouseSub.receive),
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
|
||||||
|
},
|
||||||
|
after: func() error {
|
||||||
|
want := terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp}
|
||||||
|
if diff := pretty.Compare(want, mouseSub.received); diff != "" {
|
||||||
|
return fmt.Errorf("mouseSubscriber got unexpected value, diff (-want, +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(ft.Area()),
|
||||||
|
widgetapi.Options{
|
||||||
|
WantMouse: true,
|
||||||
|
},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
handler = errorHandler{}
|
||||||
|
keySub = keySubscriber{}
|
||||||
|
mouseSub = mouseSubscriber{}
|
||||||
|
|
||||||
|
eq := eventqueue.New()
|
||||||
|
for _, ev := range tc.events {
|
||||||
|
eq.Push(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := faketerm.New(image.Point{60, 10}, faketerm.WithEventQueue(eq))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cont := container.New(
|
||||||
|
got,
|
||||||
|
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
||||||
|
WantKeyboard: true,
|
||||||
|
WantMouse: true,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||||
|
err = Run(ctx, got, cont, tc.opts...)
|
||||||
|
cancel()
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("Run => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.after != nil {
|
||||||
|
if err := tc.after(); err != nil {
|
||||||
|
t.Errorf("after => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if diff := faketerm.Diff(tc.want(got.Size()), got); diff != "" {
|
||||||
|
t.Errorf("Run => %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/mum4k/termdash/area"
|
"github.com/mum4k/termdash/area"
|
||||||
"github.com/mum4k/termdash/cell"
|
"github.com/mum4k/termdash/cell"
|
||||||
@ -52,13 +53,16 @@ func WithEventQueue(eq *eventqueue.Unbound) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Terminal is a fake terminal.
|
// Terminal is a fake terminal.
|
||||||
// This implementation is not thread-safe.
|
// This implementation is thread-safe.
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
// buffer holds the terminal cells.
|
// buffer holds the terminal cells.
|
||||||
buffer cell.Buffer
|
buffer cell.Buffer
|
||||||
|
|
||||||
// events is a queue of input events.
|
// events is a queue of input events.
|
||||||
events *eventqueue.Unbound
|
events *eventqueue.Unbound
|
||||||
|
|
||||||
|
// mu protects the buffer.
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new fake Terminal.
|
// New returns a new fake Terminal.
|
||||||
@ -89,6 +93,9 @@ func MustNew(size image.Point, opts ...Option) *Terminal {
|
|||||||
// Resize resizes the terminal to the provided size.
|
// Resize resizes the terminal to the provided size.
|
||||||
// This also clears the internal buffer.
|
// This also clears the internal buffer.
|
||||||
func (t *Terminal) Resize(size image.Point) error {
|
func (t *Terminal) Resize(size image.Point) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
b, err := cell.NewBuffer(size)
|
b, err := cell.NewBuffer(size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -100,6 +107,9 @@ func (t *Terminal) Resize(size image.Point) error {
|
|||||||
|
|
||||||
// BackBuffer returns the back buffer of the fake terminal.
|
// BackBuffer returns the back buffer of the fake terminal.
|
||||||
func (t *Terminal) BackBuffer() cell.Buffer {
|
func (t *Terminal) BackBuffer() cell.Buffer {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
return t.buffer
|
return t.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +134,9 @@ func (t *Terminal) String() string {
|
|||||||
|
|
||||||
// Implements terminalapi.Terminal.Size.
|
// Implements terminalapi.Terminal.Size.
|
||||||
func (t *Terminal) Size() image.Point {
|
func (t *Terminal) Size() image.Point {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
return t.buffer.Size()
|
return t.buffer.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +148,9 @@ func (t *Terminal) Area() image.Rectangle {
|
|||||||
|
|
||||||
// Implements terminalapi.Terminal.Clear.
|
// Implements terminalapi.Terminal.Clear.
|
||||||
func (t *Terminal) Clear(opts ...cell.Option) error {
|
func (t *Terminal) Clear(opts ...cell.Option) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
b, err := cell.NewBuffer(t.buffer.Size())
|
b, err := cell.NewBuffer(t.buffer.Size())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -160,6 +176,9 @@ func (t *Terminal) HideCursor() {
|
|||||||
|
|
||||||
// Implements terminalapi.Terminal.SetCell.
|
// Implements terminalapi.Terminal.SetCell.
|
||||||
func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error {
|
func (t *Terminal) SetCell(p image.Point, r rune, opts ...cell.Option) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
ar, err := area.FromSize(t.buffer.Size())
|
ar, err := area.FromSize(t.buffer.Size())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -184,6 +203,10 @@ func (t *Terminal) Event(ctx context.Context) terminalapi.Event {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return terminalapi.NewErrorf("unable to pull the next event: %v", err)
|
return terminalapi.NewErrorf("unable to pull the next event: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res, ok := ev.(*terminalapi.Resize); ok {
|
||||||
|
t.Resize(res.Size)
|
||||||
|
}
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user