Rework event system

* Timers should now be done through Go tickers
* Reworked event names used in Handle()
* Reworked Event type and payloads
This commit is contained in:
Caleb Bassi 2018-09-06 16:36:14 -07:00
parent 609f0e3c48
commit b227bd5277
18 changed files with 318 additions and 384 deletions

View File

@ -24,7 +24,7 @@ func main() {
ui.Render(bc)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", "<Insert>", func(ui.Event) {
ui.StopLoop()
})

View File

@ -8,6 +8,7 @@ package main
import (
"math"
"time"
ui "github.com/gizak/termui"
)
@ -22,14 +23,21 @@ func main() {
p.TextFgColor = ui.ColorWhite
p.BorderLabel = "Text Box"
p.BorderFg = ui.ColorCyan
p.Handle("/timer/1s", func(e ui.Event) {
cnt := e.Data.(ui.EvtTimer)
if cnt.Count%2 == 0 {
p.TextFgColor = ui.ColorRed
} else {
p.TextFgColor = ui.ColorWhite
pTicker := time.NewTicker(time.Second)
pTickerCount := 1
go func() {
for {
if pTickerCount%2 == 0 {
p.TextFgColor = ui.ColorRed
} else {
p.TextFgColor = ui.ColorWhite
}
pTickerCount++
<-pTicker.C
}
})
}()
listData := []string{"[0] gizak/termui", "[1] editbox.go", "[2] interrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
@ -136,14 +144,20 @@ func main() {
ui.Render(p, l, g, sls, lc, bc, lc2, p2)
}
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
draw(int(t.Count))
})
drawTicker := time.NewTicker(time.Second)
drawTickerCount := 1
go func() {
for {
draw(drawTickerCount)
drawTickerCount++
<-drawTicker.C
}
}()
ui.Loop()
}

View File

