All checks were successful
		
		
	
	ci / build (push) Successful in 3m20s
				
			Also finished mapping attribute values to/from UCL
		
			
				
	
	
		
			442 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 }
 | |
| `
 |