Initial release
This commit is contained in:
commit
4f8e530a0b
|
@ -0,0 +1,2 @@
|
|||
.idea/
|
||||
*.sh
|
|
@ -0,0 +1,8 @@
|
|||
stages:
|
||||
- test
|
||||
|
||||
test:
|
||||
image: golang:latest
|
||||
stage: test
|
||||
script:
|
||||
- go test -v ./...
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,14 @@
|
|||
# cbind
|
||||
[![GoDoc](https://godoc.org/gitlab.com/tslocum/cbind?status.svg)](https://godoc.org/gitlab.com/tslocum/cbind)
|
||||
[![CI status](https://gitlab.com/tslocum/cbind/badges/master/pipeline.svg)](https://gitlab.com/tslocum/cbind/commits/master)
|
||||
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
|
||||
|
||||
Key event handling library for tcell
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is available via [gdooc](https://godoc.org/gitlab.com/tslocum/cbind).
|
||||
|
||||
## Support
|
||||
|
||||
Please share issues/suggestions [here](https://gitlab.com/tslocum/cbind/issues).
|
|
@ -0,0 +1,77 @@
|
|||
package cbind
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type eventHandler func(ev *tcell.EventKey) *tcell.EventKey
|
||||
|
||||
// Configuration processes key events by mapping keys to event handlers.
|
||||
type Configuration struct {
|
||||
handlers map[string]eventHandler
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// NewConfiguration returns a new input configuration.
|
||||
func NewConfiguration() *Configuration {
|
||||
c := Configuration{
|
||||
handlers: make(map[string]eventHandler),
|
||||
mutex: new(sync.RWMutex),
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// SetKey sets the handler for a key.
|
||||
func (c *Configuration) SetKey(mod tcell.ModMask, key tcell.Key, handler func(ev *tcell.EventKey) *tcell.EventKey) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.handlers[fmt.Sprintf("%d-%d", mod, key)] = handler
|
||||
}
|
||||
|
||||
// SetRune sets the handler for a rune.
|
||||
func (c *Configuration) SetRune(mod tcell.ModMask, ch rune, handler func(ev *tcell.EventKey) *tcell.EventKey) {
|
||||
// Some runes are identical to named keys. Set the bind on the matching
|
||||
// named key instead.
|
||||
switch ch {
|
||||
case '\t':
|
||||
c.SetKey(mod, tcell.KeyTab, handler)
|
||||
return
|
||||
case '\n':
|
||||
c.SetKey(mod, tcell.KeyEnter, handler)
|
||||
return
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.handlers[fmt.Sprintf("%d:%d", mod, ch)] = handler
|
||||
}
|
||||
|
||||
// Capture handles key events.
|
||||
func (c *Configuration) Capture(ev *tcell.EventKey) *tcell.EventKey {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keyName string
|
||||
if ev.Key() != tcell.KeyRune {
|
||||
keyName = fmt.Sprintf("%d-%d", ev.Modifiers(), ev.Key())
|
||||
} else {
|
||||
keyName = fmt.Sprintf("%d:%d", ev.Modifiers(), ev.Rune())
|
||||
}
|
||||
|
||||
handler := c.handlers[keyName]
|
||||
if handler != nil {
|
||||
return handler(ev)
|
||||
}
|
||||
|
||||
return ev
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package cbind
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const pressTimes = 7
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wg := make([]*sync.WaitGroup, len(testCases))
|
||||
|
||||
config := NewConfiguration()
|
||||
for i, c := range testCases {
|
||||
wg[i] = new(sync.WaitGroup)
|
||||
wg[i].Add(pressTimes)
|
||||
|
||||
i := i // Capture
|
||||
if c.key != tcell.KeyRune {
|
||||
config.SetKey(c.mod, c.key, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
wg[i].Done()
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
config.SetRune(c.mod, c.ch, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
wg[i].Done()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
timeout := time.After(5 * time.Second)
|
||||
|
||||
go func() {
|
||||
for i := range testCases {
|
||||
wg[i].Wait()
|
||||
}
|
||||
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for j := 0; j < pressTimes; j++ {
|
||||
for _, c := range testCases {
|
||||
config.Capture(tcell.NewEventKey(c.key, c.ch, c.mod))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-timeout:
|
||||
t.Error("timeout")
|
||||
case <-done:
|
||||
// Wait at least one second to catch problems before exiting.
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Example of creating and using an input configuration.
|
||||
func ExampleNewConfiguration() {
|
||||
// Create a new input configuration to store the keybinds.
|
||||
c := NewConfiguration()
|
||||
|
||||
// Set keybind Alt+S.
|
||||
c.SetRune(tcell.ModAlt, 's', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
// Save
|
||||
return nil
|
||||
})
|
||||
|
||||
// Set keybind Alt+O.
|
||||
c.SetRune(tcell.ModAlt, 'o', func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
// Open
|
||||
return nil
|
||||
})
|
||||
|
||||
// Set keybind Escape.
|
||||
c.SetKey(tcell.ModNone, tcell.KeyEscape, func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
// Exit
|
||||
return nil
|
||||
})
|
||||
|
||||
// Before calling Application.Run, call Application.SetInputCapture:
|
||||
// app.SetInputCapture(c.Capture)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Package cbind provides tcell key event encoding, decoding and handling.
|
||||
|
||||
The NewConfiguration example demonstrates how to use cbind.
|
||||
|
||||
There are some limitations on reading keyboard input, which is explained in the tcell.EventKey documentation:
|
||||
|
||||
https://godoc.org/github.com/gdamore/tcell#EventKey
|
||||
*/
|
||||
package cbind
|
|
@ -0,0 +1,11 @@
|
|||
module gitlab.com/tslocum/cbind
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.3.0
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
@ -0,0 +1,169 @@
|
|||
package cbind
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Modifier labels
|
||||
const (
|
||||
LabelCtrl = "ctrl"
|
||||
LabelAlt = "alt"
|
||||
LabelMeta = "meta"
|
||||
LabelShift = "shift"
|
||||
)
|
||||
|
||||
var fullKeyNames = map[string]string{
|
||||
"backspace2": "Backspace",
|
||||
"pgup": "PageUp",
|
||||
"pgdn": "PageDown",
|
||||
"esc": "Escape",
|
||||
}
|
||||
|
||||
// Decode decodes a string as a key or combination of keys.
|
||||
func Decode(s string) (mod tcell.ModMask, key tcell.Key, ch rune, err error) {
|
||||
if len(s) == 0 {
|
||||
return 0, 0, 0, errors.New("empty string")
|
||||
}
|
||||
|
||||
// Special case for plus rune decoding
|
||||
if s[len(s)-1:] == "+" {
|
||||
key = tcell.KeyRune
|
||||
ch = '+'
|
||||
|
||||
if len(s) == 1 {
|
||||
return mod, key, ch, nil
|
||||
} else if len(s) == 2 {
|
||||
return 0, 0, 0, fmt.Errorf("invalid key %s", s)
|
||||
} else {
|
||||
s = s[:len(s)-2]
|
||||
}
|
||||
}
|
||||
|
||||
split := strings.Split(s, "+")
|
||||
DECODEPIECE:
|
||||
for _, piece := range split {
|
||||
// Decode modifiers
|
||||
pieceLower := strings.ToLower(piece)
|
||||
switch pieceLower {
|
||||
case LabelCtrl:
|
||||
mod |= tcell.ModCtrl
|
||||
continue
|
||||
case LabelAlt:
|
||||
mod |= tcell.ModAlt
|
||||
continue
|
||||
case LabelMeta:
|
||||
mod |= tcell.ModMeta
|
||||
continue
|
||||
case LabelShift:
|
||||
mod |= tcell.ModShift
|
||||
continue
|
||||
}
|
||||
|
||||
// Decode key
|
||||
for shortKey, fullKey := range fullKeyNames {
|
||||
if pieceLower == strings.ToLower(fullKey) {
|
||||
pieceLower = shortKey
|
||||
break
|
||||
}
|
||||
}
|
||||
switch pieceLower {
|
||||
case "backspace":
|
||||
key = tcell.KeyBackspace2
|
||||
continue
|
||||
case "space", "spacebar":
|
||||
key = tcell.KeyRune
|
||||
ch = ' '
|
||||
continue
|
||||
}
|
||||
for k, keyName := range tcell.KeyNames {
|
||||
if pieceLower == strings.ToLower(keyName) {
|
||||
key = k
|
||||
continue DECODEPIECE
|
||||
}
|
||||
}
|
||||
|
||||
// Decode rune
|
||||
if len(piece) > 1 {
|
||||
return 0, 0, 0, fmt.Errorf("unknown key name or invalid rune: %s", piece)
|
||||
}
|
||||
|
||||
key = tcell.KeyRune
|
||||
ch = rune(piece[0])
|
||||
}
|
||||
|
||||
return mod, key, ch, nil
|
||||
}
|
||||
|
||||
// Encode encodes a key or combination of keys a string.
|
||||
func Encode(mod tcell.ModMask, key tcell.Key, ch rune) (string, error) {
|
||||
var b strings.Builder
|
||||
var wrote bool
|
||||
|
||||
// Encode modifiers
|
||||
if mod&tcell.ModCtrl != 0 {
|
||||
b.WriteString(upperFirst(LabelCtrl))
|
||||
wrote = true
|
||||
}
|
||||
if mod&tcell.ModAlt != 0 {
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteString(upperFirst(LabelAlt))
|
||||
wrote = true
|
||||
}
|
||||
if mod&tcell.ModMeta != 0 {
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteString(upperFirst(LabelMeta))
|
||||
wrote = true
|
||||
}
|
||||
if mod&tcell.ModShift != 0 {
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteString(upperFirst(LabelShift))
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if ch == ' ' {
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteString("Space")
|
||||
} else if key != tcell.KeyRune {
|
||||
// Encode key
|
||||
keyName := tcell.KeyNames[key]
|
||||
if keyName == "" {
|
||||
return "", fmt.Errorf("invalid or unknown key: %d", key)
|
||||
}
|
||||
fullKeyName := fullKeyNames[strings.ToLower(keyName)]
|
||||
if fullKeyName != "" {
|
||||
keyName = fullKeyName
|
||||
}
|
||||
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteString(keyName)
|
||||
} else {
|
||||
// Encode rune
|
||||
if wrote {
|
||||
b.WriteRune('+')
|
||||
}
|
||||
b.WriteRune(ch)
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func upperFirst(s string) string {
|
||||
if len(s) <= 1 {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
return strings.ToUpper(s[:1]) + s[1:]
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package cbind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
mod tcell.ModMask
|
||||
key tcell.Key
|
||||
ch rune
|
||||
encoded string
|
||||
}
|
||||
|
||||
var testCases = []testCase{
|
||||
{mod: tcell.ModNone, key: tcell.KeyRune, ch: 'a', encoded: "a"},
|
||||
{mod: tcell.ModNone, key: tcell.KeyRune, ch: '+', encoded: "+"},
|
||||
{mod: tcell.ModNone, key: tcell.KeyRune, ch: ';', encoded: ";"},
|
||||
{mod: tcell.ModNone, key: tcell.KeyEnter, ch: 0, encoded: "Enter"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyRune, ch: 'a', encoded: "Alt+a"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyRune, ch: '+', encoded: "Alt++"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyRune, ch: ';', encoded: "Alt+;"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyEnter, ch: 0, encoded: "Alt+Enter"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyRune, ch: ' ', encoded: "Alt+Space"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyBackspace2, ch: 0, encoded: "Alt+Backspace"},
|
||||
{mod: tcell.ModAlt, key: tcell.KeyPgDn, ch: 0, encoded: "Alt+PageDown"},
|
||||
{mod: tcell.ModCtrl | tcell.ModAlt, key: tcell.KeyRune, ch: '+', encoded: "Ctrl+Alt++"},
|
||||
{mod: tcell.ModCtrl | tcell.ModShift, key: tcell.KeyRune, ch: '+', encoded: "Ctrl+Shift++"},
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, c := range testCases {
|
||||
encoded, err := Encode(c.mod, c.key, c.ch)
|
||||
if err != nil {
|
||||
t.Errorf("failed to encode key %d %d %d: %s", c.mod, c.key, c.ch, err)
|
||||
}
|
||||
if encoded != c.encoded {
|
||||
t.Errorf("failed to encode key %d %d %d: got %s, want %s", c.mod, c.key, c.ch, encoded, c.encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, c := range testCases {
|
||||
mod, key, ch, err := Decode(c.encoded)
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode key %s: %s", c.encoded, err)
|
||||
}
|
||||
if mod != c.mod {
|
||||
t.Errorf("failed to decode key %s: invalid modifiers: got %d, want %d", c.encoded, mod, c.mod)
|
||||
}
|
||||
if key != c.key {
|
||||
t.Errorf("failed to decode key %s: invalid key: got %d, want %d", c.encoded, key, c.key)
|
||||
}
|
||||
if ch != c.ch {
|
||||
t.Errorf("failed to decode key %s: invalid rune: got %d, want %d", c.encoded, ch, c.ch)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue