Started re-engineering the UCL command instance

This commit is contained in:
Leon Mika 2025-05-15 22:16:02 +10:00
parent 94b58e2168
commit 17381f3d0b
9 changed files with 309 additions and 172 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
debug.log
.DS_store
.idea

View file

@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
"log"
"net"
"os"
@ -159,7 +160,11 @@ func main() {
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
ReadController: tableReadController,
},
)
commandController.AddCommandLookupExtension(scriptController)
commandController.SetCommandCompletionProvider(columnsController)

2
go.mod
View file

@ -117,5 +117,5 @@ require (
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 // indirect
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 // indirect
)

2
go.sum
View file

@ -436,3 +436,5 @@ ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d h1:OqGmR0Y+OG6aFIOlXy2QwEHtuUNa
ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4=
ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+RS6NOaBQZyUEoSsA=
ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4=
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 h1:vWttdW8GJWcTUQeJFbQHqCHJDLFWQ9nccUTx/lW2v8s=
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8/go.mod h1:FMP2ncSu4UxfvB0iA2zlebwL+1UPCARdyYNOrmi86A4=

View file

@ -0,0 +1,60 @@
package cmdpacks
import (
"context"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl"
)
type StandardCommands struct {
ReadController *controllers.TableReadController
}
var cmdQuitDoc = repl.Doc{
Brief: "Quits dynamo-browse",
Usage: "quit",
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: "table [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
}
func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) {
ucl.SetBuiltin("quit", sc.cmdQuit)
ucl.SetBuiltin("table", sc.cmdTable)
}

View file

