package cmdpacks import ( "context" tea "github.com/charmbracelet/bubbletea" "github.com/pkg/errors" "lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" ) type StandardCommands struct { TableService *tables.Service State *controllers.State ReadController *controllers.TableReadController WriteController *controllers.TableWriteController ExportController *controllers.ExportController KeyBindingController *controllers.KeyBindingController PBProvider services.PasteboardProvider SettingsController *controllers.SettingsController modUI ucl.Module } func NewStandardCommands( tableService *tables.Service, state *controllers.State, readController *controllers.TableReadController, writeController *controllers.TableWriteController, exportController *controllers.ExportController, keyBindingController *controllers.KeyBindingController, pbProvider services.PasteboardProvider, settingsController *controllers.SettingsController, ) StandardCommands { modUI, ckbs := moduleUI(tableService, state, readController) keyBindingController.SetCustomKeyBindingSource(ckbs) return StandardCommands{ TableService: tableService, State: state, ReadController: readController, WriteController: writeController, ExportController: exportController, KeyBindingController: keyBindingController, PBProvider: pbProvider, SettingsController: settingsController, modUI: modUI, } } var cmdQuitDoc = repl.Doc{ Brief: "Quits dynamo-browse", Detailed: ` This will quit dynamo-browse immediately, without prompting to apply any changes. `, } func (sc StandardCommands) cmdQuit(ctx context.Context, args ucl.CallArgs) (any, error) { commandctrl.PostMsg(ctx, tea.Quit) return nil, nil } var cmdTableDoc = repl.Doc{ Brief: "Prompt for table to scan", Usage: "[NAME]", Args: []repl.ArgDoc{ {Name: "name", Brief: "Name of the table to scan"}, }, Detailed: ` If called with an argument, it will scan the table with that name and replace the current result set. If called without an argument, it will prompt for a table to scan. This command is intended only for interactive sessions and is not suitable for scripting. The scan or table prompts will happen asynchronously. `, } func (sc StandardCommands) cmdTable(ctx context.Context, args ucl.CallArgs) (any, error) { var tableName string if err := args.Bind(&tableName); err == nil { commandctrl.PostMsg(ctx, sc.ReadController.ScanTable(tableName)) return nil, nil } commandctrl.PostMsg(ctx, sc.ReadController.ListTables(false)) return nil, nil } var cmdExportDoc = repl.Doc{ Brief: "Exports a result-set as CSV", Usage: "FILENAME [-all]", Args: []repl.ArgDoc{ {Name: "filename", Brief: "Filename to export to"}, {Name: "-all", Brief: "Export all results from the table"}, }, Detailed: ` The fields of the current table view will be treated as the header of the exported table. This command is intended only for interactive sessions and is not suitable for scripting. The export will run asynchronously. `, } func (sc StandardCommands) cmdExport(ctx context.Context, args ucl.CallArgs) (any, error) { var filename string if err := args.Bind(&filename); err != nil { return nil, errors.New("expected filename") } opts := controllers.ExportOptions{ AllResults: args.HasSwitch("all"), } commandctrl.PostMsg(ctx, sc.ExportController.ExportCSV(filename, opts)) return nil, nil } var cmdMarkDoc = repl.Doc{ Brief: "Set the marked items of the current result-set", Usage: "[WHAT] [-where EXPR]", Args: []repl.ArgDoc{ {Name: "what", Brief: "Items to mark. Defaults to 'all'"}, {Name: "-where", Brief: "Filter expression select items to mark"}, }, Detailed: ` WHAT can be one of: - all: Mark all items in the current result-set - none: Unmark all items in the current result-set - toggle: Toggle the marked state of all items in the current result-set `, } func (sc StandardCommands) cmdMark(ctx context.Context, args ucl.CallArgs) (any, error) { var markOp = controllers.MarkOpMark if args.NArgs() > 0 { var markOpStr string if err := args.Bind(&markOpStr); err == nil { switch markOpStr { case "all": markOp = controllers.MarkOpMark case "none": markOp = controllers.MarkOpUnmark case "toggle": markOp = controllers.MarkOpToggle default: return nil, errors.New("unrecognised mark operation") } } } var whereExpr = "" if args.HasSwitch("where") { if err := args.BindSwitch("where", &whereExpr); err != nil { return nil, err } } commandctrl.PostMsg(ctx, sc.ReadController.Mark(markOp, whereExpr)) return nil, nil } var cmdNextPageDoc = repl.Doc{ Brief: "Retrieve and display the next page of the current result-set", Detailed: ` This command is intended only for interactive sessions and is not suitable for scripting. Fetching the next page will run asynchronously. `, } func (sc StandardCommands) cmdNextPage(ctx context.Context, args ucl.CallArgs) (any, error) { commandctrl.PostMsg(ctx, sc.ReadController.NextPage()) return nil, nil } var cmdDeleteDoc = repl.Doc{ Brief: "Delete the marked items of the current result-set", Detailed: ` The user will be prompted to confirm the deletion. If approved, the items will be deleted immediately. This command is intended only for interactive sessions and is not suitable for scripting. `, } func (sc StandardCommands) cmdDelete(ctx context.Context, args ucl.CallArgs) (any, error) { commandctrl.PostMsg(ctx, sc.WriteController.DeleteMarked()) return nil, nil } var cmdNewItemDoc = repl.Doc{ Brief: "Adds a new item to the current result-set", Detailed: ` The user will be prompted to enter the values for each required attribute, such as the partition and sort key. The new item will be commited to the database upon the next write. This command is intended only for interactive sessions and is not suitable for scripting. `, } func (sc StandardCommands) cmdNewItem(ctx context.Context, args ucl.CallArgs) (any, error) { commandctrl.PostMsg(ctx, sc.WriteController.NewItem()) return nil, nil } var cmdCloneDoc = repl.Doc{ Brief: "Adds a copy of the selected item as a new item to the current result-set", Detailed: ` The user will be prompted to enter the partition and sort key. All other attributes will be cloned from the selected item. The new item will be commited to the database upon the next write. This command is intended only for interactive sessions and is not suitable for scripting. `, } func (sc StandardCommands) cmdClone(ctx context.Context, args ucl.CallArgs) (any, error) { selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) if !ok { return nil, errors.New("no item selected") } commandctrl.PostMsg(ctx, sc.WriteController.CloneItem(selectedItemIndex)) return nil, nil } var cmdSetAttrDoc = repl.Doc{ Brief: "Modify a field value of the selected or marked items", Usage: "ATTR [TYPE]", Args: []repl.ArgDoc{ {Name: "attr", Brief: "Attribute to modify"}, {Name: "-S", Brief: "Set attribute to a string"}, {Name: "-N", Brief: "Set attribute to a number"}, {Name: "-BOOL", Brief: "Set attribute to a boolean"}, {Name: "-NULL", Brief: "Set attribute to a null"}, {Name: "-TO", Brief: "Set attribute to the result of an expression"}, }, Detailed: ` The user will be prompted to enter the new value for the attribute. If the attribute type is not known, then a type will need to be specified. Otherwise, the type will be unchanged. The modified item will be commited to the database upon the next write. `, } func (sc StandardCommands) cmdSetAttr(ctx context.Context, args ucl.CallArgs) (any, error) { var fieldName string if err := args.Bind(&fieldName); err != nil { return nil, errors.New("expected field name") } selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) if !ok { return nil, errors.New("no item selected") } var itemType = models.UnsetItemType 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 } commandctrl.PostMsg(ctx, sc.WriteController.SetAttributeValue(selectedItemIndex, itemType, fieldName)) return nil, nil } var cmdDelAttrDoc = repl.Doc{ Brief: "Remove the field of the selected or marked items", Usage: "ATTR", Args: []repl.ArgDoc{ {Name: "attr", Brief: "Attribute to remove"}, }, Detailed: ` The modified item will be commited to the database upon the next write. `, } func (sc StandardCommands) cmdDelAttr(ctx context.Context, args ucl.CallArgs) (any, error) { var fieldName string if err := args.Bind(&fieldName); err != nil { return nil, errors.New("expected field name") } selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) if !ok { return nil, errors.New("no item selected") } commandctrl.PostMsg(ctx, sc.WriteController.DeleteAttribute(selectedItemIndex, fieldName)) return nil, nil } var cmdPutDoc = repl.Doc{ Brief: "Commit changes to the table", Detailed: ` This will put all new and modified items. This command is intended only for interactive sessions and is not suitable for scripting. The user will be prompted to confirm the changes. `, } func (sc StandardCommands) cmdPut(ctx context.Context, args ucl.CallArgs) (any, error) { commandctrl.PostMsg(ctx, sc.WriteController.PutItems()) return nil, nil } var cmdTouchDoc = repl.Doc{ Brief: "Put the currently selected item", Detailed: ` This will put the currently selected item, regardless of whether it has been modified. This command is intended only for interactive sessions and is not suitable for scripting. The user will be prompted to confirm the touch. `, } func (sc StandardCommands) cmdTouch(ctx context.Context, args ucl.CallArgs) (any, error) { selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) if !ok { return nil, errors.New("no item selected") } commandctrl.PostMsg(ctx, sc.WriteController.TouchItem(selectedItemIndex)) return nil, nil } var cmdNoisyTouchDoc = repl.Doc{ Brief: "Put the currently selected item by deleting it first", Detailed: ` This will put the currently selected item, regardless of whether it has been modified. It does so by removing the item from the table, then adding it back again. This command is intended only for interactive sessions and is not suitable for scripting. The user will be prompted to confirm the touch. `, } func (sc StandardCommands) cmdNoisyTouch(ctx context.Context, args ucl.CallArgs) (any, error) { selectedItemIndex, ok := commandctrl.SelectedItemIndex(ctx) if !ok { return nil, errors.New("no item selected") } commandctrl.PostMsg(ctx, sc.WriteController.NoisyTouchItem(selectedItemIndex)) return nil, nil } var cmdRebindDoc = repl.Doc{ Brief: "Binds a new key to an action", Usage: "ACTION KEY", Args: []repl.ArgDoc{ {Name: "action", Brief: "Action to bind"}, {Name: "key", Brief: "Key to bind"}, }, Detailed: ` If the key is already bound to an action, it will be replaced. The set of actions this command accepts is well-defined. For binding to arbitrary actions, use the ui:bind command. `, } func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (any, error) { var bindingName, newKey string if err := args.Bind(&bindingName, &newKey); err != nil { return nil, errors.New("expected: bindingName newKey") } // TODO: should only force if not interactive commandctrl.PostMsg(ctx, sc.KeyBindingController.Rebind(bindingName, newKey, false)) return nil, nil } func (sc StandardCommands) InstOptions() []ucl.InstOption { return []ucl.InstOption{ ucl.WithModule(moduleRS(sc.TableService, sc.State)), ucl.WithModule(sc.modUI), ucl.WithModule(modulePB(sc.PBProvider)), ucl.WithModule(moduleOpt(sc.SettingsController)), ucl.WithModule(moduleAttrValue()), } } func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { ucl.SetBuiltin("quit", sc.cmdQuit) ucl.SetBuiltin("table", sc.cmdTable) ucl.SetBuiltin("export", sc.cmdExport) ucl.SetBuiltin("mark", sc.cmdMark) ucl.SetBuiltin("next-page", sc.cmdNextPage) ucl.SetBuiltin("delete", sc.cmdDelete) ucl.SetBuiltin("new-item", sc.cmdNewItem) ucl.SetBuiltin("clone", sc.cmdClone) ucl.SetBuiltin("set-attr", sc.cmdSetAttr) ucl.SetBuiltin("del-attr", sc.cmdDelAttr) ucl.SetBuiltin("put", sc.cmdPut) ucl.SetBuiltin("touch", sc.cmdTouch) ucl.SetBuiltin("noisy-touch", sc.cmdNoisyTouch) ucl.SetBuiltin("rebind", sc.cmdRebind) // Aliases ucl.SetBuiltin("sa", sc.cmdSetAttr) ucl.SetBuiltin("da", sc.cmdDelAttr) ucl.SetBuiltin("np", sc.cmdNextPage) ucl.SetBuiltin("w", sc.cmdPut) ucl.SetBuiltin("q", sc.cmdQuit) ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) ucl.SetPseudoVar("table", tablePVar{sc.State}) ucl.SetPseudoVar("item", itemPVar{sc.State}) } func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error { _, err := ucl.EvalString(ctx, uclPrelude) return err } const uclPrelude = ` ui:command unmark { mark none } ui:command set-opt { |n k| opt:set $n $k } `