diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index cafbe4f..2a7cc37 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -16,9 +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" + 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" @@ -79,9 +81,21 @@ func main() { state := controllers.NewState() tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true) tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController) + keyBindings := keybindings.Default() + + keyBindingService := keybindings_service.NewService(keyBindings) + keyBindingController := controllers.NewKeyBindingController(keyBindingService) commandController := commandctrl.NewCommandController() - model := ui.NewModel(tableReadController, tableWriteController, itemRendererService, commandController) + + model := ui.NewModel( + tableReadController, + tableWriteController, + itemRendererService, + commandController, + keyBindingController, + keyBindings, + ) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. lipgloss.HasDarkBackground() 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..f46a133 100644 --- a/cmd/ssm-browse/main.go +++ b/cmd/ssm-browse/main.go @@ -48,9 +48,9 @@ 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 { + "cd": func(ec commandctrl.ExecContext, 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/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/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go new file mode 100644 index 0000000..6d1ec8e --- /dev/null +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -0,0 +1,40 @@ +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(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 + 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(bindingName, newKey, true) + if err != nil { + return events.Error(err) + } + return events.SetStatus(fmt.Sprintf("Binding '%v' now bound to '%v'", bindingName, newKey)) + }) + } + + 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 new file mode 100644 index 0000000..c37de2d --- /dev/null +++ b/internal/dynamo-browse/services/keybindings/service.go @@ -0,0 +1,122 @@ +package keybindings + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/pkg/errors" + "log" + "reflect" + "strings" +) + +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, 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} + } + + // 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/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/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go new file mode 100644 index 0000000..4842c55 --- /dev/null +++ b/internal/dynamo-browse/ui/keybindings/keybindings.go @@ -0,0 +1,33 @@ +package keybindings + +import "github.com/charmbracelet/bubbles/key" + +type KeyBindings struct { + 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 { + 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..9f03df1 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,12 +1,14 @@ 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" "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" @@ -18,6 +20,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" "github.com/pkg/errors" "log" + "os" "strings" ) @@ -29,6 +32,8 @@ const ( ViewModeTableOnly = 4 ViewModeCount = 5 + + initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc" ) type Model struct { @@ -45,6 +50,7 @@ type Model struct { tableView *dynamotableview.Model itemView *dynamoitemview.Model mainView tea.Model + keyMap *keybindings.ViewKeyBindings } func NewModel( @@ -52,10 +58,12 @@ func NewModel( wc *controllers.TableWriteController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, + keyBindingController *controllers.KeyBindingController, + 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) @@ -64,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")) } @@ -85,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")) } @@ -109,23 +117,37 @@ 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()) }, + "echo": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + s := new(strings.Builder) + for _, arg := range args { + s.WriteString(arg) + } + 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 "sa": cc.Alias("set-attr"), "da": cc.Alias("del-attr"), @@ -147,10 +169,17 @@ func NewModel( tableView: dtv, itemView: div, mainView: mainView, + keyMap: defaultKeyMap.View, } } 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 } @@ -163,40 +192,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 } } 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, } } diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go index 985b694..e178a70 100644 --- a/internal/ssm-browse/ui/model.go +++ b/internal/ssm-browse/ui/model.go @@ -30,15 +30,15 @@ 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 { + "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) }