441 lines
13 KiB
Go
441 lines
13 KiB
Go
package cmdpacks
|
|
|
|
import (
|
|
"context"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"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"
|
|
"github.com/pkg/errors"
|
|
"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)),
|
|
}
|
|
}
|
|
|
|
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 }
|
|
`
|