Merge pull request #17 from lmika/feature/issue-9

Issue 9: Added the ability to rebind actions to different keys
This commit is contained in:
Leon Mika 2022-08-29 20:50:35 +10:00 committed by GitHub
commit beffba3075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 373 additions and 78 deletions

View file

@ -16,9 +16,11 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" "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/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" "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" "github.com/lmika/audax/internal/dynamo-browse/services/tables"
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" 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"
"github.com/lmika/audax/internal/dynamo-browse/ui/keybindings"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
"github.com/lmika/gopkgs/cli" "github.com/lmika/gopkgs/cli"
"log" "log"
@ -79,9 +81,21 @@ func main() {
state := controllers.NewState() state := controllers.NewState()
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true) tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true)
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController) tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController)
keyBindings := keybindings.Default()
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
commandController := commandctrl.NewCommandController() 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. // Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
lipgloss.HasDarkBackground() lipgloss.HasDarkBackground()

View file

@ -33,7 +33,7 @@ func main() {
ctrl := controllers.NewLogFileController(service, flag.Arg(0)) ctrl := controllers.NewLogFileController(service, flag.Arg(0))
cmdController := commandctrl.NewCommandController() cmdController := commandctrl.NewCommandController()
//cmdController.AddCommands(&commandctrl.CommandContext{ //cmdController.AddCommands(&commandctrl.CommandList{
// Commands: map[string]commandctrl.Command{ // Commands: map[string]commandctrl.Command{
// "cd": func(args []string) tea.Cmd { // "cd": func(args []string) tea.Cmd {
// return ctrl.ChangePrefix(args[0]) // return ctrl.ChangePrefix(args[0])

View file

@ -48,9 +48,9 @@ func main() {
ctrl := controllers.New(service) ctrl := controllers.New(service)
cmdController := commandctrl.NewCommandController() cmdController := commandctrl.NewCommandController()
cmdController.AddCommands(&commandctrl.CommandContext{ cmdController.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{ 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]) return ctrl.ChangePrefix(args[0])
}, },
}, },

View file

