diff --git a/example/barchart.go b/_example/barchart.go similarity index 100% rename from example/barchart.go rename to _example/barchart.go diff --git a/example/barchart.png b/_example/barchart.png similarity index 100% rename from example/barchart.png rename to _example/barchart.png diff --git a/example/coloredList.go b/_example/coloredList.go similarity index 100% rename from example/coloredList.go rename to _example/coloredList.go diff --git a/example/dashboard.gif b/_example/dashboard.gif similarity index 100% rename from example/dashboard.gif rename to _example/dashboard.gif diff --git a/example/dashboard.go b/_example/dashboard.go similarity index 100% rename from example/dashboard.go rename to _example/dashboard.go diff --git a/example/gauge.go b/_example/gauge.go similarity index 100% rename from example/gauge.go rename to _example/gauge.go diff --git a/example/gauge.png b/_example/gauge.png similarity index 100% rename from example/gauge.png rename to _example/gauge.png diff --git a/example/grid.gif b/_example/grid.gif similarity index 100% rename from example/grid.gif rename to _example/grid.gif diff --git a/example/grid.go b/_example/grid.go similarity index 100% rename from example/grid.go rename to _example/grid.go diff --git a/example/linechart.go b/_example/linechart.go similarity index 100% rename from example/linechart.go rename to _example/linechart.go diff --git a/example/linechart.png b/_example/linechart.png similarity index 100% rename from example/linechart.png rename to _example/linechart.png diff --git a/example/list.go b/_example/list.go similarity index 100% rename from example/list.go rename to _example/list.go diff --git a/example/list.png b/_example/list.png similarity index 100% rename from example/list.png rename to _example/list.png diff --git a/example/mbarchart.go b/_example/mbarchart.go similarity index 100% rename from example/mbarchart.go rename to _example/mbarchart.go diff --git a/example/mbarchart.png b/_example/mbarchart.png similarity index 100% rename from example/mbarchart.png rename to _example/mbarchart.png diff --git a/example/par.go b/_example/par.go similarity index 100% rename from example/par.go rename to _example/par.go diff --git a/example/par.png b/_example/par.png similarity index 100% rename from example/par.png rename to _example/par.png diff --git a/example/sparklines.go b/_example/sparklines.go similarity index 100% rename from example/sparklines.go rename to _example/sparklines.go diff --git a/example/sparklines.png b/_example/sparklines.png similarity index 100% rename from example/sparklines.png rename to _example/sparklines.png diff --git a/example/theme.go b/_example/theme.go similarity index 100% rename from example/theme.go rename to _example/theme.go diff --git a/example/themedefault.png b/_example/themedefault.png similarity index 100% rename from example/themedefault.png rename to _example/themedefault.png diff --git a/example/themehelloworld.png b/_example/themehelloworld.png similarity index 100% rename from example/themehelloworld.png rename to _example/themehelloworld.png diff --git a/widget/barchart.go b/_widget/barchart.go similarity index 100% rename from widget/barchart.go rename to _widget/barchart.go diff --git a/widget/canvas.go b/_widget/canvas.go similarity index 100% rename from widget/canvas.go rename to _widget/canvas.go diff --git a/widget/canvas_test.go b/_widget/canvas_test.go similarity index 100% rename from widget/canvas_test.go rename to _widget/canvas_test.go diff --git a/widget/gauge.go b/_widget/gauge.go similarity index 100% rename from widget/gauge.go rename to _widget/gauge.go diff --git a/widget/linechart.go b/_widget/linechart.go similarity index 100% rename from widget/linechart.go rename to _widget/linechart.go diff --git a/widget/linechart_others.go b/_widget/linechart_others.go similarity index 100% rename from widget/linechart_others.go rename to _widget/linechart_others.go diff --git a/widget/linechart_windows.go b/_widget/linechart_windows.go similarity index 100% rename from widget/linechart_windows.go rename to _widget/linechart_windows.go diff --git a/widget/list.go b/_widget/list.go similarity index 100% rename from widget/list.go rename to _widget/list.go diff --git a/widget/mbar.go b/_widget/mbar.go similarity index 100% rename from widget/mbar.go rename to _widget/mbar.go diff --git a/widget/par.go b/_widget/par.go similarity index 100% rename from widget/par.go rename to _widget/par.go diff --git a/widget/sparkline.go b/_widget/sparkline.go similarity index 100% rename from widget/sparkline.go rename to _widget/sparkline.go diff --git a/debug/debuger.go b/debug/debuger.go new file mode 100644 index 0000000..ac86226 --- /dev/null +++ b/debug/debuger.go @@ -0,0 +1,113 @@ +package debug + +import ( + "fmt" + "net/http" + + "golang.org/x/net/websocket" +) + +type Server struct { + Port string + Addr string + Path string + Msg chan string + chs []chan string +} + +type Client struct { + Port string + Addr string + Path string + ws *websocket.Conn +} + +var defaultPort = ":8080" + +func NewServer() *Server { + return &Server{ + Port: defaultPort, + Addr: "localhost", + Path: "/echo", + Msg: make(chan string), + chs: make([]chan string, 0), + } +} + +func NewClient() Client { + return Client{ + Port: defaultPort, + Addr: "localhost", + Path: "/echo", + } +} + +func (c Client) ConnectAndListen() error { + ws, err := websocket.Dial("ws://"+c.Addr+c.Port+c.Path, "", "http://"+c.Addr) + if err != nil { + return err + } + defer ws.Close() + + var m string + for { + err := websocket.Message.Receive(ws, &m) + if err != nil { + fmt.Print(err) + return err + } + fmt.Print(m) + } +} + +func (s *Server) ListenAndServe() error { + http.Handle(s.Path, websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + + mc := make(chan string) + s.chs = append(s.chs, mc) + + for m := range mc { + websocket.Message.Send(ws, m) + } + })) + + go func() { + for msg := range s.Msg { + for _, c := range s.chs { + go func(a chan string) { + a <- msg + }(c) + } + } + }() + + return http.ListenAndServe(s.Port, nil) +} + +func (s *Server) Log(msg string) { + go func() { s.Msg <- msg }() +} + +func (s *Server) Logf(format string, a ...interface{}) { + s.Log(fmt.Sprintf(format, a...)) +} + +var DefaultServer = NewServer() +var DefaultClient = NewClient() + +func ListenAndServe() error { + return DefaultServer.ListenAndServe() +} + +func ConnectAndListen() error { + return DefaultClient.ConnectAndListen() +} + +func Log(msg string) { + DefaultServer.Log(msg) +} + +func Logf(format string, a ...interface{}) { + DefaultServer.Logf(format, a...) +} diff --git a/events.go b/events.go index cca4ba3..2771e98 100644 --- a/events.go +++ b/events.go @@ -9,6 +9,7 @@ package termui import ( + "strconv" "strings" "sync" "time" @@ -25,32 +26,125 @@ type Event struct { Time int64 } -var sysevt struct { - chs []chan Event +var sysEvtChs []chan Event + +type EvtKbd struct { + KeyStr string } -func newSysEvtFromTb(e termbox.Event) Event { +func evtKbd(e termbox.Event) EvtKbd { + ek := EvtKbd{} + + k := string(e.Ch) + pre := "" + mod := "" + + if e.Mod == termbox.ModAlt { + mod = "M-" + } + if e.Ch == 0 { + if e.Key > 0xFFFF-12 { + k = "" + } else if e.Key > 0xFFFF-25 { + ks := []string{"", "", "", "", "", "", "", "", "", ""} + k = ks[0xFFFF-int(e.Key)-12] + } + + if e.Key <= 0x7F { + pre = "C-" + k = string('a' - 1 + int(e.Key)) + kmap := map[termbox.Key][2]string{ + termbox.KeyCtrlSpace: {"C-", ""}, + termbox.KeyBackspace: {"", ""}, + termbox.KeyTab: {"", ""}, + termbox.KeyEnter: {"", ""}, + termbox.KeyEsc: {"", ""}, + termbox.KeyCtrlBackslash: {"C-", "\\"}, + termbox.KeyCtrlSlash: {"C-", "/"}, + termbox.KeySpace: {"", ""}, + termbox.KeyCtrl8: {"C-", "8"}, + } + if sk, ok := kmap[e.Key]; ok { + pre = sk[0] + k = sk[1] + } + } + } + + 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{} + m.X = e.MouseX + m.Y = e.MouseY + ne.Path = "/sys/mouse" + ne.Data = m + } return ne } -func hookSysEvt() { - sysevt.chs = make([]chan Event, 0) +type EvtWnd struct { + Width int + Height int +} + +type EvtMouse struct { + X int + Y int + Press string +} + +type EvtErr error + +func hookTermboxEvt() { for { e := termbox.PollEvent() - for _, c := range sysevt.chs { - // shorten? - go func(ch chan Event, ev Event) { ch <- ev }(c, newSysEvtFromTb(e)) + + for _, c := range sysEvtChs { + go func(ch chan Event) { + ch <- crtTermboxEvt(e) + }(c) } } } func NewSysEvtCh() chan Event { ec := make(chan Event) - sysevt.chs = append(sysevt.chs, ec) + sysEvtChs = append(sysEvtChs, ec) return ec } +var DefaultEvtStream = NewEvtStream() + /* type evtCtl struct { in chan Event @@ -73,22 +167,23 @@ func newEvtCtl() evtCtl { */ // type EvtStream struct { - srcMap map[string]chan Event - stream chan Event - cache map[string][]func(Event) - wg sync.WaitGroup - Handlers map[string]func(Event) + srcMap map[string]chan Event + stream chan Event + wg sync.WaitGroup + sigStopLoop chan int + Handlers map[string]func(Event) } func NewEvtStream() *EvtStream { return &EvtStream{ - srcMap: make(map[string]chan Event), - stream: make(chan Event), + srcMap: make(map[string]chan Event), + stream: make(chan Event), + Handlers: make(map[string]func(Event)), + sigStopLoop: make(chan int), } } func (es *EvtStream) Init() { - go func() { es.wg.Wait() close(es.stream) @@ -107,17 +202,38 @@ func (es *EvtStream) Init() { // b: / // score: 0 func MatchScore(a, b string) int { - sa := strings.Split(a, "/") - sb := strings.Split(b, "/") - score := -1 + // divide by "" and rm heading "" + sliced := func(s string) []string { + ss := strings.Split(s, "/") + + i := 0 + for j := range ss { + if ss[j] == "" { + i++ + } else { + break + } + } + + return ss[i:] + } + + sa := sliced(a) + sb := sliced(b) + + score := 0 + if len(sb) > len(sa) { + return -1 // sb couldnt be more deeper than sa + } + for i, s := range sa { if i >= len(sb) { - break + break // exhaust b } if s != sb[i] { - return -1 + return -1 // mismatch } score++ } @@ -125,23 +241,98 @@ func MatchScore(a, b string) int { return score } -func (es *EvtStream) Merge(ec chan Event) { +func (es *EvtStream) Merge(name string, ec chan Event) { es.wg.Add(1) + es.srcMap[name] = ec go func(a chan Event) { - for n := range ec { + for n := range a { + n.From = name es.stream <- n } - wg.Done() + es.wg.Done() }(ec) } -/* -func (es *EvtStream) hookup() { - +func (es *EvtStream) Handle(path string, handler func(Event)) { + es.Handlers[path] = handler } -func (es EvtStream) Subscribe(uri string) chan Event { +func (es *EvtStream) match(path string) string { + n := 0 + pattern := "" + for m := range es.Handlers { + if MatchScore(path, m) < 0 { + continue + } + if pattern == "" || len(m) > n { + pattern = m + } + } + return pattern +} + +func (es *EvtStream) Loop() { + for { + select { + case e := <-es.stream: + if pattern := es.match(e.Path); pattern != "" { + es.Handlers[pattern](e) + } + case <-es.sigStopLoop: + return + } + } +} + +func (es *EvtStream) StopLoop() { + go func() { es.sigStopLoop <- 1 }() +} + +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.From = "timer" + 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 DefualtHandler = func(e Event) { } -*/ diff --git a/events_test.go b/events_test.go index 1137b1d..9fc4a0e 100644 --- a/events_test.go +++ b/events_test.go @@ -8,21 +8,34 @@ package termui -import ( - "errors" - "testing" +import "testing" - termbox "github.com/nsf/termbox-go" - "github.com/stretchr/testify/assert" -) +var ps = []string{ + "", + "/", + "/a", + "/b", + "/a/c", + "/a/b", + "/a/b/c", + "/a/b/c/d", + "/a/b/c/d/"} -type boxEvent termbox.Event +func TestMatchScore(t *testing.T) { + chk := func(a, b string, s int) { + if c := MatchScore(a, b); c != s { + t.Errorf("\na:%s\nb:%s\nshould:%d\nscore:%d", a, b, s, c) + } + } + + chk(ps[1], ps[1], 0) + chk(ps[1], ps[2], -1) + chk(ps[2], ps[1], 0) + chk(ps[4], ps[1], 0) + chk(ps[6], ps[2], 1) + chk(ps[4], ps[5], -1) +} + +func TestCrtEvt(t *testing.T) { -func TestUiEvt(t *testing.T) { - err := errors.New("This is a mock error") - event := boxEvent{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} - expetced := Event{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} - - // We need to do that ugly casting so that vet does not complain - assert.Equal(t, uiEvt(termbox.Event(event)), expetced) } diff --git a/grid.go b/grid.go index cd6f896..425fc04 100644 --- a/grid.go +++ b/grid.go @@ -275,6 +275,3 @@ func (g Grid) Buffer() Buffer { } return buf } - -// Body corresponds to the entire terminal display region. -var Body *Grid diff --git a/render.go b/render.go index ce3bdb3..1496ef1 100644 --- a/render.go +++ b/render.go @@ -4,7 +4,11 @@ package termui -import tm "github.com/nsf/termbox-go" +import ( + "time" + + tm "github.com/nsf/termbox-go" +) // Bufferer should be implemented by all renderable components. type Bufferer interface { @@ -14,16 +18,24 @@ type Bufferer interface { // Init initializes termui library. This function should be called before any others. // After initialization, the library must be finalized by 'Close' function. func Init() error { - Body = NewGrid() - Body.X = 0 - Body.Y = 0 - Body.BgColor = theme.BodyBg if err := tm.Init(); err != nil { return err } - w, _ := tm.Size() - Body.Width = w - evtListen() + + sysEvtChs = make([]chan Event, 0) + go hookTermboxEvt() + renderJobs = make(chan []Bufferer) + go func() { + for bs := range renderJobs { + Render(bs...) + } + }() + + DefaultEvtStream.Init() + DefaultEvtStream.Merge("termbox", NewSysEvtCh()) + DefaultEvtStream.Merge("timer", NewTimerCh(time.Second)) + DefaultEvtStream.Handle("/", DefualtHandler) + return nil } @@ -64,3 +76,9 @@ func Render(bs ...Bufferer) { // render tm.Flush() } + +var renderJobs chan []Bufferer + +func SendBufferToRender(bs ...Bufferer) { + go func() { renderJobs <- bs }() +} diff --git a/test/runtest.go b/test/runtest.go new file mode 100644 index 0000000..fc23ca7 --- /dev/null +++ b/test/runtest.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "os" + + "github.com/gizak/termui" + "github.com/gizak/termui/debug" +) + +func main() { + // run as client + if len(os.Args) > 1 { + fmt.Print(debug.ConnectAndListen()) + return + } + + // run as server + go func() { panic(debug.ListenAndServe()) }() + + if err := termui.Init(); err != nil { + panic(err) + } + defer termui.Close() + + termui.Handle("/sys", func(e termui.Event) { + k, ok := e.Data.(termui.EvtKbd) + debug.Logf("-->%v\n", e) + if ok && k.KeyStr == "q" { + termui.StopLoop() + } + }) + + termui.Handle("/timer", func(e termui.Event) { + //debug.Logf("-->%v\n", e) + }) + termui.Loop() +}