@ -2,6 +2,7 @@ package commandctrl
import (
"context"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/pkg/errors"
"log"
@ -17,30 +18,42 @@ import (
const commandsCategory = "commands"
type cmdMessage struct {
cmd string
}
type CommandController struct {
uclInst *ucl.Inst
historyProvider IterProvider
commandList *CommandList
lookupExtensions []CommandLookupExtension
completionProvider CommandCompletionProvider
cmdChan chan cmdMessage
msgChan chan tea.Msg
interactive bool
}
func NewCommandController(historyProvider IterProvider) *CommandController {
func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController {
cc := &CommandController{
historyProvider: historyProvider,
commandList: nil,
lookupExtensions: nil,
cmdChan: make(chan cmdMessage),
msgChan: make(chan tea.Msg),
interactive: true,
}
cc.uclInst = ucl.New(
ucl.WithOut(ucl.LineHandler(cc.printLine)),
ucl.WithMissingBuiltinHandler(cc.cmdInvoker),
ucl.WithModule(builtins.OS()),
ucl.WithModule(builtins.FS(nil)),
)
for _, pkg := range pkgs {
pkg.ConfigureUCL(cc.uclInst)
}
go cc.cmdLooper()
return cc
}
@ -101,17 +114,42 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms
return nil
}
res, err := c.uclInst.Eval(context.Background(), commandInput)
if err != nil {
return events.Error(err)
select {
case c.cmdChan <- cmdMessage{cmd: input}:
// good
default:
return events.Error(errors.New("command currently running"))
}
if teaMsg, ok := res.(teaMsgWrapper); ok {
return teaMsg.msg
}
/*
res, err := c.uclInst.Eval(context.Background(), commandInput)
if err != nil {
return events.Error(err)
}
if teaMsg, ok := res.(teaMsgWrapper); ok {
return teaMsg.msg
}
*/
return nil
}
func (c *CommandController) cmdLooper() {
ctx := context.WithValue(context.Background(), commandCtlKey, c)
for {
select {
case cmdChan := <-c.cmdChan:
res, err := c.uclInst.Eval(ctx, cmdChan.cmd)
if err != nil {
c.postMessage(events.Error(err))
} else if res != nil {
c.postMessage(events.StatusMsg(fmt.Sprint(res)))
}
}
}
}
func (c *CommandController) Alias(commandName string) Command {
return func(ctx ExecContext, args ucl.CallArgs) tea.Msg {
command := c.lookupCommand(commandName)
@ -158,13 +196,13 @@ func (c *CommandController) ExecuteFile(filename string) error {
}
func (c *CommandController) executeFile(file []byte, filename string) error {
msg := c.execute(ExecContext{FromFile: true}, string(file))
switch m := msg.(type) {
case events.ErrorMsg:
log.Printf("%v: error - %v", filename, m.Error())
case events.StatusMsg:
log.Printf("%v: %v", filename, string(m))
}
//msg := c.execute(ExecContext{FromFile: true}, string(file))
//switch m := msg.(type) {
//case events.ErrorMsg:
// log.Printf("%v: error - %v", filename, m.Error())
//case events.StatusMsg:
// log.Printf("%v: %v", filename, string(m))
//}
return nil
}
@ -194,6 +232,14 @@ func (c *CommandController) printLine(s string) {
}
}
func (c *CommandController) postMessage(msg tea.Msg) {
if c.msgChan == nil {
return
}
c.msgChan <- msg
}
type teaMsgWrapper struct {
msg tea.Msg
}

View file

@ -0,0 +1,17 @@
package commandctrl
import (
"context"
tea "github.com/charmbracelet/bubbletea"
)
type commandCtlKeyType struct{}
var commandCtlKey = commandCtlKeyType{}
func PostMsg(ctx context.Context, msg tea.Msg) {
cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController)
if ok {
cmdCtl.postMessage(msg)
}
}

View file

@ -0,0 +1,7 @@
package commandctrl
import "ucl.lmika.dev/ucl"
type CommandPack interface {
ConfigureUCL(ucl *ucl.Inst)
}

View file

@ -1,16 +1,11 @@
package ui
import (
"log"
"os"
"ucl.lmika.dev/ucl"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
@ -26,7 +21,8 @@ import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
bus "github.com/lmika/events"
"github.com/pkg/errors"
"log"
"os"
)
const (
@ -94,162 +90,164 @@ func NewModel(
dialogPrompt := dialogprompt.New(statusAndPrompt)
tableSelect := tableselect.New(dialogPrompt, uiStyles)
cc.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{
"quit": commandctrl.NoArgCommand(tea.Quit),
"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 ucl.CallArgs) tea.Msg {
var filename string
if err := args.Bind(&filename); err != nil {
return events.Error(errors.New("expected filename"))
}
opts := controllers.ExportOptions{
AllResults: args.HasSwitch("all"),
}
return exportController.ExportCSV(filename, opts)
},
"mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
var markOp = controllers.MarkOpMark
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 events.Error(errors.New("unrecognised mark operation"))
/*
cc.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{
"quit": commandctrl.NoArgCommand(tea.Quit),
"table": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
var tableName string
if err := args.Bind(&tableName); err == nil {
return rc.ScanTable(tableName)
}
}
var whereExpr = ""
_ = args.BindSwitch("where", &whereExpr)
return rc.Mark(markOp, whereExpr)
},
"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 ucl.CallArgs) tea.Msg {
return wc.CloneItem(dtv.SelectedItemIndex())
},
"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
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
}
return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName)
},
"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(), fieldName)
},
"put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.PutItems()
},
"touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.TouchItem(dtv.SelectedItemIndex())
},
"noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.NoisyTouchItem(dtv.SelectedItemIndex())
},
/*
"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())
return rc.ListTables(false)
},
*/
"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"))
}
"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"))
}
var value string
if err := args.Bind(&value); err == nil {
return settingsController.SetSetting(name, value)
}
opts := controllers.ExportOptions{
AllResults: args.HasSwitch("all"),
}
return settingsController.SetSetting(name, "")
return exportController.ExportCSV(filename, opts)
},
"mark": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
var markOp = controllers.MarkOpMark
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 events.Error(errors.New("unrecognised mark operation"))
}
}
var whereExpr = ""
_ = args.BindSwitch("where", &whereExpr)
return rc.Mark(markOp, whereExpr)
},
"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 ucl.CallArgs) tea.Msg {
return wc.CloneItem(dtv.SelectedItemIndex())
},
"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
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
}
return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName)
},
"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(), fieldName)
},
"put": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.PutItems()
},
"touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.TouchItem(dtv.SelectedItemIndex())
},
"noisy-touch": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
return wc.NoisyTouchItem(dtv.SelectedItemIndex())
},
"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"))
}
var value string
if err := args.Bind(&value); err == nil {
return settingsController.SetSetting(name, value)
}
return settingsController.SetSetting(name, "")
},
"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(bindingName, newKey, ctx.FromFile)
},
"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(name)
},
"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(name)
},
// Aliases
"sa": cc.Alias("set-attr"),
"da": cc.Alias("del-attr"),
"np": cc.Alias("next-page"),
"w": cc.Alias("put"),
"q": cc.Alias("quit"),
},
"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(bindingName, newKey, ctx.FromFile)
},
"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(name)
},
"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(name)
},
// Aliases
"sa": cc.Alias("set-attr"),
"da": cc.Alias("del-attr"),
"np": cc.Alias("next-page"),
"w": cc.Alias("put"),
"q": cc.Alias("quit"),
},
})
*/
root := layout.FullScreen(tableSelect)