ucl: integrated ucl with the command evaluator
This commit is contained in:
parent
e37b8099a3
commit
b2ddc62555
7 changed files with 170 additions and 98 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue