about summary refs log tree commit diff stats
path: root/config
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2019-03-15 01:12:06 -0400
committerDrew DeVault <sir@cmpwn.com>2019-03-15 01:31:23 -0400
commit8d20e9218ece5927d786d6e2fac5c50572fb9c81 (patch)
treee4cdf0ba821dccbb510f169c7731ae78ce987830 /config
parentd274bf926c79ec834afcac00dab3f95f8bd5325f (diff)
downloadaerc-8d20e9218ece5927d786d6e2fac5c50572fb9c81.tar.gz
Implement key bindings subsystem
Which is not yet rigged up
Diffstat (limited to 'config')
-rw-r--r--config/aerc.conf63
-rw-r--r--config/bindings.go284
-rw-r--r--config/bindings_test.go61
-rw-r--r--config/config.go16
4 files changed, 391 insertions, 33 deletions
diff --git a/config/aerc.conf b/config/aerc.conf
index 76b0310..3b29a77 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -91,43 +91,44 @@ alternatives=text/plain,text/html
 
 [lbinds]
 #
-# Binds are of the form <key sequence> = <command to run>
-# To use '=' in a key sequence, substitute it with "Eq": "<Ctrl+Eq>"
-# If you wish to bind #, you can wrap the key sequence in quotes: "#" = quit
-#
-# lbinds are bindings that take effect in the list view
-# mbinds are bindings that take effect in the message view
-q=:quit<Enter>
-<Ctrl+c>=:quit<Enter>
-
-j=:next-message<Enter>
-<Down>=:next-message<Enter>
-<Ctrl+d>=:next-message --scroll 50%<Enter>
-<Ctrl+f>=:next-message --scroll 100%<Enter>
-<PageDown>=:next-message --scroll 100%<Enter>
-<WheelDown>=:next-message --scroll 1<Enter>
-
-k=:previous-message<Enter>
-<Up>=:previous-message<Enter>
-<Ctrl+u>=:previous-message --scroll 50%<Enter>
-<Ctrl+b>=:previous-message --scroll 100%<Enter>
-<PageUp>=:previous-message --scroll 100%<Enter>
-<WheelUp>=:previous-message --scroll 1<Enter>
-g=:select-message 0<Enter>
-G=:select-message -1<Enter>
-
-J=:next-folder<Enter>
-K=:previous-folder<Enter>
+# Binds are of the form <input keys> = <output keys>
+# Pressing <input keys> in sequence will then simulate pressing <output keys>
+#
+# Use <C-*> to refer to control+something.
+#
+# lbinds are effective in the list view
+# mbinds are effective in the message view
+q = :quit<Enter>
+<C-c> = :quit<Enter>
+
+j = :next-message<Enter>
+<Down> = :next-message<Enter>
+<C-d> = :next-message --scroll 50%<Enter>
+<C-f> = :next-message --scroll 100%<Enter>
+<PageDown> = :next-message --scroll 100%<Enter>
+<WheelDown> = :next-message --scroll 1<Enter>
+
+k = :previous-message<Enter>
+<Up> = :previous-message<Enter>
+<C-u> = :previous-message --scroll 50%<Enter>
+<C-b> = :previous-message --scroll 100%<Enter>
+<PageUp> = :previous-message --scroll 100%<Enter>
+<WheelUp> = :previous-message --scroll 1<Enter>
+g = :select-message 0<Enter>
+G = :select-message -1<Enter>
+
+J = :next-folder<Enter>
+K = :previous-folder<Enter>
 l = :next-account<Enter>
 <Right> = :next-account<Enter>
 h = :previous-account<Enter>
 <Left> = :previous-account<Enter>
 
-<Enter>=:view-message<Enter>
-d=:confirm 'Really delete this message?' ':delete-message<Enter>'<Enter>
+<Enter> = :view-message<Enter>
+d = :confirm 'Really delete this message?' ':delete-message<Enter>'<Enter>
 
-c=:cd 
-$=:term-exec 
+c = :cd 
+$ = :term-exec 
 
 [mbinds]
 #
