From d497a379ca16f0e06bdd9ea251e28031d6a62045 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Sun, 2 Oct 2022 20:18:45 +0200 Subject: [PATCH 1/2] feat: benchmark the key match operation --- key/key_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/key/key_test.go b/key/key_test.go index 359a4c26..44098603 100644 --- a/key/key_test.go +++ b/key/key_test.go @@ -2,6 +2,8 @@ package key import ( "testing" + + tea "github.com/charmbracelet/bubbletea" ) func TestBinding_Enabled(t *testing.T) { @@ -24,3 +26,19 @@ func TestBinding_Enabled(t *testing.T) { t.Errorf("expected key not to be Enabled") } } + +func BenchmarkMatches(b *testing.B) { + msg1 := tea.KeyMsg(tea.Key{Type: tea.KeyRunes, Runes: []rune("c"), Alt: true}) + msg2 := tea.KeyMsg(tea.Key{Type: tea.KeyEnter}) + kb := NewBinding(WithKeys("alt+c")) + b.Run("success", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Matches(msg1, kb) + } + }) + b.Run("fail", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Matches(msg2, kb) + } + }) +} From 43a92fbf0ec6708a3ead34b31b0f9fd71dac86a5 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Sun, 2 Oct 2022 20:21:14 +0200 Subject: [PATCH 2/2] fix: optimize the key binding match operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reduces the complexity of the comparison and gets rid of the heap allocation on every match. Benchmark before/after (`go test -bench . -benchtime 3s -count 5 -benchmem`): ``` name old time/op new time/op delta Matches/success-32 66.9ns ± 1% 15.7ns ± 0% -76.47% (p=0.016 n=5+4) Matches/fail-32 27.9ns ± 0% 12.4ns ± 2% -55.70% (p=0.016 n=4+5) name old alloc/op new alloc/op delta Matches/success-32 5.00B ± 0% 0.00B -100.00% (p=0.008 n=5+5) Matches/fail-32 0.00B 0.00B ~ (all equal) name old allocs/op new allocs/op delta Matches/success-32 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Matches/fail-32 0.00 0.00 ~ (all equal) ``` --- key/key.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/key/key.go b/key/key.go index ac081948..00e96dd4 100644 --- a/key/key.go +++ b/key/key.go @@ -37,13 +37,15 @@ package key import ( + "strings" + tea "github.com/charmbracelet/bubbletea" ) // Binding describes a set of keybindings and, optionally, their associated // help text. type Binding struct { - keys []string + keys []tea.Key help Help disabled bool } @@ -64,7 +66,7 @@ func NewBinding(opts ...BindingOpt) Binding { // WithKeys initializes a keybinding with the given keystrokes. func WithKeys(keys ...string) BindingOpt { return func(b *Binding) { - b.keys = keys + b.SetKeys(keys...) } } @@ -84,12 +86,21 @@ func WithDisabled() BindingOpt { // SetKeys sets the keys for the keybinding. func (b *Binding) SetKeys(keys ...string) { - b.keys = keys + b.keys = make([]tea.Key, 0, len(keys)) + for _, k := range keys { + if tk, ok := MakeKey(k); ok { + b.keys = append(b.keys, tk) + } + } } // Keys returns the keys for the keybinding. func (b Binding) Keys() []string { - return b.keys + kn := make([]string, len(b.keys)) + for i, tk := range b.keys { + kn[i] = tk.String() + } + return kn } // SetHelp sets the help text for the keybinding. @@ -130,13 +141,82 @@ type Help struct { // Matches checks if the given KeyMsg matches the given bindings. func Matches(k tea.KeyMsg, b ...Binding) bool { - keys := k.String() for _, binding := range b { for _, v := range binding.keys { - if keys == v && binding.Enabled() { + if keyEq(v, tea.Key(k)) && binding.Enabled() { return true } } } return false } + +func keyEq(a, b tea.Key) bool { + if a.Type != b.Type { + return false + } + if a.Alt != b.Alt { + return false + } + if len(a.Runes) != len(b.Runes) { + return false + } + for i, ar := range a.Runes { + if b.Runes[i] != ar { + return false + } + } + return true +} + +// MakeKey returns a tea.Key for the given keyName. +func MakeKey(keyName string) (tea.Key, bool) { + alt := false + if strings.HasPrefix(keyName, "alt+") { + alt = true + keyName = keyName[4:] + } + // Is this a special key? + k, ok := allKeys[keyName] + if ok { + k.Alt = alt + return k, true + } + // Not a special key: either a simple key "a" or with an alt + // modifier "alt+a". + r := []rune(keyName) + if len(r) != 1 { + // Caller used a key name which we don't understand, bail. + return tea.Key{}, false + } + return tea.Key{ + Type: tea.KeyRunes, + Runes: r, + Alt: alt, + }, true +} + +// allKeys contains the map of all "special" keys and their +// ctrl/shift/alt combinations. +var allKeys = func() map[string]tea.Key { + result := make(map[string]tea.Key) + for i := 0; ; i++ { + k := tea.Key{Type: tea.KeyType(i)} + keyName := k.String() + // fmt.Println("found key:", keyName) + if keyName == "" { + break + } + result[keyName] = k + } + for i := -2; ; i-- { + k := tea.Key{Type: tea.KeyType(i)} + keyName := k.String() + // fmt.Println("found key:", keyName) + if keyName == "" { + break + } + result[keyName] = k + } + return result +}()