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

Implementing an unbound FIFO queue of terminal events.

This commit is contained in:
Jakub Sobon 2018-04-02 01:59:04 +02:00
parent 16de0d2b87
commit 3ebc253453
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
5 changed files with 324 additions and 0 deletions

132
eventqueue/eventqueue.go Normal file
View File

@ -0,0 +1,132 @@
// Package eventqueue provides an unboud FIFO queue of events.
package eventqueue
import (
"context"
"sync"
"time"
"github.com/mum4k/termdash/terminalapi"
)
// node is a single data item on the queue.
type node struct {
next *node
event terminalapi.Event
}
// Unbound is an unbound FIFO queue of terminal events.
// Unbound must not be copied, pass it by reference only.
// This implementation is thread-safe.
type Unbound struct {
first *node
last *node
// mu protects first and last.
mu sync.Mutex
// cond is used to notify any callers waiting on a call to Pull().
cond *sync.Cond
// condMU protects cond.
condMU sync.RWMutex
// done is closed when the queue isn't needed anymore.
done chan struct{}
}
// New returns a new Unbound queue of terminal events.
// Call Close() when done with the queue.
func New() *Unbound {
u := &Unbound{
done: make(chan (struct{})),
}
u.cond = sync.NewCond(&u.condMU)
go u.wake() // Stops when Close() is called.
return u
}
// wake periodically wakes up a goroutine waiting at Pull() so it can
// check if the context expired.
func (u *Unbound) wake() {
const spinTime = 250 * time.Millisecond
t := time.NewTicker(spinTime)
defer t.Stop()
for {
select {
case <-t.C:
u.cond.Signal()
case <-u.done:
return
}
}
}
// empty determines if the queue is empty.
func (u *Unbound) empty() bool {
return u.first == nil
}
// Put puts an event onto the queue.
func (u *Unbound) Push(e terminalapi.Event) {
u.mu.Lock()
defer u.mu.Unlock()
n := &node{
event: e,
}
if u.empty() {
u.first = n
u.last = n
} else {
u.last.next = n
u.last = n
}
u.cond.Signal()
}
// Get gets an event from the queue. Returns nil if the queue is empty.
func (u *Unbound) Pop() terminalapi.Event {
u.mu.Lock()
defer u.mu.Unlock()
if u.empty() {
return nil
}
n := u.first
u.first = u.first.next
if u.empty() {
u.last = nil
}
return n.event
}
// Pull is like Pop(), but blocks until an item is available or the context
// expires.
func (u *Unbound) Pull(ctx context.Context) (terminalapi.Event, error) {
if e := u.Pop(); e != nil {
return e, nil
}
u.cond.L.Lock()
defer u.cond.L.Unlock()
for {
if e := u.Pop(); e != nil {
return e, nil
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
u.cond.Wait()
}
}
// Close should be called when the queue isn't needed anymore.
func (u *Unbound) Close() {
close(u.done)
}

View File

@ -0,0 +1,102 @@
package eventqueue
import (
"context"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/terminalapi"
)
func TestQueue(t *testing.T) {
tests := []struct {
desc string
pushes []terminalapi.Event
wantPops []terminalapi.Event
}{
{
desc: "empty queue returns nil",
wantPops: []terminalapi.Event{
nil,
},
},
{
desc: "queue is FIFO",
pushes: []terminalapi.Event{
terminalapi.NewError("error1"),
terminalapi.NewError("error2"),
terminalapi.NewError("error3"),
},
wantPops: []terminalapi.Event{
terminalapi.NewError("error1"),
terminalapi.NewError("error2"),
terminalapi.NewError("error3"),
nil,
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
q := New()
defer q.Close()
for _, ev := range tc.pushes {
q.Push(ev)
}
for i, want := range tc.wantPops {
got := q.Pop()
if diff := pretty.Compare(want, got); diff != "" {
t.Errorf("Pop[%d] => unexpected diff (-want, +got):\n%s", i, diff)
}
}
})
}
}
func TestPullEventAvailable(t *testing.T) {
q := New()
defer q.Close()
want := terminalapi.NewError("error event")
q.Push(want)
ctx := context.Background()
got, err := q.Pull(ctx)
if err != nil {
t.Fatalf("Pull => unexpected error: %v", err)
}
if diff := pretty.Compare(want, got); diff != "" {
t.Errorf("Pull => unexpected diff (-want, +got):\n%s", diff)
}
}
func TestPullBlocksUntilAvailable(t *testing.T) {
q := New()
defer q.Close()
want := terminalapi.NewError("error event")
ch := make(chan struct{})
go func() {
<-ch
q.Push(want)
}()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
got, err := q.Pull(ctx)
if err == nil {
t.Fatal("Pull => expected timeout error, got nil")
}
close(ch)
got, err = q.Pull(context.Background())
if err != nil {
t.Fatalf("Pull => unexpected error: %v", err)
}
if diff := pretty.Compare(want, got); diff != "" {
t.Errorf("Pull => unexpected diff (-want, +got):\n%s", diff)
}
}

View File

@ -0,0 +1,53 @@
// Binary boxes just creates containers with borders.
package main
import (
"time"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/terminal/termbox"
)
func main() {
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()
c := container.New(
t,
container.SplitVertical(
container.Left(
container.SplitHorizontal(
container.Top(
container.Border(draw.LineStyleLight),
),
container.Bottom(
container.SplitHorizontal(
container.Top(
container.Border(draw.LineStyleLight),
),
container.Bottom(
container.Border(draw.LineStyleLight),
),
),
),
),
),
container.Right(
container.Border(draw.LineStyleLight),
),
),
)
if err := c.Draw(); err != nil {
panic(err)
}
if err := t.Flush(); err != nil {
panic(err)
}
time.Sleep(3 * time.Second)
}

31
experimental/term/term.go Normal file
View File

@ -0,0 +1,31 @@
// Binary term just initializes the terminal and sets a few cells.
package main
import (
"image"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminalapi"
)
func main() {
t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
if err != nil {
panic(err)
}
defer t.Close()
if err := t.SetCell(image.Point{0, 0}, 'X', cell.FgColor(cell.ColorMagenta)); err != nil {
panic(err)
}
if err := t.SetCell(t.Size().Sub(image.Point{1, 1}), 'X', cell.FgColor(cell.ColorMagenta)); err != nil {
panic(err)
}
if err := t.Flush(); err != nil {
panic(err)
}
time.Sleep(3 * time.Second)
}

View File

@ -51,6 +51,12 @@ func (*Mouse) isEvent() {}
// Error is an event indicating an error while processing input.
type Error string
// NewError returns a new Error event.
func NewError(e string) *Error {
err := Error(e)
return &err
}
func (*Error) isEvent() {}
// Error returns the error that occurred.