diff --git a/config/bindings.go b/config/bindings.go
new file mode 100644
index 0000000..c10b68f
--- /dev/null
+++ b/config/bindings.go
@@ -0,0 +1,284 @@
+package config
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/gdamore/tcell"
+)
+
+type KeyStroke struct {
+	Key  tcell.Key
+	Rune rune
+}
+
+type Binding struct {
+	Output []KeyStroke
+	Input  []KeyStroke
+}
+
+type KeyBindings []*Binding
+
+const (
+	BINDING_FOUND = iota
+	BINDING_INCOMPLETE
+	BINDING_NOT_FOUND
+)
+
+type BindingSearchResult int
+
+func NewKeyBindings() *KeyBindings {
+	return &KeyBindings{}
+}
+
+func (bindings *KeyBindings) Add(binding *Binding) {
+	// TODO: Search for conflicts?
+	*bindings = append(*bindings, binding)
+}
+
+func (bindings *KeyBindings) GetBinding(
+	input []KeyStroke) (BindingSearchResult, []KeyStroke) {
+
+	incomplete := false
+	// TODO: This could probably be a sorted list to speed things up
+	// TODO: Deal with bindings that share a prefix
+	for _, binding := range *bindings {
+		if len(binding.Input) < len(input) {
+			continue
+		}
+		for i, stroke := range input {
+			if stroke != binding.Input[i] {
+				goto next
+			}
+		}
+		if len(binding.Input) != len(input) {
+			incomplete = true
+		} else {
+			return BINDING_FOUND, binding.Output
+		}
+	next:
+	}
+	if incomplete {
+		return BINDING_INCOMPLETE, nil
+	}
+	return BINDING_NOT_FOUND, nil
+}
+
+var (
+	keyNames map[string]tcell.Key
+)
+
+func ParseKeyStrokes(keystrokes string) ([]KeyStroke, error) {
+	var strokes []KeyStroke
+	buf := bytes.NewBufferString(keystrokes)
+	for {
+		tok, _, err := buf.ReadRune()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return nil, err
+		}
+		// TODO: make it possible to bind to < or > themselves (and default to
+		// switching accounts)
+		switch tok {
+		case '<':
+			name, err := buf.ReadString(byte('>'))
+			if err == io.EOF {
+				return nil, errors.New("Expecting '>'")
+			} else if err != nil {
+				return nil, err
+			} else if name == ">" {
+				return nil, errors.New("Expected a key name")
+			}
+			name = name[:len(name)-1]
+			if key, ok := keyNames[strings.ToLower(name)]; ok {
+				strokes = append(strokes, KeyStroke{
+					Key: key,
+				})
+			} else {
+				return nil, errors.New(fmt.Sprintf("Unknown key '%s'", name))
+			}
+		case '>':
+			return nil, errors.New("Found '>' without '<'")
+		default:
+			strokes = append(strokes, KeyStroke{
+				Key:  tcell.KeyRune,
+				Rune: tok,
+			})
+		}
+	}
+	return strokes, nil
+}
+
+func ParseBinding(input, output string) (*Binding, error) {
+	in, err := ParseKeyStrokes(input)
+	if err != nil {
+		return nil, err
+	}
+	out, err := ParseKeyStrokes(output)
+	if err != nil {
+		return nil, err
+	}
+	return &Binding{
+		Input:  in,
+		Output: out,
+	}, nil
+}
+
+func init() {
+	keyNames = make(map[string]tcell.Key)
+	keyNames["up"] = tcell.KeyUp
+	keyNames["down"] = tcell.KeyDown
+	keyNames["right"] = tcell.KeyRight
+	keyNames["left"] = tcell.KeyLeft
+	keyNames["upleft"] = tcell.KeyUpLeft
+	keyNames["upright"] = tcell.KeyUpRight
+	keyNames["downleft"] = tcell.KeyDownLeft
+	keyNames["downright"] = tcell.KeyDownRight
+	keyNames["center"] = tcell.KeyCenter
+	keyNames["pgup"] = tcell.KeyPgUp
+	keyNames["pgdn"] = tcell.KeyPgDn
+	keyNames["home"] = tcell.KeyHome
+	keyNames["end"] = tcell.KeyEnd
+	keyNames["insert"] = tcell.KeyInsert
+	keyNames["delete"] = tcell.KeyDelete
+	keyNames["help"] = tcell.KeyHelp
+	keyNames["exit"] = tcell.KeyExit
+	keyNames["clear"] = tcell.KeyClear
+	keyNames["cancel"] = tcell.KeyCancel
+	keyNames["print"] = tcell.KeyPrint
+	keyNames["pause"] = tcell.KeyPause
+	keyNames["backtab"] = tcell.KeyBacktab
+	keyNames["f1"] = tcell.KeyF1
+	keyNames["f2"] = tcell.KeyF2
+	keyNames["f3"] = tcell.KeyF3
+	keyNames["f4"] = tcell.KeyF4
+	keyNames["f5"] = tcell.KeyF5
+	keyNames["f6"] = tcell.KeyF6
+	keyNames["f7"] = tcell.KeyF7
+	keyNames["f8"] = tcell.KeyF8
+	keyNames["f9"] = tcell.KeyF9
+	keyNames["f10"] = tcell.KeyF10
+	keyNames["f11"] = tcell.KeyF11
+	keyNames["f12"] = tcell.KeyF12
+	keyNames["f13"] = tcell.KeyF13
+	keyNames["f14"] = tcell.KeyF14
+	keyNames["f15"] = tcell.KeyF15
+	keyNames["f16"] = tcell.KeyF16
+	keyNames["f17"] = tcell.KeyF17
+	keyNames["f18"] = tcell.KeyF18
+	keyNames["f19"] = tcell.KeyF19
+	keyNames["f20"] = tcell.KeyF20
+	keyNames["f21"] = tcell.KeyF21
+	keyNames["f22"] = tcell.KeyF22
+	keyNames["f23"] = tcell.KeyF23
+	keyNames["f24"] = tcell.KeyF24
+	keyNames["f25"] = tcell.KeyF25
+	keyNames["f26"] = tcell.KeyF26
+	keyNames["f27"] = tcell.KeyF27
+	keyNames["f28"] = tcell.KeyF28
+	keyNames["f29"] = tcell.KeyF29
+	keyNames["f30"] = tcell.KeyF30
+	keyNames["f31"] = tcell.KeyF31
+	keyNames["f32"] = tcell.KeyF32
+	keyNames["f33"] = tcell.KeyF33
+	keyNames["f34"] = tcell.KeyF34
+	keyNames["f35"] = tcell.KeyF35
+	keyNames["f36"] = tcell.KeyF36
+	keyNames["f37"] = tcell.KeyF37
+	keyNames["f38"] = tcell.KeyF38
+	keyNames["f39"] = tcell.KeyF39
+	keyNames["f40"] = tcell.KeyF40
+	keyNames["f41"] = tcell.KeyF41
+	keyNames["f42"] = tcell.KeyF42
+	keyNames["f43"] = tcell.KeyF43
+	keyNames["f44"] = tcell.KeyF44
+	keyNames["f45"] = tcell.KeyF45
+	keyNames["f46"] = tcell.KeyF46
+	keyNames["f47"] = tcell.KeyF47
+	keyNames["f48"] = tcell.KeyF48
+	keyNames["f49"] = tcell.KeyF49
+	keyNames["f50"] = tcell.KeyF50
+	keyNames["f51"] = tcell.KeyF51
+	keyNames["f52"] = tcell.KeyF52
+	keyNames["f53"] = tcell.KeyF53
+	keyNames["f54"] = tcell.KeyF54
+	keyNames["f55"] = tcell.KeyF55
+	keyNames["f56"] = tcell.KeyF56
+	keyNames["f57"] = tcell.KeyF57
+	keyNames["f58"] = tcell.KeyF58
+	keyNames["f59"] = tcell.KeyF59
+	keyNames["f60"] = tcell.KeyF60
+	keyNames["f61"] = tcell.KeyF61
+	keyNames["f62"] = tcell.KeyF62
+	keyNames["f63"] = tcell.KeyF63
+	keyNames["f64"] = tcell.KeyF64
+	keyNames["c-space"] = tcell.KeyCtrlSpace
+	keyNames["c-a"] = tcell.KeyCtrlA
+	keyNames["c-b"] = tcell.KeyCtrlB
+	keyNames["c-c"] = tcell.KeyCtrlC
+	keyNames["c-d"] = tcell.KeyCtrlD
+	keyNames["c-e"] = tcell.KeyCtrlE
+	keyNames["c-f"] = tcell.KeyCtrlF
+	keyNames["c-g"] = tcell.KeyCtrlG
+	keyNames["c-h"] = tcell.KeyCtrlH
+	keyNames["c-i"] = tcell.KeyCtrlI
+	keyNames["c-j"] = tcell.KeyCtrlJ
+	keyNames["c-k"] = tcell.KeyCtrlK
+	keyNames["c-l"] = tcell.KeyCtrlL
+	keyNames["c-m"] = tcell.KeyCtrlM
+	keyNames["c-n"] = tcell.KeyCtrlN
+	keyNames["c-o"] = tcell.KeyCtrlO
+	keyNames["c-p"] = tcell.KeyCtrlP
+	keyNames["c-q"] = tcell.KeyCtrlQ
+	keyNames["c-r"] = tcell.KeyCtrlR
+	keyNames["c-s"] = tcell.KeyCtrlS
+	keyNames["c-t"] = tcell.KeyCtrlT
+	keyNames["c-u"] = tcell.KeyCtrlU
+	keyNames["c-v"] = tcell.KeyCtrlV
+	keyNames["c-w"] = tcell.KeyCtrlW
+	keyNames["c-x"] = tcell.KeyCtrlX
+	keyNames["c-y"] = tcell.KeyCtrlY
+	keyNames["c-z"] = tcell.KeyCtrlZ
+	keyNames["c-]"] = tcell.KeyCtrlLeftSq
+	keyNames["c-\\"] = tcell.KeyCtrlBackslash
+	keyNames["c-["] = tcell.KeyCtrlRightSq
+	keyNames["c-^"] = tcell.KeyCtrlCarat
+	keyNames["c-_"] = tcell.KeyCtrlUnderscore
+	keyNames["NUL"] = tcell.KeyNUL
+	keyNames["SOH"] = tcell.KeySOH
+	keyNames["STX"] = tcell.KeySTX
+	keyNames["ETX"] = tcell.KeyETX
+	keyNames["EOT"] = tcell.KeyEOT
+	keyNames["ENQ"] = tcell.KeyENQ
+	keyNames["ACK"] = tcell.KeyACK
+	keyNames["BEL"] = tcell.KeyBEL
+	keyNames["BS"] = tcell.KeyBS
+	keyNames["TAB"] = tcell.KeyTAB
+	keyNames["LF"] = tcell.KeyLF
+	keyNames["VT"] = tcell.KeyVT
+	keyNames["FF"] = tcell.KeyFF
+	keyNames["CR"] = tcell.KeyCR
+	keyNames["SO"] = tcell.KeySO
+	keyNames["SI"] = tcell.KeySI
+	keyNames["DLE"] = tcell.KeyDLE
+	keyNames["DC1"] = tcell.KeyDC1
+	keyNames["DC2"] = tcell.KeyDC2
+	keyNames["DC3"] = tcell.KeyDC3
+	keyNames["DC4"] = tcell.KeyDC4
+	keyNames["NAK"] = tcell.KeyNAK
+	keyNames["SYN"] = tcell.KeySYN
+	keyNames["ETB"] = tcell.KeyETB
+	keyNames["CAN"] = tcell.KeyCAN
+	keyNames["EM"] = tcell.KeyEM
+	keyNames["SUB"] = tcell.KeySUB
+	keyNames["ESC"] = tcell.KeyESC
+	keyNames["FS"] = tcell.KeyFS
+	keyNames["GS"] = tcell.KeyGS
+	keyNames["RS"] = tcell.KeyRS
+	keyNames["US"] = tcell.KeyUS
+	keyNames["DEL"] = tcell.KeyDEL
+}
diff --git a/config/bindings_test.go b/config/bindings_test.go
new file mode 100644
index 0000000..1d1cbfe
--- /dev/null
+++ b/config/bindings_test.go
@@ -0,0 +1,61 @@
+package config
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gdamore/tcell"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetBinding(t *testing.T) {
+	assert := assert.New(t)
+
+	bindings := NewKeyBindings()
+	add := func(binding, cmd string) {
+		b, _ := ParseBinding(binding, cmd)
+		bindings.Add(b)
+	}
+
+	add("abc", ":abc")
+	add("cba", ":cba")
+	add("foo", ":foo")
+	add("bar", ":bar")
+
+	test := func(input []KeyStroke, result int, output string) {
+		_output, _ := ParseKeyStrokes(output)
+		r, out := bindings.GetBinding(input)
+		assert.Equal(result, int(r), fmt.Sprintf(
+			"%s: Expected result %d, got %d", output, result, r))
+		assert.Equal(_output, out, fmt.Sprintf(
+			"%s: Expected output %v, got %v", output, _output, out))
+	}
+
+	test([]KeyStroke{
+		{tcell.KeyRune, 'a'},
+	}, BINDING_INCOMPLETE, "")
+	test([]KeyStroke{
+		{tcell.KeyRune, 'a'},
+		{tcell.KeyRune, 'b'},
+		{tcell.KeyRune, 'c'},
+	}, BINDING_FOUND, ":abc")
+	test([]KeyStroke{
+		{tcell.KeyRune, 'c'},
+		{tcell.KeyRune, 'b'},
+		{tcell.KeyRune, 'a'},
+	}, BINDING_FOUND, ":cba")
+	test([]KeyStroke{
+		{tcell.KeyRune, 'f'},
+		{tcell.KeyRune, 'o'},
+	}, BINDING_INCOMPLETE, "")
+	test([]KeyStroke{
+		{tcell.KeyRune, '4'},
+		{tcell.KeyRune, '0'},
+		{tcell.KeyRune, '4'},
+	}, BINDING_NOT_FOUND, "")
+
+	add("<C-a>", "c-a")
+	test([]KeyStroke{
+		{tcell.KeyCtrlA, 0},
+	}, BINDING_FOUND, "c-a")
+}
diff --git a/config/config.go b/config/config.go
index 142a1e0..ff0e094 100644
--- a/config/config.go
+++ b/config/config.go
@@ -29,6 +29,7 @@ type AccountConfig struct {
 }
 
 type AercConfig struct {
+	Lbinds   *KeyBindings
 	Ini      *ini.File       `ini:"-"`
 	Accounts []AccountConfig `ini:"-"`
 	Ui       UIConfig
@@ -94,7 +95,9 @@ func LoadConfig(root *string) (*AercConfig, error) {
 	}
 	file.NameMapper = mapName
 	config := &AercConfig{
-		Ini: file,
+		Lbinds: NewKeyBindings(),
+		Ini:    file,
+
 		Ui: UIConfig{
 			IndexFormat:     "%4C %Z %D %-17.17n %s",
 			TimestampFormat: "%F %l:%M %p",
@@ -110,9 +113,18 @@ func LoadConfig(root *string) (*AercConfig, error) {
 			EmptyMessage:      "(no messages)",
 		},
 	}
-	if ui, err := file.GetSection("ui"); err != nil {
+	if ui, err := file.GetSection("ui"); err == nil {
 		ui.MapTo(config.Ui)
 	}
+	if lbinds, err := file.GetSection("lbinds"); err == nil {
+		for key, value := range lbinds.KeysHash() {
+			binding, err := ParseBinding(key, value)
+			if err != nil {
+				return nil, err
+			}
+			config.Lbinds.Add(binding)
+		}
+	}
 	accountsPath := path.Join(*root, "accounts.conf")
 	if accounts, err := loadAccountConfig(accountsPath); err != nil {
 		return nil, err