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"),