Added back the core interactive commands

This commit is contained in:
Leon Mika 2025-05-16 18:01:28 +10:00
parent 17381f3d0b
commit cb908ec4eb
6 changed files with 347 additions and 4 deletions

View file

@ -162,7 +162,9 @@ func main() {
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
ReadController: tableReadController,
ReadController: tableReadController,
WriteController: tableWriteController,
ExportController: exportController,
},
)
commandController.AddCommandLookupExtension(scriptController)
@ -183,6 +185,7 @@ func main() {
pasteboardProvider,
keyBindings,
)
commandController.SetUIStateProvider(&model)
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
osstyle.DetectCurrentScheme()

View file

@ -5,17 +5,21 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/pkg/errors"
"ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl"
)
type StandardCommands struct {
ReadController *controllers.TableReadController
ReadController *controllers.TableReadController
WriteController *controllers.TableWriteController
ExportController *controllers.ExportController
KeyBindingController *controllers.KeyBindingController
}
var cmdQuitDoc = repl.Doc{
Brief: "Quits dynamo-browse",
Usage: "quit",
Detailed: `
This will quit dynamo-browse immediately, without prompting to apply
any changes.
@ -29,7 +33,7 @@ func (sc StandardCommands) cmdQuit(ctx context.Context, args ucl.CallArgs) (any,
var cmdTableDoc = repl.Doc{
Brief: "Prompt for table to scan",
Usage: "table [NAME]",
Usage: "[NAME]",
Args: []repl.ArgDoc{
{Name: "name", Brief: "Name of the table to scan"},
},
@ -54,7 +58,321 @@ func (sc StandardCommands) cmdTable(ctx context.Context, args ucl.CallArgs) (any
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) ConfigureUCL(ucl *ucl.Inst) {
ucl.SetBuiltin("quit", sc.cmdQuit)
ucl.SetBuiltin("table", sc.cmdTable)
ucl.SetBuiltin("export", sc.cmdExport)
ucl.SetBuiltin("mark", sc.cmdMark)
// unmark --> alias for { mark none }
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)
// set-opt --> alias to opts:set
}

View file

@ -28,6 +28,7 @@ type CommandController struct {
commandList *CommandList
lookupExtensions []CommandLookupExtension
completionProvider CommandCompletionProvider
uiStateProvider UIStateProvider
cmdChan chan cmdMessage
msgChan chan tea.Msg
interactive bool
@ -68,6 +69,10 @@ func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) {
}
}
func (c *CommandController) SetUIStateProvider(provider UIStateProvider) {
c.uiStateProvider = provider
}
func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) {
c.lookupExtensions = append(c.lookupExtensions, ext)
}

View file

@ -15,3 +15,12 @@ func PostMsg(ctx context.Context, msg tea.Msg) {
cmdCtl.postMessage(msg)
}
}
func SelectedItemIndex(ctx context.Context) (int, bool) {
cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController)
if !ok {
return 0, false
}
return cmdCtl.uiStateProvider.SelectedItemIndex(), true
}

View file

@ -8,3 +8,7 @@ import (
type IterProvider interface {
Iter(ctx context.Context, category string) services.HistoryProvider
}
type UIStateProvider interface {
SelectedItemIndex() int
}

View file

@ -393,3 +393,7 @@ func (m *Model) promptToQuit() tea.Msg {
return nil
})
}
func (m *Model) SelectedItemIndex() int {
return m.tableView.SelectedItemIndex()
}