diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 8adcce4..ac7ac1f 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -187,6 +187,7 @@ func main() { jobsController.SetMessageSender(p.Send) scriptController.Init() scriptController.SetMessageSender(p.Send) + commandController.SetMessageSender(p.Send) log.Println("launching") if err := p.Start(); err != nil { diff --git a/go.mod b/go.mod index 96398dc..bf5197e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.0 require ( - github.com/alecthomas/participle/v2 v2.0.0-beta.5 + github.com/alecthomas/participle/v2 v2.1.1 github.com/asdine/storm v2.1.2+incompatible github.com/aws/aws-sdk-go-v2 v1.18.1 github.com/aws/aws-sdk-go-v2/config v1.18.27 @@ -23,13 +23,13 @@ require ( github.com/cloudcmds/tamarin v1.0.0 github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 - github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 + github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe github.com/mattn/go-runewidth v0.0.14 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 github.com/muesli/reflow v0.3.0 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.design/x/clipboard v0.6.2 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a ) @@ -101,7 +101,7 @@ require ( github.com/risor-io/risor v1.4.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tidwall/gjson v1.14.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -117,4 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 // indirect ) diff --git a/go.sum b/go.sum index 7d71b0d..526e0ec 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/W github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= @@ -223,6 +225,8 @@ github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNo github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= +github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -288,6 +292,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -300,6 +306,8 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -418,3 +426,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +ucl.lmika.dev v0.0.0-20240427010304-6315afc54287 h1:llPHrjca54duvQx9PgMTFDhOW2VQiVvqV1CEHpO4AnY= +ucl.lmika.dev v0.0.0-20240427010304-6315afc54287/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= +ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw= +ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index c0d857f..a1360d5 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "ucl.lmika.dev/ucl" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/shellwords" @@ -18,18 +19,25 @@ import ( const commandsCategory = "commands" type CommandController struct { + uclInst *ucl.Inst historyProvider IterProvider commandList *CommandList + msgSender func(tea.Msg) lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider } func NewCommandController(historyProvider IterProvider) *CommandController { - return &CommandController{ + cc := &CommandController{ historyProvider: historyProvider, commandList: nil, lookupExtensions: nil, } + cc.uclInst = ucl.New( + ucl.WithOut(ucl.LineHandler(cc.printLine)), + ucl.WithMissingBuiltinHandler(cc.cmdInvoker), + ) + return cc } func (c *CommandController) AddCommands(ctx *CommandList) { @@ -37,6 +45,10 @@ func (c *CommandController) AddCommands(ctx *CommandList) { c.commandList = ctx } +func (c *CommandController) SetMessageSender(msg func(tea.Msg)) { + c.msgSender = msg +} + func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) { c.lookupExtensions = append(c.lookupExtensions, ext) } @@ -83,29 +95,25 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms return nil } - tokens := shellwords.Split(input) - command := c.lookupCommand(tokens[0]) - if command == nil { - return events.Error(errors.New("no such command: " + tokens[0])) + res, err := c.uclInst.Eval(context.Background(), commandInput) + if err != nil { + return events.Error(err) } - return command(ctx, tokens[1:]) + if teaMsg, ok := res.(teaMsgWrapper); ok { + return teaMsg.msg + } + return nil } -func (c *CommandController) Alias(commandName string, aliasArgs []string) Command { - return func(ctx ExecContext, args []string) tea.Msg { +func (c *CommandController) Alias(commandName string) Command { + return func(ctx ExecContext, args ucl.CallArgs) tea.Msg { command := c.lookupCommand(commandName) if command == nil { return events.Error(errors.New("no such command: " + commandName)) } - var allArgs []string - if len(aliasArgs) > 0 { - allArgs = append(append([]string{}, aliasArgs...), args...) - } else { - allArgs = args - } - return command(ctx, allArgs) + return command(ctx, args) } } @@ -160,3 +168,26 @@ func (c *CommandController) executeFile(file []byte, filename string) error { } return scnr.Err() } + +func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { + command := c.lookupCommand(name) + if command == nil { + return nil, errors.New("no such command: " + name) + } + + res := command(ExecContext{}, args) + if errMsg, isErrMsg := res.(events.ErrorMsg); isErrMsg { + return nil, errMsg + } + return teaMsgWrapper{res}, nil +} + +func (c *CommandController) printLine(s string) { + if c.msgSender != nil { + c.msgSender(events.StatusMsg(s)) + } +} + +type teaMsgWrapper struct { + msg tea.Msg +} diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go index 7861e09..cd922ff 100644 --- a/internal/common/ui/commandctrl/types.go +++ b/internal/common/ui/commandctrl/types.go @@ -1,11 +1,14 @@ package commandctrl -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "ucl.lmika.dev/ucl" +) -type Command func(ctx ExecContext, args []string) tea.Msg +type Command func(ctx ExecContext, args ucl.CallArgs) tea.Msg func NoArgCommand(cmd tea.Cmd) Command { - return func(ctx ExecContext, args []string) tea.Msg { + return func(ctx ExecContext, args ucl.CallArgs) tea.Msg { return cmd() } } diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index f706ddd..ff2b8d6 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "ucl.lmika.dev/ucl" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" @@ -106,11 +107,19 @@ func (sc *ScriptController) LookupCommand(name string) commandctrl.Command { return nil } - return func(execCtx commandctrl.ExecContext, args []string) tea.Msg { + return func(execCtx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { errChan := sc.waitAndPrintScriptError() ctx := context.Background() - if err := cmd.Invoke(ctx, args, errChan); err != nil { + invokeArgs := make([]string, 0) + for args.NArgs() > 0 { + var s string + if err := args.Bind(&s); err == nil { + invokeArgs = append(invokeArgs, s) + } + } + + if err := cmd.Invoke(ctx, invokeArgs, errChan); err != nil { return events.Error(err) } return nil diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index cb07519..8d3297e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -3,7 +3,7 @@ package ui import ( "log" "os" - "strings" + "ucl.lmika.dev/ucl" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -97,30 +97,32 @@ func NewModel( cc.AddCommands(&commandctrl.CommandList{ Commands: map[string]commandctrl.Command{ "quit": commandctrl.NoArgCommand(tea.Quit), - "table": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { - return rc.ListTables(false) - } else { - return rc.ScanTable(args[0]) + "table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var tableName string + if err := args.Bind(&tableName); err == nil { + return rc.ScanTable(tableName) } + + return rc.ListTables(false) }, - "export": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "export": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var filename string + if err := args.Bind(&filename); err != nil { return events.Error(errors.New("expected filename")) } - opts := controllers.ExportOptions{} - if len(args) == 2 && args[0] == "-all" { - opts.AllResults = true - args = args[1:] + opts := controllers.ExportOptions{ + AllResults: args.HasSwitch("all"), } - return exportController.ExportCSV(args[0], opts) + return exportController.ExportCSV(filename, opts) }, - "mark": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { var markOp = controllers.MarkOpMark - if len(args) > 0 { - switch args[0] { + + var markOpStr string + if err := args.Bind(&markOpStr); err == nil { + switch markOpStr { case "all": markOp = controllers.MarkOpMark case "none": @@ -133,108 +135,121 @@ func NewModel( } var whereExpr = "" - if len(args) == 3 && args[1] == "-where" { - whereExpr = args[2] - } + _ = args.BindSwitch("where", &whereExpr) return rc.Mark(markOp, whereExpr) }, - "next-page": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "unmark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + return rc.Mark(controllers.MarkOpUnmark, "") + }, + "next-page": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return rc.NextPage() }, "delete": commandctrl.NoArgCommand(wc.DeleteMarked), // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem), - "clone": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "clone": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.CloneItem(dtv.SelectedItemIndex()) }, - "set-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "set-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + if err := args.Bind(&fieldName); err != nil { return events.Error(errors.New("expected field")) } var itemType = models.UnsetItemType - if len(args) == 2 { - switch strings.ToUpper(args[0]) { - case "-S": - itemType = models.StringItemType - case "-N": - itemType = models.NumberItemType - case "-BOOL": - itemType = models.BoolItemType - case "-NULL": - itemType = models.NullItemType - case "-TO": - itemType = models.ExprValueItemType - default: - return events.Error(errors.New("unrecognised item type")) - } - args = args[1:] + switch { + case args.HasSwitch("S"): + itemType = models.StringItemType + case args.HasSwitch("N"): + itemType = models.NumberItemType + case args.HasSwitch("BOOL"): + itemType = models.BoolItemType + case args.HasSwitch("NULL"): + itemType = models.NullItemType + case args.HasSwitch("TO"): + itemType = models.ExprValueItemType + default: + return events.Error(errors.New("unrecognised item type")) } - return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, args[0]) + return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName) }, - "del-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) == 0 { + "del-attr": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var fieldName string + // TODO: support rest args + if err := args.Bind(&fieldName); err != nil { return events.Error(errors.New("expected field")) } - return wc.DeleteAttribute(dtv.SelectedItemIndex(), args[0]) + + return wc.DeleteAttribute(dtv.SelectedItemIndex(), fieldName) }, - "put": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.PutItems() }, - "touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { return wc.TouchItem(dtv.SelectedItemIndex()) }, - "noisy-touch": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + "noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) 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) + /* + "echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + s := new(strings.Builder) + for _, arg := range args { + s.WriteString(arg) + } + return events.StatusMsg(s.String()) + }, + */ + "set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { + return events.Error(errors.New("expected settingName")) } - return events.StatusMsg(s.String()) - }, - "set": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - switch len(args) { - case 1: - return settingsController.SetSetting(args[0], "") - case 2: - return settingsController.SetSetting(args[0], args[1]) + + var value string + if err := args.Bind(&value); err == nil { + return settingsController.SetSetting(name, value) } - return events.Error(errors.New("expected: settingName [value]")) + + return settingsController.SetSetting(name, "") }, - "rebind": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 2 { + "rebind": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var bindingName, newKey string + if err := args.Bind(&bindingName, &newKey); err != nil { return events.Error(errors.New("expected: bindingName newKey")) } - return keyBindingController.Rebind(args[0], args[1], ctx.FromFile) + + return keyBindingController.Rebind(bindingName, newKey, ctx.FromFile) }, - "run-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 1 { + "run-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { return events.Error(errors.New("expected: script name")) } - return scriptController.RunScript(args[0]) + + return scriptController.RunScript(name) }, - "load-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg { - if len(args) != 1 { + "load-script": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg { + var name string + if err := args.Bind(&name); err != nil { return events.Error(errors.New("expected: script name")) } - return scriptController.LoadScript(args[0]) + + return scriptController.LoadScript(name) }, // Aliases - "unmark": cc.Alias("mark", []string{"none"}), - "sa": cc.Alias("set-attr", nil), - "da": cc.Alias("del-attr", nil), - "np": cc.Alias("next-page", nil), - "w": cc.Alias("put", nil), - "q": cc.Alias("quit", nil), + "sa": cc.Alias("set-attr"), + "da": cc.Alias("del-attr"), + "np": cc.Alias("next-page"), + "w": cc.Alias("put"), + "q": cc.Alias("quit"), }, })