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:
parent
16de0d2b87
commit
3ebc253453
132
eventqueue/eventqueue.go
Normal file
132
eventqueue/eventqueue.go
Normal 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)
|
||||
}
|
102
eventqueue/eventqueue_test.go
Normal file
102
eventqueue/eventqueue_test.go
Normal 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)
|
||||
}
|
||||
}
|
53
experimental/boxes/boxes.go
Normal file
53
experimental/boxes/boxes.go
Normal 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
31
experimental/term/term.go
Normal 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)
|
||||
}
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user