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:
commit
beffba3075
|
@ -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()
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
40
internal/dynamo-browse/controllers/keybinding.go
Normal file
40
internal/dynamo-browse/controllers/keybinding.go
Normal 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)
|
||||||
|
}
|
14
internal/dynamo-browse/services/keybindings/errors.go
Normal file
14
internal/dynamo-browse/services/keybindings/errors.go
Normal 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)
|
||||||
|
}
|
122
internal/dynamo-browse/services/keybindings/service.go
Normal file
122
internal/dynamo-browse/services/keybindings/service.go
Normal 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
|
||||||
|
}
|
31
internal/dynamo-browse/ui/keybindings/defaults.go
Normal file
31
internal/dynamo-browse/ui/keybindings/defaults.go
Normal 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")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
33
internal/dynamo-browse/ui/keybindings/keybindings.go
Normal file
33
internal/dynamo-browse/ui/keybindings/keybindings.go
Normal 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"`
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue