2017-01-14 14:07:43 +08:00
|
|
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
2015-04-09 12:21:36 +08:00
|
|
|
// Use of this source code is governed by a MIT license that can
|
|
|
|
// be found in the LICENSE file.
|
|
|
|
|
2015-04-09 00:06:47 +08:00
|
|
|
package termui
|
|
|
|
|
2015-08-20 03:22:53 +08:00
|
|
|
import (
|
2015-09-18 23:41:44 +08:00
|
|
|
"strconv"
|
2015-08-31 11:03:47 +08:00
|
|
|
"sync"
|
2015-04-09 00:06:47 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
tb "github.com/nsf/termbox-go"
|
2015-04-09 00:06:47 +08:00
|
|
|
)
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
/*
|
|
|
|
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>
|
2018-09-16 03:10:53 +08:00
|
|
|
meta events:
|
|
|
|
<Keyboard>
|
|
|
|
<Mouse>
|
2018-09-07 07:36:14 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
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.
|
2015-04-09 00:06:47 +08:00
|
|
|
type Event struct {
|
2018-09-07 07:36:14 +08:00
|
|
|
Type EventType
|
|
|
|
ID string
|
|
|
|
Payload interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mouse payload.
|
|
|
|
type Mouse struct {
|
|
|
|
Drag bool
|
|
|
|
X int
|
|
|
|
Y int
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2018-09-16 03:10:53 +08:00
|
|
|
switch e.Type {
|
|
|
|
case KeyboardEvent:
|
|
|
|
if val, ok := defaultES.handlers["<Keyboard>"]; ok {
|
|
|
|
val(e)
|
|
|
|
}
|
|
|
|
case MouseEvent:
|
|
|
|
if val, ok := defaultES.handlers["<Mouse>"]; ok {
|
|
|
|
val(e)
|
|
|
|
}
|
|
|
|
}
|
2018-09-07 07:36:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
}
|
2015-08-31 11:03:47 +08:00
|
|
|
}
|
2015-04-09 00:06:47 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
// StopLoop stops the event loop.
|
|
|
|
func StopLoop() {
|
|
|
|
defaultES.stopLoop <- true
|
|
|
|
}
|
2015-04-09 00:06:47 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
}
|
2015-08-31 11:03:47 +08:00
|
|
|
}
|
2015-04-09 00:06:47 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
func EventHook(f func(Event)) {
|
|
|
|
defaultES.Lock()
|
|
|
|
defaultES.hook = f
|
|
|
|
defaultES.Unlock()
|
|
|
|
}
|
2015-09-18 23:41:44 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
// 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 {
|
2015-09-18 23:41:44 +08:00
|
|
|
k := string(e.Ch)
|
|
|
|
pre := ""
|
|
|
|
mod := ""
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
if e.Mod == tb.ModAlt {
|
|
|
|
mod = "<M-"
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
|
|
|
if e.Ch == 0 {
|
|
|
|
if e.Key > 0xFFFF-12 {
|
|
|
|
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
|
|
|
|
} else if e.Key > 0xFFFF-25 {
|
2018-09-07 07:36:14 +08:00
|
|
|
ks := []string{"<Insert>", "<Delete>", "<Home>", "<End>", "<Previous>", "<Next>", "<Up>", "<Down>", "<Left>", "<Right>"}
|
2015-09-18 23:41:44 +08:00
|
|
|
k = ks[0xFFFF-int(e.Key)-12]
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Key <= 0x7F {
|
2018-09-07 07:36:14 +08:00
|
|
|
pre = "<C-"
|
2015-09-18 23:41:44 +08:00
|
|
|
k = string('a' - 1 + int(e.Key))
|
2018-09-07 07:36:14 +08:00
|
|
|
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"},
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
|
|
|
if sk, ok := kmap[e.Key]; ok {
|
|
|
|
pre = sk[0]
|
|
|
|
k = sk[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
if pre != "" {
|
|
|
|
k += ">"
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
2015-04-09 00:06:47 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
id := pre + mod + k
|
2017-05-27 18:11:46 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
return Event{
|
|
|
|
Type: KeyboardEvent,
|
|
|
|
ID: id,
|
2018-01-12 02:01:45 +08:00
|
|
|
}
|
2017-05-27 18:11:46 +08:00
|
|
|
}
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
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>",
|
2015-08-31 11:03:47 +08:00
|
|
|
}
|
2015-09-18 23:41:44 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
converted, ok := mouseButtonMap[e.Key]
|
|
|
|
if !ok {
|
|
|
|
converted = "Unknown_Mouse_Button"
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
Drag := false
|
|
|
|
if e.Mod == tb.ModMotion {
|
|
|
|
Drag = true
|
2015-08-20 03:22:53 +08:00
|
|
|
}
|
2015-09-19 09:07:57 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
return Event{
|
|
|
|
Type: MouseEvent,
|
|
|
|
ID: converted,
|
|
|
|
Payload: Mouse{
|
|
|
|
X: e.MouseX,
|
|
|
|
Y: e.MouseY,
|
|
|
|
Drag: Drag,
|
|
|
|
},
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
2015-10-08 02:25:59 +08:00
|
|
|
}
|
2017-01-17 16:11:16 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
// convertTermboxEvent turns a termbox event into a termui event.
|
|
|
|
func convertTermboxEvent(e tb.Event) Event {
|
|
|
|
if e.Type == tb.EventError {
|
|
|
|
panic(e.Err)
|
2016-08-13 19:43:04 +08:00
|
|
|
}
|
2015-08-09 07:07:32 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
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,
|
|
|
|
},
|
2015-10-08 02:25:59 +08:00
|
|
|
}
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
return Event{}
|
2015-09-18 23:41:44 +08:00
|
|
|
}
|
|
|
|
|
2017-01-17 16:11:16 +08:00
|
|
|
var DefaultHandler = func(e Event) {
|
2015-04-09 00:06:47 +08:00
|
|
|
}
|
2015-10-14 00:45:03 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
func ResetHandlers() {
|
|
|
|
defaultES.Lock()
|
|
|
|
defaultES.handlers = make(map[string]func(Event))
|
|
|
|
defaultES.Unlock()
|
|
|
|
}
|
2015-10-14 00:45:03 +08:00
|
|
|
|
2018-09-07 07:36:14 +08:00
|
|
|
func ResetHandler(handle string) {
|
|
|
|
defaultES.Lock()
|
|
|
|
delete(defaultES.handlers, handle)
|
|
|
|
defaultES.Unlock()
|
2015-10-14 00:45:03 +08:00
|
|
|
}
|