From 7c5bfd27a395d113d72051ec7fcbdbe7eb45b680 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 25 Aug 2022 22:14:36 +1000 Subject: [PATCH] issue-9: finishing keybinding service and implemented controller Have now got rebinding keys working with the "rebind" command. Still need to make sure key names are correct and implement rebinding as part of an RC file and add bindings for the table. --- cmd/dynamo-browse/main.go | 6 +- .../dynamo-browse/controllers/keybinding.go | 38 +++++++ .../services/keybindings/errors.go | 14 +++ .../services/keybindings/service.go | 106 ++++++++++++++++-- internal/dynamo-browse/ui/keybindings.go | 2 +- internal/dynamo-browse/ui/model.go | 10 +- 6 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 internal/dynamo-browse/controllers/keybinding.go create mode 100644 internal/dynamo-browse/services/keybindings/errors.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 76257cb..5d17f84 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -98,15 +98,17 @@ func main() { }, } - commandController := commandctrl.NewCommandController() keyBindingService := keybindings.NewService(defaultKeyBindings) - _ = keyBindingService + keyBindingController := controllers.NewKeyBindingController(keyBindingService) + + commandController := commandctrl.NewCommandController() model := ui.NewModel( tableReadController, tableWriteController, itemRendererService, commandController, + keyBindingController, defaultKeyBindings, ) diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go new file mode 100644 index 0000000..c665bc8 --- /dev/null +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -0,0 +1,38 @@ +package controllers + +import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/audax/internal/common/ui/events" + "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" + "github.com/pkg/errors" +) + +type KeyBindingController struct { + service *keybindings.Service +} + +func NewKeyBindingController(service *keybindings.Service) *KeyBindingController { + return &KeyBindingController{service: service} +} + +func (kb *KeyBindingController) Rebind(newKey string, bindingName string) tea.Msg { + err := kb.service.Rebind(newKey, bindingName, false) + if err == nil { + return events.SetStatus(fmt.Sprintf("Key '%v' now bound to '%v'", newKey, bindingName)) + } + + var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError + if errors.As(err, &keyAlreadyBoundErr) { + promptMsg := fmt.Sprintf("Key '%v' already bound to '%v'. Continue? ", keyAlreadyBoundErr.Key, keyAlreadyBoundErr.ExistingBindingName) + return events.Confirm(promptMsg, func() tea.Msg { + err := kb.service.Rebind(newKey, bindingName, true) + if err != nil { + return events.Error(err) + } + return events.SetStatus(fmt.Sprintf("Key '%v' now bound to '%v'", newKey, bindingName)) + }) + } + + return events.Error(err) +} diff --git a/internal/dynamo-browse/services/keybindings/errors.go b/internal/dynamo-browse/services/keybindings/errors.go new file mode 100644 index 0000000..c989ffc --- /dev/null +++ b/internal/dynamo-browse/services/keybindings/errors.go @@ -0,0 +1,14 @@ +package keybindings + +import ( + "fmt" +) + +type KeyAlreadyBoundError struct { + Key string + ExistingBindingName string +} + +func (e KeyAlreadyBoundError) Error() string { + return fmt.Sprintf("key '%v' already bound to '%v'", e.Key, e.ExistingBindingName) +} diff --git a/internal/dynamo-browse/services/keybindings/service.go b/internal/dynamo-browse/services/keybindings/service.go index 7535f49..26943ec 100644 --- a/internal/dynamo-browse/services/keybindings/service.go +++ b/internal/dynamo-browse/services/keybindings/service.go @@ -1,6 +1,11 @@ package keybindings -import "reflect" +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/pkg/errors" + "reflect" + "strings" +) type Service struct { keyBindingValue reflect.Value @@ -17,16 +22,95 @@ func NewService(keyBinding any) *Service { } } -func (s *Service) Rebind(name string, key string) error { +func (s *Service) Rebind(newKey string, name string, force bool) error { + // Check if there already exists a binding + if !force { + var foundBinding = "" + s.walkBindingFields(func(bindingName string, binding key.Binding) bool { + for _, key := range binding.Keys() { + if key == newKey { + foundBinding = bindingName + return false + } + } + return true + }) -} - -func (s *Service) findFieldForBinding(name string) reflect.Value { - -} - -func (s *Service) findFieldForBindingInGroup(group reflect.Value, name string) reflect.Value { - for i := 0; i < group.NumField(); i++ { - group.Field(i).Type(). + if foundBinding != "" { + return KeyAlreadyBoundError{Key: newKey, ExistingBindingName: foundBinding} + } } + + // Rebind + binding := s.findFieldForBinding(name) + if binding == nil { + return errors.Errorf("invalid binding: %v", name) + } + + *binding = key.NewBinding(key.WithKeys(newKey)) + return nil +} + +func (s *Service) findFieldForBinding(name string) *key.Binding { + return s.findFieldForBindingInGroup(s.keyBindingValue, name) +} + +func (s *Service) findFieldForBindingInGroup(group reflect.Value, name string) *key.Binding { + bindingName, bindingSuffix, _ := strings.Cut(name, ".") + + groupType := group.Type() + for i := 0; i < group.NumField(); i++ { + fieldType := groupType.Field(i) + + keymapTag := fieldType.Tag.Get("keymap") + if keymapTag != bindingName { + continue + } + + if fieldType.Type.Kind() == reflect.Pointer && fieldType.Type.Elem().Kind() == reflect.Struct { + return s.findFieldForBindingInGroup(group.Field(i).Elem(), bindingSuffix) + } + + binding, isBinding := group.Field(i).Addr().Interface().(*key.Binding) + if !isBinding { + return nil + } + return binding + } + return nil +} + +func (s *Service) walkBindingFields(fn func(name string, binding key.Binding) bool) { + s.walkBindingFieldsInGroup(s.keyBindingValue, "", fn) +} + +func (s *Service) walkBindingFieldsInGroup(group reflect.Value, prefix string, fn func(name string, binding key.Binding) bool) bool { + groupType := group.Type() + for i := 0; i < group.NumField(); i++ { + fieldType := groupType.Field(i) + + keymapTag := fieldType.Tag.Get("keymap") + + var fullName string + if prefix != "" { + fullName = prefix + "." + keymapTag + } else { + fullName = keymapTag + } + + if fieldType.Type.Kind() == reflect.Pointer && fieldType.Type.Elem().Kind() == reflect.Struct { + if !s.walkBindingFieldsInGroup(group.Field(i).Elem(), fullName, fn) { + return false + } + } + + binding, isBinding := group.Field(i).Addr().Interface().(*key.Binding) + if !isBinding { + continue + } + if !fn(fullName, *binding) { + return false + } + } + return true } diff --git a/internal/dynamo-browse/ui/keybindings.go b/internal/dynamo-browse/ui/keybindings.go index b56d454..f7a6674 100644 --- a/internal/dynamo-browse/ui/keybindings.go +++ b/internal/dynamo-browse/ui/keybindings.go @@ -3,7 +3,7 @@ package ui import "github.com/charmbracelet/bubbles/key" type KeyBindings struct { - View *ViewKeyBindings `keymap:"view,group"` + View *ViewKeyBindings `keymap:"view"` } type ViewKeyBindings struct { diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index c1c4177..4216a6e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -54,6 +54,7 @@ func NewModel( wc *controllers.TableWriteController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, + keyBindingController *controllers.KeyBindingController, defaultKeyMap *KeyBindings, ) Model { uiStyles := styles.DefaultStyles @@ -129,9 +130,12 @@ func NewModel( return wc.NoisyTouchItem(dtv.SelectedItemIndex()) }, - //"rebind": func(args []string) tea.Msg { - // - //}, + "rebind": func(args []string) tea.Msg { + if len(args) != 2 { + return events.Error(errors.New("expected: name newKey")) + } + return keyBindingController.Rebind(args[0], args[1]) + }, // Aliases "sa": cc.Alias("set-attr"),