From 2f89610c51ac4e08b40cdbc450e9a7c22fb6350a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 24 Aug 2022 22:06:29 +1000 Subject: [PATCH 1/5] issue-9: moved keybindings out into a separate type Also started working on a service which can be used to rebind keys using reflection. --- cmd/dynamo-browse/main.go | 29 +++++++++++++++- internal/dynamo-browse/controllers/events.go | 2 +- .../services/keybindings/service.go | 32 ++++++++++++++++++ internal/dynamo-browse/ui/keybindings.go | 21 ++++++++++++ internal/dynamo-browse/ui/model.go | 33 +++++++++++-------- 5 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 internal/dynamo-browse/services/keybindings/service.go create mode 100644 internal/dynamo-browse/ui/keybindings.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index cafbe4f..76257cb 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lmika/audax/internal/common/ui/commandctrl" @@ -16,6 +17,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" "github.com/lmika/audax/internal/dynamo-browse/services/tables" workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" "github.com/lmika/audax/internal/dynamo-browse/ui" @@ -80,8 +82,33 @@ func main() { tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true) tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController) + defaultKeyBindings := &ui.KeyBindings{ + View: &ui.ViewKeyBindings{ + Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")), + CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")), + Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")), + PromptForQuery: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")), + PromptForFilter: key.NewBinding(key.WithKeys("f"), key.WithHelp("/", "filter")), + ViewBack: key.NewBinding(key.WithKeys("backspace"), key.WithHelp("backspace", "go back")), + ViewForward: key.NewBinding(key.WithKeys("\\"), key.WithHelp("\\", "go forward")), + CycleLayoutForward: key.NewBinding(key.WithKeys("w"), key.WithHelp("w", "cycle layout forward")), + CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")), + PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")), + Quit: key.NewBinding(key.WithKeys("ctrl+c", "esc"), key.WithHelp("ctrl+c/esc", "quit")), + }, + } + commandController := commandctrl.NewCommandController() - model := ui.NewModel(tableReadController, tableWriteController, itemRendererService, commandController) + keyBindingService := keybindings.NewService(defaultKeyBindings) + _ = keyBindingService + + model := ui.NewModel( + tableReadController, + tableWriteController, + itemRendererService, + commandController, + defaultKeyBindings, + ) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. lipgloss.HasDarkBackground() diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 9e1bbae..7896902 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -27,7 +27,7 @@ func (rs NewResultSet) ModeMessage() string { } if rs.currentFilter != "" { - modeLine = fmt.Sprintf("%v - Filter: '%v'", modeLine, rs.currentFilter) + modeLine = fmt.Sprintf("%v - PromptForFilter: '%v'", modeLine, rs.currentFilter) } return modeLine } diff --git a/internal/dynamo-browse/services/keybindings/service.go b/internal/dynamo-browse/services/keybindings/service.go new file mode 100644 index 0000000..7535f49 --- /dev/null +++ b/internal/dynamo-browse/services/keybindings/service.go @@ -0,0 +1,32 @@ +package keybindings + +import "reflect" + +type Service struct { + keyBindingValue reflect.Value +} + +func NewService(keyBinding any) *Service { + v := reflect.ValueOf(keyBinding) + if v.Kind() != reflect.Pointer { + panic("keyBinding must be a pointer to a struct") + } + + return &Service{ + keyBindingValue: v.Elem(), + } +} + +func (s *Service) Rebind(name string, key string) error { + +} + +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(). + } +} diff --git a/internal/dynamo-browse/ui/keybindings.go b/internal/dynamo-browse/ui/keybindings.go new file mode 100644 index 0000000..b56d454 --- /dev/null +++ b/internal/dynamo-browse/ui/keybindings.go @@ -0,0 +1,21 @@ +package ui + +import "github.com/charmbracelet/bubbles/key" + +type KeyBindings struct { + View *ViewKeyBindings `keymap:"view,group"` +} + +type ViewKeyBindings struct { + Mark key.Binding `keymap:"mark"` + CopyItemToClipboard key.Binding `keymap:"copy-item-to-clipboard"` + Rescan key.Binding `keymap:"rescan"` + PromptForQuery key.Binding `keymap:"prompt-for-query"` + PromptForFilter key.Binding `keymap:"prompt-for-filter"` + ViewBack key.Binding `keymap:"view-back"` + ViewForward key.Binding `keymap:"view-forward"` + CycleLayoutForward key.Binding `keymap:"cycle-layout-forward"` + CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"` + PromptForCommand key.Binding `keymap:"prompt-for-command"` + Quit key.Binding `keymap:"quit"` +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index c5584c3..c1c4177 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,6 +1,7 @@ package ui import ( + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/audax/internal/common/ui/commandctrl" "github.com/lmika/audax/internal/common/ui/events" @@ -45,6 +46,7 @@ type Model struct { tableView *dynamotableview.Model itemView *dynamoitemview.Model mainView tea.Model + keyMap *ViewKeyBindings } func NewModel( @@ -52,6 +54,7 @@ func NewModel( wc *controllers.TableWriteController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, + defaultKeyMap *KeyBindings, ) Model { uiStyles := styles.DefaultStyles @@ -126,6 +129,10 @@ func NewModel( return wc.NoisyTouchItem(dtv.SelectedItemIndex()) }, + //"rebind": func(args []string) tea.Msg { + // + //}, + // Aliases "sa": cc.Alias("set-attr"), "da": cc.Alias("del-attr"), @@ -147,6 +154,7 @@ func NewModel( tableView: dtv, itemView: div, mainView: mainView, + keyMap: defaultKeyMap.View, } } @@ -163,40 +171,39 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.tableView.Refresh() case tea.KeyMsg: if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() { - log.Printf("key = %+v", msg) - switch msg.String() { - case "m": + switch { + case key.Matches(msg, m.keyMap.Mark): if idx := m.tableView.SelectedItemIndex(); idx >= 0 { return m, func() tea.Msg { return m.tableWriteController.ToggleMark(idx) } } - case "c": + case key.Matches(msg, m.keyMap.CopyItemToClipboard): if idx := m.tableView.SelectedItemIndex(); idx >= 0 { return m, func() tea.Msg { return m.tableReadController.CopyItemToClipboard(idx) } } - case "R": + case key.Matches(msg, m.keyMap.Rescan): return m, m.tableReadController.Rescan - case "?": + case key.Matches(msg, m.keyMap.PromptForQuery): return m, m.tableReadController.PromptForQuery - case "/": + case key.Matches(msg, m.keyMap.PromptForFilter): return m, m.tableReadController.Filter - case "backspace": + case key.Matches(msg, m.keyMap.ViewBack): return m, m.tableReadController.ViewBack - case "\\": + case key.Matches(msg, m.keyMap.ViewForward): return m, m.tableReadController.ViewForward - case "w": + case key.Matches(msg, m.keyMap.CycleLayoutForward): return m, func() tea.Msg { return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, 1, ViewModeCount)} } - case "W": + case key.Matches(msg, m.keyMap.CycleLayoutBackwards): return m, func() tea.Msg { return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, -1, ViewModeCount)} } //case "e": // m.itemEdit.Visible() // return m, nil - case ":": + case key.Matches(msg, m.keyMap.PromptForCommand): return m, m.commandController.Prompt - case "ctrl+c", "esc": + case key.Matches(msg, m.keyMap.Quit): return m, tea.Quit } } From 7c5bfd27a395d113d72051ec7fcbdbe7eb45b680 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 25 Aug 2022 22:14:36 +1000 Subject: [PATCH 2/5] 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"), From d9c9e5d845c3057561d32f09d9311daf4ebf2c84 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 25 Aug 2022 22:31:33 +1000 Subject: [PATCH 3/5] issue-9: added clearing of existing bindings for keys --- cmd/dynamo-browse/main.go | 25 +++---------- .../dynamo-browse/controllers/keybinding.go | 10 +++--- .../services/keybindings/service.go | 36 +++++++++++-------- .../dynamo-browse/ui/keybindings/defaults.go | 31 ++++++++++++++++ .../ui/{ => keybindings}/keybindings.go | 16 +++++++-- internal/dynamo-browse/ui/model.go | 7 ++-- .../ui/teamodels/dynamotableview/model.go | 27 +++----------- 7 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 internal/dynamo-browse/ui/keybindings/defaults.go rename internal/dynamo-browse/ui/{ => keybindings}/keybindings.go (61%) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 5d17f84..2a7cc37 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lmika/audax/internal/common/ui/commandctrl" @@ -17,10 +16,11 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" + keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" "github.com/lmika/audax/internal/dynamo-browse/services/tables" workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" "github.com/lmika/audax/internal/dynamo-browse/ui" + "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" "github.com/lmika/gopkgs/cli" "log" @@ -81,24 +81,9 @@ func main() { state := controllers.NewState() tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true) tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController) + keyBindings := keybindings.Default() - defaultKeyBindings := &ui.KeyBindings{ - View: &ui.ViewKeyBindings{ - Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")), - CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")), - Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")), - PromptForQuery: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")), - PromptForFilter: key.NewBinding(key.WithKeys("f"), key.WithHelp("/", "filter")), - ViewBack: key.NewBinding(key.WithKeys("backspace"), key.WithHelp("backspace", "go back")), - ViewForward: key.NewBinding(key.WithKeys("\\"), key.WithHelp("\\", "go forward")), - CycleLayoutForward: key.NewBinding(key.WithKeys("w"), key.WithHelp("w", "cycle layout forward")), - CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")), - PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")), - Quit: key.NewBinding(key.WithKeys("ctrl+c", "esc"), key.WithHelp("ctrl+c/esc", "quit")), - }, - } - - keyBindingService := keybindings.NewService(defaultKeyBindings) + keyBindingService := keybindings_service.NewService(keyBindings) keyBindingController := controllers.NewKeyBindingController(keyBindingService) commandController := commandctrl.NewCommandController() @@ -109,7 +94,7 @@ func main() { itemRendererService, commandController, keyBindingController, - defaultKeyBindings, + keyBindings, ) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go index c665bc8..79aa578 100644 --- a/internal/dynamo-browse/controllers/keybinding.go +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -16,21 +16,21 @@ 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) +func (kb *KeyBindingController) Rebind(bindingName string, newKey string) tea.Msg { + err := kb.service.Rebind(bindingName, newKey, false) if err == nil { - return events.SetStatus(fmt.Sprintf("Key '%v' now bound to '%v'", newKey, bindingName)) + return events.SetStatus(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey)) } 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) + err := kb.service.Rebind(bindingName, newKey, true) if err != nil { return events.Error(err) } - return events.SetStatus(fmt.Sprintf("Key '%v' now bound to '%v'", newKey, bindingName)) + return events.SetStatus(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey)) }) } diff --git a/internal/dynamo-browse/services/keybindings/service.go b/internal/dynamo-browse/services/keybindings/service.go index 26943ec..c37de2d 100644 --- a/internal/dynamo-browse/services/keybindings/service.go +++ b/internal/dynamo-browse/services/keybindings/service.go @@ -3,6 +3,7 @@ package keybindings import ( "github.com/charmbracelet/bubbles/key" "github.com/pkg/errors" + "log" "reflect" "strings" ) @@ -22,23 +23,28 @@ func NewService(keyBinding any) *Service { } } -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 { +func (s *Service) Rebind(name string, newKey string, force bool) error { + // Check if there already exists a binding (or clear it) + var foundBinding = "" + s.walkBindingFields(func(bindingName string, binding *key.Binding) bool { + for _, boundKey := range binding.Keys() { + if boundKey == newKey { + if force { + // TODO: only filter out "boundKey" rather clear + log.Printf("clearing binding of %v", bindingName) + *binding = key.NewBinding() + return true + } else { foundBinding = bindingName return false } } - return true - }) - - if foundBinding != "" { - return KeyAlreadyBoundError{Key: newKey, ExistingBindingName: foundBinding} } + return true + }) + + if foundBinding != "" { + return KeyAlreadyBoundError{Key: newKey, ExistingBindingName: foundBinding} } // Rebind @@ -80,11 +86,11 @@ func (s *Service) findFieldForBindingInGroup(group reflect.Value, name string) * return nil } -func (s *Service) walkBindingFields(fn func(name string, binding key.Binding) bool) { +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 { +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) @@ -108,7 +114,7 @@ func (s *Service) walkBindingFieldsInGroup(group reflect.Value, prefix string, f if !isBinding { continue } - if !fn(fullName, *binding) { + if !fn(fullName, binding) { return false } } diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go new file mode 100644 index 0000000..0a0738a --- /dev/null +++ b/internal/dynamo-browse/ui/keybindings/defaults.go @@ -0,0 +1,31 @@ +package keybindings + +import "github.com/charmbracelet/bubbles/key" + +func Default() *KeyBindings { + return &KeyBindings{ + TableView: &TableKeyBinding{ + MoveUp: key.NewBinding(key.WithKeys("i", "up")), + MoveDown: key.NewBinding(key.WithKeys("k", "down")), + PageUp: key.NewBinding(key.WithKeys("I", "pgup")), + PageDown: key.NewBinding(key.WithKeys("K", "pgdown")), + Home: key.NewBinding(key.WithKeys("0", "home")), + End: key.NewBinding(key.WithKeys("$", "end")), + ColLeft: key.NewBinding(key.WithKeys("j", "left")), + ColRight: key.NewBinding(key.WithKeys("l", "right")), + }, + View: &ViewKeyBindings{ + Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")), + CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")), + Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")), + PromptForQuery: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")), + PromptForFilter: key.NewBinding(key.WithKeys("f"), key.WithHelp("/", "filter")), + ViewBack: key.NewBinding(key.WithKeys("backspace"), key.WithHelp("backspace", "go back")), + ViewForward: key.NewBinding(key.WithKeys("\\"), key.WithHelp("\\", "go forward")), + CycleLayoutForward: key.NewBinding(key.WithKeys("w"), key.WithHelp("w", "cycle layout forward")), + CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")), + PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")), + Quit: key.NewBinding(key.WithKeys("ctrl+c", "esc"), key.WithHelp("ctrl+c/esc", "quit")), + }, + } +} diff --git a/internal/dynamo-browse/ui/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go similarity index 61% rename from internal/dynamo-browse/ui/keybindings.go rename to internal/dynamo-browse/ui/keybindings/keybindings.go index f7a6674..4842c55 100644 --- a/internal/dynamo-browse/ui/keybindings.go +++ b/internal/dynamo-browse/ui/keybindings/keybindings.go @@ -1,9 +1,21 @@ -package ui +package keybindings import "github.com/charmbracelet/bubbles/key" type KeyBindings struct { - View *ViewKeyBindings `keymap:"view"` + TableView *TableKeyBinding `keymap:"item-table"` + View *ViewKeyBindings `keymap:"view"` +} + +type TableKeyBinding struct { + MoveUp key.Binding `keymap:"move-up"` + MoveDown key.Binding `keymap:"move-down"` + PageUp key.Binding `keymap:"page-up"` + PageDown key.Binding `keymap:"page-down"` + Home key.Binding `keymap:"goto-top"` + End key.Binding `keymap:"goto-bottom"` + ColLeft key.Binding `keymap:"move-left"` + ColRight key.Binding `keymap:"move-right"` } type ViewKeyBindings struct { diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 4216a6e..ac056ee 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -8,6 +8,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dialogprompt" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemedit" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview" @@ -46,7 +47,7 @@ type Model struct { tableView *dynamotableview.Model itemView *dynamoitemview.Model mainView tea.Model - keyMap *ViewKeyBindings + keyMap *keybindings.ViewKeyBindings } func NewModel( @@ -55,11 +56,11 @@ func NewModel( itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, keyBindingController *controllers.KeyBindingController, - defaultKeyMap *KeyBindings, + defaultKeyMap *keybindings.KeyBindings, ) Model { uiStyles := styles.DefaultStyles - dtv := dynamotableview.New(uiStyles) + dtv := dynamotableview.New(defaultKeyMap.TableView, uiStyles) div := dynamoitemview.New(itemRendererService, uiStyles) mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div) diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index 5e515c7..e70092b 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" @@ -20,22 +21,11 @@ var ( Background(lipgloss.Color("#4479ff")) ) -type KeyBinding struct { - MoveUp key.Binding - MoveDown key.Binding - PageUp key.Binding - PageDown key.Binding - Home key.Binding - End key.Binding - ColLeft key.Binding - ColRight key.Binding -} - type Model struct { frameTitle frame.FrameTitle table table.Model w, h int - keyBinding KeyBinding + keyBinding *keybindings.TableKeyBinding // model state colOffset int @@ -43,7 +33,7 @@ type Model struct { resultSet *models.ResultSet } -func New(uiStyles styles.Styles) *Model { +func New(keyBinding *keybindings.TableKeyBinding, uiStyles styles.Styles) *Model { tbl := table.New(table.SimpleColumns([]string{"pk", "sk"}), 100, 100) rows := make([]table.Row, 0) tbl.SetRows(rows) @@ -53,16 +43,7 @@ func New(uiStyles styles.Styles) *Model { return &Model{ frameTitle: frameTitle, table: tbl, - keyBinding: KeyBinding{ - MoveUp: key.NewBinding(key.WithKeys("i", "up")), - MoveDown: key.NewBinding(key.WithKeys("k", "down")), - PageUp: key.NewBinding(key.WithKeys("I", "pgup")), - PageDown: key.NewBinding(key.WithKeys("K", "pgdown")), - Home: key.NewBinding(key.WithKeys("0", "home")), - End: key.NewBinding(key.WithKeys("$", "end")), - ColLeft: key.NewBinding(key.WithKeys("j", "left")), - ColRight: key.NewBinding(key.WithKeys("l", "right")), - }, + keyBinding: keyBinding, } } From 24304d21c3de681fdb9738c162ffe1c242f1c9b5 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 28 Aug 2022 10:54:25 +1000 Subject: [PATCH 4/5] issue-9: added the RC file Added the RC file which can be used to load commands on startup. Also added the "echo" command which can be used for debugging. --- cmd/slog-view/main.go | 2 +- cmd/ssm-browse/main.go | 2 +- internal/common/ui/commandctrl/commandctrl.go | 58 ++++++++++++++++--- internal/common/ui/commandctrl/context.go | 16 +---- internal/common/ui/commandctrl/types.go | 8 +-- .../dynamo-browse/controllers/keybinding.go | 6 +- internal/dynamo-browse/ui/model.go | 40 +++++++++---- internal/ssm-browse/ui/model.go | 2 +- 8 files changed, 92 insertions(+), 42 deletions(-) diff --git a/cmd/slog-view/main.go b/cmd/slog-view/main.go index 94d4ddb..c94acc3 100644 --- a/cmd/slog-view/main.go +++ b/cmd/slog-view/main.go @@ -33,7 +33,7 @@ func main() { ctrl := controllers.NewLogFileController(service, flag.Arg(0)) cmdController := commandctrl.NewCommandController() - //cmdController.AddCommands(&commandctrl.CommandContext{ + //cmdController.AddCommands(&commandctrl.CommandList{ // Commands: map[string]commandctrl.Command{ // "cd": func(args []string) tea.Cmd { // return ctrl.ChangePrefix(args[0]) diff --git a/cmd/ssm-browse/main.go b/cmd/ssm-browse/main.go index c5e46c9..e1dab01 100644 --- a/cmd/ssm-browse/main.go +++ b/cmd/ssm-browse/main.go @@ -48,7 +48,7 @@ func main() { ctrl := controllers.New(service) cmdController := commandctrl.NewCommandController() - cmdController.AddCommands(&commandctrl.CommandContext{ + cmdController.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ "cd": func(args []string) tea.Msg { return ctrl.ChangePrefix(args[0]) diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index f26ab6c..65cc0ae 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -1,9 +1,13 @@ package commandctrl import ( + "bufio" + "bytes" tea "github.com/charmbracelet/bubbletea" "github.com/pkg/errors" "log" + "os" + "path/filepath" "strings" "github.com/lmika/audax/internal/common/ui/events" @@ -11,7 +15,7 @@ import ( ) type CommandController struct { - commandList *CommandContext + commandList *CommandList } func NewCommandController() *CommandController { @@ -20,7 +24,7 @@ func NewCommandController() *CommandController { } } -func (c *CommandController) AddCommands(ctx *CommandContext) { +func (c *CommandController) AddCommands(ctx *CommandList) { ctx.parent = c.commandList c.commandList = ctx } @@ -35,6 +39,10 @@ func (c *CommandController) Prompt() tea.Msg { } func (c *CommandController) Execute(commandInput string) tea.Msg { + return c.execute(ExecContext{FromFile: false}, commandInput) +} + +func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Msg { input := strings.TrimSpace(commandInput) if input == "" { return nil @@ -43,31 +51,65 @@ func (c *CommandController) Execute(commandInput string) tea.Msg { tokens := shellwords.Split(input) command := c.lookupCommand(tokens[0]) if command == nil { - log.Println("No such command: ", tokens) return events.Error(errors.New("no such command: " + tokens[0])) } - return command(tokens[1:]) + return command(ctx, tokens[1:]) } func (c *CommandController) Alias(commandName string) Command { - return func(args []string) tea.Msg { + return func(ctx ExecContext, args []string) tea.Msg { command := c.lookupCommand(commandName) if command == nil { - log.Println("No such command: ", commandName) return events.Error(errors.New("no such command: " + commandName)) } - return command(args) + return command(ctx, args) } } func (c *CommandController) lookupCommand(name string) Command { for ctx := c.commandList; ctx != nil; ctx = ctx.parent { - log.Printf("Looking in command list: %v", c.commandList) if cmd, ok := ctx.Commands[name]; ok { return cmd } } return nil } + +func (c *CommandController) ExecuteFile(filename string) error { + baseFilename := filepath.Base(filename) + + if rcFile, err := os.ReadFile(filename); err == nil { + if err := c.executeFile(rcFile, baseFilename); err != nil { + return errors.Wrapf(err, "error executing %v", filename) + } + } else { + return errors.Wrapf(err, "error loading %v", filename) + } + return nil +} + +func (c *CommandController) executeFile(file []byte, filename string) error { + scnr := bufio.NewScanner(bytes.NewReader(file)) + + lineNo := 0 + for scnr.Scan() { + lineNo++ + line := strings.TrimSpace(scnr.Text()) + if line == "" { + continue + } else if line[0] == '#' { + continue + } + + msg := c.execute(ExecContext{FromFile: true}, line) + switch m := msg.(type) { + case events.ErrorMsg: + log.Printf("%v:%v: error - %v", filename, lineNo, m.Error()) + case events.StatusMsg: + log.Printf("%v:%v: %v", filename, lineNo, string(m)) + } + } + return scnr.Err() +} diff --git a/internal/common/ui/commandctrl/context.go b/internal/common/ui/commandctrl/context.go index 5417c97..1ba126a 100644 --- a/internal/common/ui/commandctrl/context.go +++ b/internal/common/ui/commandctrl/context.go @@ -1,16 +1,6 @@ package commandctrl -import "context" - -type commandArgContextKeyType struct{} - -var commandArgContextKey = commandArgContextKeyType{} - -func WithCommandArgs(ctx context.Context, args []string) context.Context { - return context.WithValue(ctx, commandArgContextKey, args) -} - -func CommandArgs(ctx context.Context) []string { - args, _ := ctx.Value(commandArgContextKey).([]string) - return args +type ExecContext struct { + // FromFile is true if the command is executed as part of a command + FromFile bool } diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go index c5f6f74..79c4828 100644 --- a/internal/common/ui/commandctrl/types.go +++ b/internal/common/ui/commandctrl/types.go @@ -2,16 +2,16 @@ package commandctrl import tea "github.com/charmbracelet/bubbletea" -type Command func(args []string) tea.Msg +type Command func(ctx ExecContext, args []string) tea.Msg func NoArgCommand(cmd tea.Cmd) Command { - return func(args []string) tea.Msg { + return func(ctx ExecContext, args []string) tea.Msg { return cmd() } } -type CommandContext struct { +type CommandList struct { Commands map[string]Command - parent *CommandContext + parent *CommandList } diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go index 79aa578..6d1ec8e 100644 --- a/internal/dynamo-browse/controllers/keybinding.go +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -16,10 +16,12 @@ func NewKeyBindingController(service *keybindings.Service) *KeyBindingController return &KeyBindingController{service: service} } -func (kb *KeyBindingController) Rebind(bindingName string, newKey string) tea.Msg { - err := kb.service.Rebind(bindingName, newKey, false) +func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg { + err := kb.service.Rebind(bindingName, newKey, force) if err == nil { return events.SetStatus(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey)) + } else if force { + return events.Error(errors.Wrapf(err, "cannot bind '%v' to '%v'", bindingName, newKey)) } var keyAlreadyBoundErr keybindings.KeyAlreadyBoundError diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index ac056ee..9f03df1 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -20,6 +20,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" "github.com/pkg/errors" "log" + "os" "strings" ) @@ -31,6 +32,8 @@ const ( ViewModeTableOnly = 4 ViewModeCount = 5 + + initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc" ) type Model struct { @@ -69,17 +72,17 @@ func NewModel( dialogPrompt := dialogprompt.New(statusAndPrompt) tableSelect := tableselect.New(dialogPrompt, uiStyles) - cc.AddCommands(&commandctrl.CommandContext{ + cc.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ "quit": commandctrl.NoArgCommand(tea.Quit), - "table": func(args []string) tea.Msg { + "table": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) == 0 { return rc.ListTables() } else { return rc.ScanTable(args[0]) } }, - "export": func(args []string) tea.Msg { + "export": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) == 0 { return events.Error(errors.New("expected filename")) } @@ -90,7 +93,7 @@ func NewModel( // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem), - "set-attr": func(args []string) tea.Msg { + "set-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) == 0 { return events.Error(errors.New("expected field")) } @@ -114,28 +117,35 @@ func NewModel( return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, args[0]) }, - "del-attr": func(args []string) tea.Msg { + "del-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) == 0 { return events.Error(errors.New("expected field")) } return wc.DeleteAttribute(dtv.SelectedItemIndex(), args[0]) }, - "put": func(args []string) tea.Msg { + "put": func(ctx commandctrl.ExecContext, args []string) tea.Msg { return wc.PutItems() }, - "touch": func(args []string) tea.Msg { + "touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { return wc.TouchItem(dtv.SelectedItemIndex()) }, - "noisy-touch": func(args []string) tea.Msg { + "noisy-touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { return wc.NoisyTouchItem(dtv.SelectedItemIndex()) }, - "rebind": func(args []string) tea.Msg { - if len(args) != 2 { - return events.Error(errors.New("expected: name newKey")) + "echo": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + s := new(strings.Builder) + for _, arg := range args { + s.WriteString(arg) } - return keyBindingController.Rebind(args[0], args[1]) + return events.SetStatus(s.String()) + }, + "rebind": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + if len(args) != 2 { + return events.Error(errors.New("expected: bindingName newKey")) + } + return keyBindingController.Rebind(args[0], args[1], ctx.FromFile) }, // Aliases @@ -164,6 +174,12 @@ func NewModel( } func (m Model) Init() tea.Cmd { + // TODO: this should probably be moved somewhere else + rcFilename := os.ExpandEnv(initRCFilename) + if err := m.commandController.ExecuteFile(rcFilename); err != nil { + log.Println(err) + } + return m.tableReadController.Init } diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go index 985b694..2feeaf5 100644 --- a/internal/ssm-browse/ui/model.go +++ b/internal/ssm-browse/ui/model.go @@ -30,7 +30,7 @@ func NewModel(controller *controllers.SSMController, cmdController *commandctrl. statusAndPrompt := statusandprompt.New( layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails), "", defaultStyles.StatusAndPrompt) - cmdController.AddCommands(&commandctrl.CommandContext{ + cmdController.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ "clone": func(args []string) tea.Msg { if currentParam := ssmList.CurrentParameter(); currentParam != nil { From aa7ec9f863cc9c3b2072b64f6cf23d3afd3f7fe6 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 29 Aug 2022 20:45:34 +1000 Subject: [PATCH 5/5] issue-9: fixed unit tests --- cmd/ssm-browse/main.go | 2 +- internal/ssm-browse/ui/model.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/ssm-browse/main.go b/cmd/ssm-browse/main.go index e1dab01..f46a133 100644 --- a/cmd/ssm-browse/main.go +++ b/cmd/ssm-browse/main.go @@ -50,7 +50,7 @@ func main() { cmdController := commandctrl.NewCommandController() cmdController.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ - "cd": func(args []string) tea.Msg { + "cd": func(ec commandctrl.ExecContext, args []string) tea.Msg { return ctrl.ChangePrefix(args[0]) }, }, diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go index 2feeaf5..e178a70 100644 --- a/internal/ssm-browse/ui/model.go +++ b/internal/ssm-browse/ui/model.go @@ -32,13 +32,13 @@ func NewModel(controller *controllers.SSMController, cmdController *commandctrl. cmdController.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ - "clone": func(args []string) tea.Msg { + "clone": func(ec commandctrl.ExecContext, args []string) tea.Msg { if currentParam := ssmList.CurrentParameter(); currentParam != nil { return controller.Clone(*currentParam) } return events.Error(errors.New("no parameter selected")) }, - "delete": func(args []string) tea.Msg { + "delete": func(ec commandctrl.ExecContext, args []string) tea.Msg { if currentParam := ssmList.CurrentParameter(); currentParam != nil { return controller.DeleteParameter(*currentParam) }