@ -71,7 +71,7 @@ func main() {
ui.Render(g0, g1, g2, g3, g4)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -8,6 +8,7 @@ package main
import (
"math"
"time"
ui "github.com/gizak/termui"
)
@ -91,29 +92,33 @@ func main() {
ui.Render(ui.Body)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
i := t.Count
if i > 103 {
ui.StopLoop()
return
drawTicker := time.NewTicker(time.Second)
drawTickerCount := 1
go func() {
for {
if drawTickerCount > 103 {
ui.StopLoop()
return
}
for _, g := range gs {
g.Percent = (g.Percent + 3) % 100
}
sp.Lines[0].Data = spdata[:100+drawTickerCount]
lc.Data["default"] = sinps[2*drawTickerCount:]
ui.Render(ui.Body)
drawTickerCount++
<-drawTicker.C
}
}()
for _, g := range gs {
g.Percent = (g.Percent + 3) % 100
}
sp.Lines[0].Data = spdata[:100+i]
lc.Data["default"] = sinps[2*i:]
ui.Render(ui.Body)
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Body.Width = ui.TermWidth()
ui.Handle("<Resize>", func(e ui.Event) {
payload := e.Payload.(ui.Resize)
ui.Body.Width = payload.Width
ui.Body.Align()
ui.Clear()
ui.Render(ui.Body)

View File

@ -63,7 +63,7 @@ func main() {
ui.Render(lc0, lc1, lc2)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -32,7 +32,7 @@ func main() {
ui.Render(ls)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -41,7 +41,7 @@ func main() {
ui.Render(bc)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -39,7 +39,7 @@ func main() {
ui.Render(par0, par1, par2, par3)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -37,14 +37,21 @@ func main() {
return fmt.Sprintf("%.02f", v)
}
ui.Handle("/timer/1s", func(e ui.Event) {
if run {
pc.Data, pc.Offset = randomDataAndOffset()
ui.Render(pc)
}
})
drawTicker := time.NewTicker(time.Second)
drawTickerCount := 1
go func() {
for {
if run {
pc.Data, pc.Offset = randomDataAndOffset()
ui.Render(pc)
}
ui.Handle("/sys/kbd/s", func(ui.Event) {
drawTickerCount++
<-drawTicker.C
}
}()
ui.Handle("s", func(ui.Event) {
run = !run
if run {
pc.BorderLabel = "Pie Chart"
@ -54,7 +61,7 @@ func main() {
ui.Render(pc)
})
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -56,7 +56,7 @@ func main() {
ui.Render(spls0, spls1, spls2)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -50,7 +50,7 @@ func main() {
ui.Render(table2)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

View File

@ -58,17 +58,17 @@ func main() {
ui.Render(header, tabpane)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/sys/kbd/j", func(ui.Event) {
ui.Handle("j", func(ui.Event) {
tabpane.SetActiveLeft()
ui.Clear()
ui.Render(header, tabpane)
})
ui.Handle("/sys/kbd/k", func(ui.Event) {
ui.Handle("k", func(ui.Event) {
tabpane.SetActiveRight()
ui.Clear()
ui.Render(header, tabpane)

View File

@ -8,6 +8,7 @@ package main
import (
"math"
"time"
ui "github.com/gizak/termui"
)
@ -134,14 +135,20 @@ func main() {
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/timer/1s", func(e ui.Event) {
t := e.Data.(ui.EvtTimer)
draw(int(t.Count))
})
drawTicker := time.NewTicker(time.Second)
drawTickerCount := 1
go func() {
for {
draw(drawTickerCount)
drawTickerCount++
<-drawTicker.C
}
}()
ui.Loop()
}

View File

@ -17,6 +17,7 @@ import (
"sort"
"strconv"
"strings"
"time"
ui "github.com/gizak/termui"
"github.com/gizak/termui/extra"
@ -330,35 +331,42 @@ func main() {
ui.Render(header, tabpane)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/sys/kbd/j", func(ui.Event) {
ui.Handle("j", func(ui.Event) {
tabpane.SetActiveLeft()
ui.Render(header, tabpane)
})
ui.Handle("/sys/kbd/k", func(ui.Event) {
ui.Handle("k", func(ui.Event) {
tabpane.SetActiveRight()
ui.Render(header, tabpane)
})
ui.Handle("/timer/1s", func(e ui.Event) {
cs, errcs := getCpusStatsMap()
if errcs != nil {
panic(errcs)
}
cpusStats.tick(cs)
cpuTabElems.Update(*cpusStats)
drawTicker := time.NewTicker(time.Second)
drawTickerCount := 1
go func() {
for {
cs, errcs := getCpusStatsMap()
if errcs != nil {
panic(errcs)
}
cpusStats.tick(cs)
cpuTabElems.Update(*cpusStats)
ms, errm := getMemStats()
if errm != nil {
panic(errm)
ms, errm := getMemStats()
if errm != nil {
panic(errm)
}
memTabElems.Update(ms)
ui.Render(header, tabpane)
drawTickerCount++
<-drawTicker.C
}
memTabElems.Update(ms)
ui.Render(header, tabpane)
})
}()
ui.Loop()
}

View File

@ -25,7 +25,7 @@ func main() {
ui.Render(p)
ui.Handle("/sys/kbd/q", func(ui.Event) {
ui.Handle("q", func(ui.Event) {
ui.StopLoop()
})

494
events.go
View File

@ -5,60 +5,163 @@
package termui
import (
"path"
"strconv"
"sync"
"time"
"github.com/nsf/termbox-go"
tb "github.com/nsf/termbox-go"
)
/*
Here's the list of events which can be assigned handlers using Handle():
mouse events:
<MouseLeft> <MouseRight> <MouseMiddle>
<MouseWheelUp> <MouseWheelDown>
keyboard events:
any uppercase or lowercase letter or a set of two letters like j or jj or J or JJ
<C-d> etc
<M-d> etc
<Up> <Down> <Left> <Right>
<Insert> <Delete> <Home> <End> <Previous> <Next>
<Backspace> <Tab> <Enter> <Escape> <Space>
<C-<Space>> etc
terminal events:
<Resize>
*/
type EventType int
const (
KeyboardEvent EventType = iota
MouseEvent
ResizeEvent
)
type eventStream struct {
sync.RWMutex
handlers map[string]func(Event)
stopLoop chan bool
eventQueue chan tb.Event // list of events from termbox
hook func(Event)
}
var defaultES = eventStream{
handlers: make(map[string]func(Event)),
stopLoop: make(chan bool, 1),
eventQueue: make(chan tb.Event),
hook: DefaultHandler,
}
// Event contains an ID used for Handle() and an optional payload.
type Event struct {
Type string
Path string
From string
To string
Data interface{}
Time int64
Type EventType
ID string
Payload interface{}
}
var sysEvtChs []chan Event
type EvtKbd struct {
KeyStr string
// Mouse payload.
type Mouse struct {
Drag bool
X int
Y int
}
func evtKbd(e termbox.Event) EvtKbd {
ek := EvtKbd{}
// Resize payload.
type Resize struct {
Width int
Height int
}
// handleEvent calls the approriate callback function if there is one.
func handleEvent(e Event) {
if val, ok := defaultES.handlers[e.ID]; ok {
val(e)
}
}
// Loop gets events from termbox and passes them off to handleEvent.
// Stops when StopLoop is called.
func Loop() {
go func() {
for {
defaultES.eventQueue <- tb.PollEvent()
}
}()
for {
select {
case <-defaultES.stopLoop:
return
case e := <-defaultES.eventQueue:
ne := convertTermboxEvent(e)
defaultES.RLock()
handleEvent(ne)
defaultES.hook(ne)
defaultES.RUnlock()
}
}
}
// StopLoop stops the event loop.
func StopLoop() {
defaultES.stopLoop <- true
}
// Handle assigns event names to their handlers. Takes a string, strings, or a slice of strings, and a function.
func Handle(things ...interface{}) {
function := things[len(things)-1].(func(Event))
for _, thing := range things {
if value, ok := thing.(string); ok {
defaultES.Lock()
defaultES.handlers[value] = function
defaultES.Unlock()
}
if value, ok := thing.([]string); ok {
defaultES.Lock()
for _, name := range value {
defaultES.handlers[name] = function
}
defaultES.Unlock()
}
}
}
func EventHook(f func(Event)) {
defaultES.Lock()
defaultES.hook = f
defaultES.Unlock()
}
// convertTermboxKeyboardEvent converts a termbox keyboard event to a more friendly string format.
// Combines modifiers into the string instead of having them as additional fields in an event.
func convertTermboxKeyboardEvent(e tb.Event) Event {
k := string(e.Ch)
pre := ""
mod := ""
if e.Mod == termbox.ModAlt {
mod = "M-"
if e.Mod == tb.ModAlt {
mod = "<M-"
}
if e.Ch == 0 {
if e.Key > 0xFFFF-12 {
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
} else if e.Key > 0xFFFF-25 {
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
ks := []string{"<Insert>", "<Delete>", "<Home>", "<End>", "<Previous>", "<Next>", "<Up>", "<Down>", "<Left>", "<Right>"}
k = ks[0xFFFF-int(e.Key)-12]
}
if e.Key <= 0x7F {
pre = "C-"
pre = "<C-"
k = string('a' - 1 + int(e.Key))
kmap := map[termbox.Key][2]string{
termbox.KeyCtrlSpace: {"C-", "<space>"},
termbox.KeyBackspace: {"", "<backspace>"},
termbox.KeyTab: {"", "<tab>"},
termbox.KeyEnter: {"", "<enter>"},
termbox.KeyEsc: {"", "<escape>"},
termbox.KeyCtrlBackslash: {"C-", "\\"},
termbox.KeyCtrlSlash: {"C-", "/"},
termbox.KeySpace: {"", "<space>"},
termbox.KeyCtrl8: {"C-", "8"},
kmap := map[tb.Key][2]string{
tb.KeyCtrlSpace: {"C-", "<Space>"},
tb.KeyBackspace: {"", "<Backspace>"},
tb.KeyTab: {"", "<Tab>"},
tb.KeyEnter: {"", "<Enter>"},
tb.KeyEsc: {"", "<Escape>"},
tb.KeyCtrlBackslash: {"C-", "\\"},
tb.KeyCtrlSlash: {"C-", "/"},
tb.KeySpace: {"", "<Space>"},
tb.KeyCtrl8: {"C-", "8"},
}
if sk, ok := kmap[e.Key]; ok {
pre = sk[0]
@ -67,288 +170,85 @@ func evtKbd(e termbox.Event) EvtKbd {
}
}
ek.KeyStr = pre + mod + k
return ek
}
func crtTermboxEvt(e termbox.Event) Event {
systypemap := map[termbox.EventType]string{
termbox.EventKey: "keyboard",
termbox.EventResize: "window",
termbox.EventMouse: "mouse",
termbox.EventError: "error",
termbox.EventInterrupt: "interrupt",
}
ne := Event{From: "/sys", Time: time.Now().Unix()}
typ := e.Type
ne.Type = systypemap[typ]
switch typ {
case termbox.EventKey:
kbd := evtKbd(e)
ne.Path = "/sys/kbd/" + kbd.KeyStr
ne.Data = kbd
case termbox.EventResize:
wnd := EvtWnd{}
wnd.Width = e.Width
wnd.Height = e.Height
ne.Path = "/sys/wnd/resize"
ne.Data = wnd
case termbox.EventError:
err := EvtErr(e.Err)
ne.Path = "/sys/err"
ne.Data = err
case termbox.EventMouse:
m := evtMouse(e)
ne.Path = "/sys/mouse/" + m.Press
ne.Data = m
}
return ne
}
type EvtWnd struct {
Width int
Height int
}
type EvtMouse struct {
X int
Y int
Drag bool
Press string
}
func evtMouse(e termbox.Event) EvtMouse {
em := EvtMouse{}
em.X = e.MouseX
em.Y = e.MouseY
if e.Mod == termbox.ModMotion {
em.Drag = true
if pre != "" {
k += ">"
}
switch e.Key {
case termbox.MouseLeft:
em.Press = "MouseLeft"
case termbox.MouseMiddle:
em.Press = "MouseMiddle"
case termbox.MouseRight:
em.Press = "MouseRight"
id := pre + mod + k
case termbox.MouseRelease:
em.Press = "MouseRelease"
case termbox.MouseWheelUp:
em.Press = "MouseWheelUp"
case termbox.MouseWheelDown:
em.Press = "MouseWheelDown"
default:
em.Press = "Unknown_Mouse_Button"
return Event{
Type: KeyboardEvent,
ID: id,
}
return em
}
type EvtErr error
func convertTermboxMouseEvent(e tb.Event) Event {
mouseButtonMap := map[tb.Key]string{
tb.MouseLeft: "<MouseLeft>",
tb.MouseMiddle: "<MouseMiddle>",
tb.MouseRight: "<MouseRight>",
tb.MouseRelease: "<MouseRelease>",
tb.MouseWheelUp: "<MouseWheelUp>",
tb.MouseWheelDown: "<MouseWheelDown>",
}
func hookTermboxEvt() {
for {
e := termbox.PollEvent()
converted, ok := mouseButtonMap[e.Key]
if !ok {
converted = "Unknown_Mouse_Button"
}
for _, c := range sysEvtChs {
go func(ch chan Event) {
ch <- crtTermboxEvt(e)
}(c)
Drag := false
if e.Mod == tb.ModMotion {
Drag = true
}
return Event{
Type: MouseEvent,
ID: converted,
Payload: Mouse{
X: e.MouseX,
Y: e.MouseY,
Drag: Drag,
},
}
}
// convertTermboxEvent turns a termbox event into a termui event.
func convertTermboxEvent(e tb.Event) Event {
if e.Type == tb.EventError {
panic(e.Err)
}
switch e.Type {
case tb.EventKey:
return convertTermboxKeyboardEvent(e)
case tb.EventMouse:
return convertTermboxMouseEvent(e)
case tb.EventResize:
return Event{
Type: ResizeEvent,
ID: "<Resize>",
Payload: Resize{
Width: e.Width,
Height: e.Height,
},
}
}
}
func NewSysEvtCh() chan Event {
ec := make(chan Event)
sysEvtChs = append(sysEvtChs, ec)
return ec
}
var DefaultEvtStream *EvtStream
type EvtStream struct {
sync.RWMutex
srcMap map[string]chan Event
stream chan Event
wg sync.WaitGroup
sigStopLoop chan Event
Handlers map[string]func(Event)
hook func(Event)
}
func NewEvtStream() *EvtStream {
return &EvtStream{
srcMap: make(map[string]chan Event),
stream: make(chan Event),
Handlers: make(map[string]func(Event)),
sigStopLoop: make(chan Event),
}
}
func (es *EvtStream) Init() {
es.Merge("internal", es.sigStopLoop)
go func() {
es.wg.Wait()
close(es.stream)
}()
}
func cleanPath(p string) string {
if p == "" {
return "/"
}
if p[0] != '/' {
p = "/" + p
}
return path.Clean(p)
}
func isPathMatch(pattern, path string) bool {
if len(pattern) == 0 {
return false
}
n := len(pattern)
return len(path) >= n && path[0:n] == pattern
}
func (es *EvtStream) Merge(name string, ec chan Event) {
es.Lock()
defer es.Unlock()
es.wg.Add(1)
es.srcMap[name] = ec
go func(a chan Event) {
for n := range a {
n.From = name
es.stream <- n
}
es.wg.Done()
}(ec)
}
func (es *EvtStream) Handle(path string, handler func(Event)) {
es.Handlers[cleanPath(path)] = handler
}
func findMatch(mux map[string]func(Event), path string) string {
n := -1
pattern := ""
for m := range mux {
if !isPathMatch(m, path) {
continue
}
if len(m) > n {
pattern = m
n = len(m)
}
}
return pattern
}
// Remove all existing defined Handlers from the map
func (es *EvtStream) ResetHandlers() {
for Path, _ := range es.Handlers {
delete(es.Handlers, Path)
}
return
}
func (es *EvtStream) match(path string) string {
return findMatch(es.Handlers, path)
}
func (es *EvtStream) Hook(f func(Event)) {
es.hook = f
}
func (es *EvtStream) Loop() {
for e := range es.stream {
switch e.Path {
case "/sig/stoploop":
return
}
go func(a Event) {
es.RLock()
defer es.RUnlock()
if pattern := es.match(a.Path); pattern != "" {
es.Handlers[pattern](a)
}
}(e)
if es.hook != nil {
es.hook(e)
}
}
}
func (es *EvtStream) StopLoop() {
go func() {
e := Event{
Path: "/sig/stoploop",
}
es.sigStopLoop <- e
}()
}
func Merge(name string, ec chan Event) {
DefaultEvtStream.Merge(name, ec)
}
func Handle(path string, handler func(Event)) {
DefaultEvtStream.Handle(path, handler)
}
func Loop() {
DefaultEvtStream.Loop()
}
func StopLoop() {
DefaultEvtStream.StopLoop()
}
type EvtTimer struct {
Duration time.Duration
Count uint64
}
func NewTimerCh(du time.Duration) chan Event {
t := make(chan Event)
go func(a chan Event) {
n := uint64(0)
for {
n++
time.Sleep(du)
e := Event{}
e.Type = "timer"
e.Path = "/timer/" + du.String()
e.Time = time.Now().Unix()
e.Data = EvtTimer{
Duration: du,
Count: n,
}
t <- e
}
}(t)
return t
return Event{}
}
var DefaultHandler = func(e Event) {
}
var usrEvtCh = make(chan Event)
func SendCustomEvt(path string, data interface{}) {
e := Event{}
e.Path = path
e.Data = data
e.Time = time.Now().Unix()
usrEvtCh <- e
func ResetHandlers() {
defaultES.Lock()
defaultES.handlers = make(map[string]func(Event))
defaultES.Unlock()
}
func ResetHandler(handle string) {
defaultES.Lock()
delete(defaultES.handlers, handle)
defaultES.Unlock()
}

View File

@ -8,7 +8,6 @@ import (
"image"
"runtime/debug"
"sync"
"time"
tb "github.com/nsf/termbox-go"
)
@ -25,10 +24,10 @@ func Init() error {
return err
}
tb.SetInputMode(tb.InputEsc | tb.InputMouse)
DefaultEvtStream = NewEvtStream()
// DefaultEvtStream = NewEvtStream()
sysEvtChs = make([]chan Event, 0)
go hookTermboxEvt()
// sysEvtChs = make([]chan Event, 0)
// go hookTermboxEvt()
renderJobs = make(chan []Bufferer)
//renderLock = new(sync.RWMutex)
@ -39,19 +38,13 @@ func Init() error {
Body.BgColor = ThemeAttr("bg")
Body.Width = TermWidth()
DefaultEvtStream.Init()
DefaultEvtStream.Merge("termbox", NewSysEvtCh())
DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
DefaultEvtStream.Merge("custom", usrEvtCh)
DefaultEvtStream.Handle("/", DefaultHandler)
DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) {
w := e.Data.(EvtWnd)
Body.Width = w.Width
Handle("<Resize>", func(e Event) {
payload := e.Payload.(Resize)
Body.Width = payload.Width
})
DefaultWgtMgr = NewWgtMgr()
DefaultEvtStream.Hook(DefaultWgtMgr.WgtHandlersHook())
EventHook(DefaultWgtMgr.WgtHandlersHook())
go func() {
for bs := range renderJobs {

View File

@ -75,8 +75,8 @@ func GenId() string {
func (wm WgtMgr) WgtHandlersHook() func(Event) {
return func(e Event) {
for _, v := range wm {
if k := findMatch(v.Handlers, e.Path); k != "" {
v.Handlers[k](e)
if val, ok := v.Handlers[e.ID]; ok {
val(e)
}
}
}