@ -1,9 +1,13 @@
package commandctrl package commandctrl
import ( import (
"bufio"
"bytes"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/pkg/errors" "github.com/pkg/errors"
"log" "log"
"os"
"path/filepath"
"strings" "strings"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/audax/internal/common/ui/events"
@ -11,7 +15,7 @@ import (
) )
type CommandController struct { type CommandController struct {
commandList *CommandContext commandList *CommandList
} }
func NewCommandController() *CommandController { 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 ctx.parent = c.commandList
c.commandList = ctx c.commandList = ctx
} }
@ -35,6 +39,10 @@ func (c *CommandController) Prompt() tea.Msg {
} }
func (c *CommandController) Execute(commandInput string) 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) input := strings.TrimSpace(commandInput)
if input == "" { if input == "" {
return nil return nil
@ -43,31 +51,65 @@ func (c *CommandController) Execute(commandInput string) tea.Msg {
tokens := shellwords.Split(input) tokens := shellwords.Split(input)
command := c.lookupCommand(tokens[0]) command := c.lookupCommand(tokens[0])
if command == nil { if command == nil {
log.Println("No such command: ", tokens)
return events.Error(errors.New("no such command: " + tokens[0])) 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 { 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) command := c.lookupCommand(commandName)
if command == nil { if command == nil {
log.Println("No such command: ", commandName)
return events.Error(errors.New("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 { func (c *CommandController) lookupCommand(name string) Command {
for ctx := c.commandList; ctx != nil; ctx = ctx.parent { 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 { if cmd, ok := ctx.Commands[name]; ok {
return cmd return cmd
} }
} }
return nil 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()
}

View file

@ -1,16 +1,6 @@
package commandctrl package commandctrl
import "context" type ExecContext struct {
// FromFile is true if the command is executed as part of a command
type commandArgContextKeyType struct{} FromFile bool
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
} }

View file

@ -2,16 +2,16 @@ package commandctrl
import tea "github.com/charmbracelet/bubbletea" 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 { func NoArgCommand(cmd tea.Cmd) Command {
return func(args []string) tea.Msg { return func(ctx ExecContext, args []string) tea.Msg {
return cmd() return cmd()
} }
} }
type CommandContext struct { type CommandList struct {
Commands map[string]Command Commands map[string]Command
parent *CommandContext parent *CommandList
} }

View file

@ -27,7 +27,7 @@ func (rs NewResultSet) ModeMessage() string {
} }
if rs.currentFilter != "" { if rs.currentFilter != "" {
modeLine = fmt.Sprintf("%v - Filter: '%v'", modeLine, rs.currentFilter) modeLine = fmt.Sprintf("%v - PromptForFilter: '%v'", modeLine, rs.currentFilter)
} }
return modeLine return modeLine
} }

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

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

View file

@ -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"`
}

View file

@ -1,12 +1,14 @@
package ui package ui
import ( import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/commandctrl" "github.com/lmika/audax/internal/common/ui/commandctrl"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" "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/dialogprompt"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemedit" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemedit"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview" "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/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"log" "log"
"os"
"strings" "strings"
) )
@ -29,6 +32,8 @@ const (
ViewModeTableOnly = 4 ViewModeTableOnly = 4
ViewModeCount = 5 ViewModeCount = 5
initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc"
) )
type Model struct { type Model struct {
@ -45,6 +50,7 @@ type Model struct {
tableView *dynamotableview.Model tableView *dynamotableview.Model
itemView *dynamoitemview.Model itemView *dynamoitemview.Model
mainView tea.Model mainView tea.Model
keyMap *keybindings.ViewKeyBindings
} }
func NewModel( func NewModel(
@ -52,10 +58,12 @@ func NewModel(
wc *controllers.TableWriteController, wc *controllers.TableWriteController,
itemRendererService *itemrenderer.Service, itemRendererService *itemrenderer.Service,
cc *commandctrl.CommandController, cc *commandctrl.CommandController,
keyBindingController *controllers.KeyBindingController,
defaultKeyMap *keybindings.KeyBindings,
) Model { ) Model {
uiStyles := styles.DefaultStyles uiStyles := styles.DefaultStyles
dtv := dynamotableview.New(uiStyles) dtv := dynamotableview.New(defaultKeyMap.TableView, uiStyles)
div := dynamoitemview.New(itemRendererService, uiStyles) div := dynamoitemview.New(itemRendererService, uiStyles)
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div) mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
@ -64,17 +72,17 @@ func NewModel(
dialogPrompt := dialogprompt.New(statusAndPrompt) dialogPrompt := dialogprompt.New(statusAndPrompt)
tableSelect := tableselect.New(dialogPrompt, uiStyles) tableSelect := tableselect.New(dialogPrompt, uiStyles)
cc.AddCommands(&commandctrl.CommandContext{ cc.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{ Commands: map[string]commandctrl.Command{
"quit": commandctrl.NoArgCommand(tea.Quit), "quit": commandctrl.NoArgCommand(tea.Quit),
"table": func(args []string) tea.Msg { "table": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
if len(args) == 0 { if len(args) == 0 {
return rc.ListTables() return rc.ListTables()
} else { } else {
return rc.ScanTable(args[0]) return rc.ScanTable(args[0])
} }
}, },
"export": func(args []string) tea.Msg { "export": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
if len(args) == 0 { if len(args) == 0 {
return events.Error(errors.New("expected filename")) return events.Error(errors.New("expected filename"))
} }
@ -85,7 +93,7 @@ func NewModel(
// TEMP // TEMP
"new-item": commandctrl.NoArgCommand(wc.NewItem), "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 { if len(args) == 0 {
return events.Error(errors.New("expected field")) return events.Error(errors.New("expected field"))
} }
@ -109,23 +117,37 @@ func NewModel(
return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, args[0]) 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 { if len(args) == 0 {
return events.Error(errors.New("expected field")) return events.Error(errors.New("expected field"))
} }
return wc.DeleteAttribute(dtv.SelectedItemIndex(), args[0]) 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() return wc.PutItems()
}, },
"touch": func(args []string) tea.Msg { "touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
return wc.TouchItem(dtv.SelectedItemIndex()) 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()) 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 // Aliases
"sa": cc.Alias("set-attr"), "sa": cc.Alias("set-attr"),
"da": cc.Alias("del-attr"), "da": cc.Alias("del-attr"),
@ -147,10 +169,17 @@ func NewModel(
tableView: dtv, tableView: dtv,
itemView: div, itemView: div,
mainView: mainView, mainView: mainView,
keyMap: defaultKeyMap.View,
} }
} }
func (m Model) Init() tea.Cmd { 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 return m.tableReadController.Init
} }
@ -163,40 +192,39 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.tableView.Refresh() return m, m.tableView.Refresh()
case tea.KeyMsg: case tea.KeyMsg:
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() { if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() {
log.Printf("key = %+v", msg) switch {
switch msg.String() { case key.Matches(msg, m.keyMap.Mark):
case "m":
if idx := m.tableView.SelectedItemIndex(); idx >= 0 { if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
return m, func() tea.Msg { return m.tableWriteController.ToggleMark(idx) } 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 { if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
return m, func() tea.Msg { return m.tableReadController.CopyItemToClipboard(idx) } return m, func() tea.Msg { return m.tableReadController.CopyItemToClipboard(idx) }
} }
case "R": case key.Matches(msg, m.keyMap.Rescan):
return m, m.tableReadController.Rescan return m, m.tableReadController.Rescan
case "?": case key.Matches(msg, m.keyMap.PromptForQuery):
return m, m.tableReadController.PromptForQuery return m, m.tableReadController.PromptForQuery
case "/": case key.Matches(msg, m.keyMap.PromptForFilter):
return m, m.tableReadController.Filter return m, m.tableReadController.Filter
case "backspace": case key.Matches(msg, m.keyMap.ViewBack):
return m, m.tableReadController.ViewBack return m, m.tableReadController.ViewBack
case "\\": case key.Matches(msg, m.keyMap.ViewForward):
return m, m.tableReadController.ViewForward return m, m.tableReadController.ViewForward
case "w": case key.Matches(msg, m.keyMap.CycleLayoutForward):
return m, func() tea.Msg { return m, func() tea.Msg {
return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, 1, ViewModeCount)} 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 m, func() tea.Msg {
return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, -1, ViewModeCount)} return controllers.SetTableItemView{ViewIndex: utils.Cycle(m.mainViewIndex, -1, ViewModeCount)}
} }
//case "e": //case "e":
// m.itemEdit.Visible() // m.itemEdit.Visible()
// return m, nil // return m, nil
case ":": case key.Matches(msg, m.keyMap.PromptForCommand):
return m, m.commandController.Prompt return m, m.commandController.Prompt
case "ctrl+c", "esc": case key.Matches(msg, m.keyMap.Quit):
return m, tea.Quit return m, tea.Quit
} }
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models" "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/dynamoitemview"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
@ -20,22 +21,11 @@ var (
Background(lipgloss.Color("#4479ff")) 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 { type Model struct {
frameTitle frame.FrameTitle frameTitle frame.FrameTitle
table table.Model table table.Model
w, h int w, h int
keyBinding KeyBinding keyBinding *keybindings.TableKeyBinding
// model state // model state
colOffset int colOffset int
@ -43,7 +33,7 @@ type Model struct {
resultSet *models.ResultSet 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) tbl := table.New(table.SimpleColumns([]string{"pk", "sk"}), 100, 100)
rows := make([]table.Row, 0) rows := make([]table.Row, 0)
tbl.SetRows(rows) tbl.SetRows(rows)
@ -53,16 +43,7 @@ func New(uiStyles styles.Styles) *Model {
return &Model{ return &Model{
frameTitle: frameTitle, frameTitle: frameTitle,
table: tbl, table: tbl,
keyBinding: KeyBinding{ 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")),
},
} }
} }

View file

@ -30,15 +30,15 @@ func NewModel(controller *controllers.SSMController, cmdController *commandctrl.
statusAndPrompt := statusandprompt.New( statusAndPrompt := statusandprompt.New(
layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails), "", defaultStyles.StatusAndPrompt) layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails), "", defaultStyles.StatusAndPrompt)
cmdController.AddCommands(&commandctrl.CommandContext{ cmdController.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{ 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 { if currentParam := ssmList.CurrentParameter(); currentParam != nil {
return controller.Clone(*currentParam) return controller.Clone(*currentParam)
} }
return events.Error(errors.New("no parameter selected")) 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 { if currentParam := ssmList.CurrentParameter(); currentParam != nil {
return controller.DeleteParameter(*currentParam) return controller.DeleteParameter(*currentParam)
} }