// Copyright 2017 Zack Guo . All rights reserved. // Use of this source code is governed by a MIT license that can // be found in the LICENSE file. package termui import ( "errors" "path" "sync" "time" "github.com/gdamore/tcell/v2" ) type Event struct { Type string Path string From string To string Data interface{} Time int64 } var sysEvtChs []chan Event type EvtKbd struct { KeyStr string } func evtKbd(e tcell.EventKey) EvtKbd { ek := EvtKbd{} k := string(e.Rune()) pre := "" mod := "" if e.Modifiers() == tcell.ModAlt { mod = "M-" } if e.Rune() == 0 { // Doesn't appear to be used by cointop // TODO: FIXME // if e.Key > 0xFFFF-12 { // k = "" // } else if e.Key > 0xFFFF-25 { // ks := []string{"", "", "", "", "", "", "", "", "", ""} // k = ks[0xFFFF-int(e.Key)-12] // } // TODO: FIXME // if e.Key <= 0x7F { // pre = "C-" // k = fmt.Sprintf("%v", 'a'-1+int(e.Key)) // kmap := map[termbox.Key][2]string{ // termbox.KeyCtrlSpace: {"C-", ""}, // TODO: FIXME // termbox.KeyBackspace: {"", ""}, // termbox.KeyTab: {"", ""}, // termbox.KeyEnter: {"", ""}, // termbox.KeyEsc: {"", ""}, // termbox.KeyCtrlBackslash: {"C-", "\\"}, // termbox.KeyCtrlSlash: {"C-", "/"}, // termbox.KeySpace: {"", ""}, // termbox.KeyCtrl8: {"C-", "8"}, // TODO: FIXME // } // if sk, ok := kmap[e.Key]; ok { // pre = sk[0] // k = sk[1] // } // } } ek.KeyStr = pre + mod + k return ek } func crtTermboxEvt(e tcell.Event) Event { ne := Event{From: "/sys", Time: e.When().Unix()} switch tev := e.(type) { case *tcell.EventResize: wnd := EvtWnd{} wnd.Width, wnd.Height = tev.Size() ne.Path = "/sys/wnd/resize" ne.Data = wnd ne.Type = "window" // log.Debugf("XXX Resized to %d,%d", wnd.Width, wnd.Height) return ne case *tcell.EventMouse: m := EvtMouse{} m.X, m.Y = tev.Position() ne.Path = "/sys/mouse" ne.Data = m ne.Type = "mouse" return ne case *tcell.EventKey: kbd := evtKbd(*tev) ne.Path = "/sys/kbd/" + kbd.KeyStr ne.Data = kbd ne.Type = "keyboard" return ne case *tcell.EventError: ne.Path = "/sys/err" ne.Data = errors.New(tev.Error()) ne.Type = "error" return ne case *tcell.EventInterrupt: ne.Type = "interrupt" default: ne.Type = "" // TODO: unhandled event? } return ne } type EvtWnd struct { Width int Height int } type EvtMouse struct { X int Y int Press string } type EvtErr error // func hookTermboxEvt() { // log.Debugf("XXX hookTermboxEvt") // for { // e := termbox.PollEvent() // log.Debugf("XXX event %s", e) // for _, c := range sysEvtChs { // func(ch chan Event) { // ch <- crtTermboxEvt(e) // }(c) // } // } // } func NewSysEvtCh() chan Event { ec := make(chan Event) sysEvtChs = append(sysEvtChs, ec) return ec } var DefaultEvtStream = NewEvtStream() 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 } // ResetHandlers 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 } 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 ResetHandlers() { DefaultEvtStream.ResetHandlers() } 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 } 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 }