From cae7509a76c4d22bda991b2c6fe489ce22cf9e33 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 25 May 2025 13:31:00 +1000 Subject: [PATCH] Almost feature complete - Added reading of UCL scripts - Added pasteboard commands - Added ui:command which will define a proc at the top-level --- cmd/dynamo-browse/main.go | 18 +++-- go.mod | 2 +- go.sum | 2 + .../common/ui/commandctrl/cmdpacks/modpb.go | 46 ++++++++++++ .../common/ui/commandctrl/cmdpacks/modrs.go | 28 +++++++- .../common/ui/commandctrl/cmdpacks/modui.go | 15 ++++ .../common/ui/commandctrl/cmdpacks/proxy.go | 4 +- .../common/ui/commandctrl/cmdpacks/stdcmds.go | 5 ++ internal/common/ui/commandctrl/commandctrl.go | 71 +++++++++++++++---- internal/common/ui/commandctrl/ctx.go | 1 + internal/dynamo-browse/controllers/export.go | 2 +- internal/dynamo-browse/ui/model.go | 38 ++++------ 12 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/modpb.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 862941d..43fec27 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -8,6 +8,7 @@ import ( "log" "net" "os" + "strings" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" @@ -45,6 +46,7 @@ func main() { var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans") var flagWorkspace = flag.String("w", "", "workspace file") var flagQuery = flag.String("q", "", "run query") + var flagExtDir = flag.String("ext-dir", "$HOME/.config/dynamo-browse/ext:$HOME/.config/dynamo-browse/.", "directory to search for extensions") flag.Parse() ctx := context.Background() @@ -128,7 +130,7 @@ func main() { exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() - scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) + //scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) if *flagQuery != "" { if *flagTable == "" { @@ -167,10 +169,11 @@ func main() { tableWriteController, exportController, keyBindingController, + pasteboardProvider, ) commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands) - commandController.AddCommandLookupExtension(scriptController) + //commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) model := ui.NewModel( @@ -182,7 +185,7 @@ func main() { jobsController, itemRendererService, commandController, - scriptController, + //scriptController, eventBus, keyBindingController, pasteboardProvider, @@ -196,8 +199,13 @@ func main() { p := tea.NewProgram(model, tea.WithAltScreen()) jobsController.SetMessageSender(p.Send) - scriptController.Init() - scriptController.SetMessageSender(p.Send) + //scriptController.Init() + //scriptController.SetMessageSender(p.Send) + + if err := commandController.LoadExtensions(context.Background(), strings.Split(*flagExtDir, string(os.PathListSeparator))); err != nil { + fmt.Printf("Unable to load extensions: %v", err) + } + go commandController.StartMessageSender(p.Send) log.Println("launching") diff --git a/go.mod b/go.mod index ac57b4e..3113de7 100644 --- a/go.mod +++ b/go.mod @@ -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-20250519120409-53b05b5ba6f8 // indirect + ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e // indirect ) diff --git a/go.sum b/go.sum index 0aafeb1..2b9e341 100644 --- a/go.sum +++ b/go.sum @@ -460,3 +460,5 @@ ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5 ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 h1:h32JQi0d1MI86RaAMaEU7kvti4uSLX5XYe/nk2abApg= ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= +ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e h1:N+HzQUunDUvdjAzbSDtHQZVZ1k+XHbVgbNwmc+EKmlQ= +ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= diff --git a/internal/common/ui/commandctrl/cmdpacks/modpb.go b/internal/common/ui/commandctrl/cmdpacks/modpb.go new file mode 100644 index 0000000..528499b --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/modpb.go @@ -0,0 +1,46 @@ +package cmdpacks + +import ( + "context" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" + "ucl.lmika.dev/ucl" +) + +type pbModule struct { + pasteboardProvider *pasteboardprovider.Provider +} + +func (m pbModule) pbGet(ctx context.Context, args ucl.CallArgs) (any, error) { + s, ok := m.pasteboardProvider.ReadText() + if !ok { + return "", nil + } + return s, nil +} + +func (m pbModule) pbPut(ctx context.Context, args ucl.CallArgs) (any, error) { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + if err := m.pasteboardProvider.WriteText([]byte(s)); err != nil { + return nil, err + } + return s, nil +} + +func modulePB( + pasteboardProvider *pasteboardprovider.Provider, +) ucl.Module { + m := &pbModule{ + pasteboardProvider: pasteboardProvider, + } + + return ucl.Module{ + Name: "pb", + Builtins: map[string]ucl.BuiltinHandler{ + "get": m.pbGet, + "put": m.pbPut, + }, + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index 407120c..148723c 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -252,13 +252,36 @@ func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err er return nil, err } - // TEMP + // TEMP: attribute is always S if err := q.SetEvalItem(item.item, &types.AttributeValueMemberS{Value: val.String()}); err != nil { return nil, err } item.resultSet.SetDirty(item.idx, true) commandctrl.QueueRefresh(ctx) - // END TEMP + + return item, nil +} + +func (rs *rsModule) rsDel(ctx context.Context, args ucl.CallArgs) (_ any, err error) { + var ( + item itemProxy + expr string + ) + + if err := args.Bind(&item, &expr); err != nil { + return nil, err + } + + q, err := queryexpr.Parse(expr) + if err != nil { + return nil, err + } + + if err := q.DeleteAttribute(item.item); err != nil { + return nil, err + } + item.resultSet.SetDirty(item.idx, true) + commandctrl.QueueRefresh(ctx) return item, nil } @@ -279,6 +302,7 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module "next-page": m.rsNextPage, "union": m.rsUnion, "set": m.rsSet, + "del": m.rsDel, }, } } diff --git a/internal/common/ui/commandctrl/cmdpacks/modui.go b/internal/common/ui/commandctrl/cmdpacks/modui.go index 6bcd6ce..26253e6 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modui.go +++ b/internal/common/ui/commandctrl/cmdpacks/modui.go @@ -17,6 +17,20 @@ type uiModule struct { readController *controllers.TableReadController } +func (m *uiModule) uiCommand(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + name string + cmd ucl.Invokable + ) + if err := args.Bind(&name, &cmd); err != nil { + return nil, err + } + + invoker := commandctrl.GetInvoker(ctx) + invoker.Inst().SetBuiltinInvokable(name, cmd) + return nil, nil +} + func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error) { var prompt string if err := args.Bind(&prompt); err != nil { @@ -152,6 +166,7 @@ func moduleUI( return ucl.Module{ Name: "ui", Builtins: map[string]ucl.BuiltinHandler{ + "command": m.uiCommand, "prompt": m.uiPrompt, "prompt-table": m.uiPromptTable, "confirm": m.uiConfirm, diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index 6801946..8dad249 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -126,8 +126,8 @@ var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{ return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey) }, fields: map[string]func(t models.KeyAttribute) ucl.Object{ - "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, - "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, + "PK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, + "SK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, }, } diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 73d2c9c..9019ec6 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -6,6 +6,7 @@ import ( "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/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" "ucl.lmika.dev/repl" @@ -19,6 +20,7 @@ type StandardCommands struct { WriteController *controllers.TableWriteController ExportController *controllers.ExportController KeyBindingController *controllers.KeyBindingController + PBProvider *pasteboardprovider.Provider modUI ucl.Module } @@ -30,6 +32,7 @@ func NewStandardCommands( writeController *controllers.TableWriteController, exportController *controllers.ExportController, keyBindingController *controllers.KeyBindingController, + pbProvider *pasteboardprovider.Provider, ) StandardCommands { modUI, ckbs := moduleUI(tableService, state, readController) keyBindingController.SetCustomKeyBindingSource(ckbs) @@ -41,6 +44,7 @@ func NewStandardCommands( WriteController: writeController, ExportController: exportController, KeyBindingController: keyBindingController, + PBProvider: pbProvider, modUI: modUI, } } @@ -389,6 +393,7 @@ 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)), } } diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index d4de9be..bfbe79b 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -1,6 +1,7 @@ package commandctrl import ( + "bytes" "context" "fmt" tea "github.com/charmbracelet/bubbletea" @@ -136,7 +137,7 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms } func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) { - return c.uclInst.Eval(ctx, commandInput) + return c.uclInst.EvalString(ctx, commandInput) } func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea.Msg) { @@ -202,7 +203,47 @@ func (c *CommandController) lookupCommand(name string) Command { return nil } -func (c *CommandController) ExecuteFile(filename string) error { +func (c *CommandController) LoadExtensions(ctx context.Context, baseDirs []string) error { + log.Printf("loading extensions: %v", baseDirs) + for _, baseDir := range baseDirs { + baseDir = os.ExpandEnv(baseDir) + descendIntoSubDirs := !strings.HasSuffix(baseDir, ".") + + if stat, err := os.Stat(baseDir); err != nil { + if os.IsNotExist(err) { + continue + } + return err + } else if !stat.IsDir() { + continue + } + + log.Printf("walking %v", baseDir) + if err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if !descendIntoSubDirs && path != baseDir { + return filepath.SkipDir + } + return nil + } + if strings.HasSuffix(info.Name(), ".ucl") { + if err := c.ExecuteFile(ctx, path); err != nil { + log.Println(err) + } + log.Printf("loaded %v\n", path) + } + return nil + }); err != nil { + return err + } + } + return nil +} + +func (c *CommandController) ExecuteFile(ctx context.Context, filename string) error { oldInteractive := c.interactive c.interactive = false defer func() { @@ -211,27 +252,31 @@ func (c *CommandController) ExecuteFile(filename string) error { baseFilename := filepath.Base(filename) + execCtx := execContext{ctrl: c} + ctx = context.WithValue(context.Background(), commandCtlKey, &execCtx) + if rcFile, err := os.ReadFile(filename); err == nil { - if err := c.executeFile(rcFile, baseFilename); err != nil { - return errors.Wrapf(err, "error executing %v", filename) + if err := c.executeFile(ctx, rcFile); err != nil { + return errors.Wrapf(err, "error executing %v", baseFilename) } } else { - return errors.Wrapf(err, "error loading %v", filename) + return errors.Wrapf(err, "error loading %v", baseFilename) } return nil } -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)) - //} +func (c *CommandController) executeFile(ctx context.Context, file []byte) error { + if _, err := c.uclInst.Eval(ctx, bytes.NewReader(file), ucl.WithSubEnv()); err != nil { + return err + } + return nil } +func (c *CommandController) Inst() *ucl.Inst { + return c.uclInst +} + func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { command := c.lookupCommand(name) if command == nil { diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index fef635b..dffff70 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -59,4 +59,5 @@ func QueueRefresh(ctx context.Context) { type Invoker interface { Invoke(invokable ucl.Invokable, args []any) tea.Msg + Inst() *ucl.Inst } diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index 7f4ce8e..23c91a9 100644 --- a/internal/dynamo-browse/controllers/export.go +++ b/internal/dynamo-browse/controllers/export.go @@ -107,7 +107,7 @@ func (c *ExportController) ExportCSVToClipboard() tea.Msg { if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil { return events.Error(err) } - return nil + return events.StatusMsg("Table copied to clipboard") } // TODO: this really needs to be a service! diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 81f6926..90dfa0f 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -22,7 +22,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" bus "github.com/lmika/events" "log" - "os" ) const ( @@ -33,8 +32,6 @@ const ( ViewModeTableOnly = 4 ViewModeCount = 5 - - initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc" ) type Model struct { @@ -43,14 +40,14 @@ type Model struct { settingsController *controllers.SettingsController exportController *controllers.ExportController commandController *commandctrl.CommandController - scriptController *controllers.ScriptController - jobController *controllers.JobsController - colSelector *colselector.Model - relSelector *relselector.Model - itemEdit *dynamoitemedit.Model - statusAndPrompt *statusandprompt.StatusAndPrompt - tableSelect *tableselect.Model - eventBus *bus.Bus + //scriptController *controllers.ScriptController + jobController *controllers.JobsController + colSelector *colselector.Model + relSelector *relselector.Model + itemEdit *dynamoitemedit.Model + statusAndPrompt *statusandprompt.StatusAndPrompt + tableSelect *tableselect.Model + eventBus *bus.Bus mainViewIndex int @@ -71,7 +68,7 @@ func NewModel( jobController *controllers.JobsController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, - scriptController *controllers.ScriptController, + //scriptController *controllers.ScriptController, eventBus *bus.Bus, keyBindingController *controllers.KeyBindingController, pasteboardProvider services.PasteboardProvider, @@ -255,7 +252,8 @@ func NewModel( tableReadController: rc, tableWriteController: wc, commandController: cc, - scriptController: scriptController, + //scriptController: scriptController, + exportController: exportController, jobController: jobController, itemEdit: itemEdit, colSelector: colSelector, @@ -318,10 +316,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // return m, nil case key.Matches(msg, m.keyMap.ShowColumnOverlay): return m, events.SetTeaMessage(controllers.ShowColumnOverlay{}) - case key.Matches(msg, m.keyMap.ShowRelItemsOverlay): - if idx := m.tableView.SelectedItemIndex(); idx >= 0 { - return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx)) - } + //case key.Matches(msg, m.keyMap.ShowRelItemsOverlay): + // if idx := m.tableView.SelectedItemIndex(); idx >= 0 { + // return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx)) + // } case key.Matches(msg, m.keyMap.PromptForCommand): return m, m.commandController.Prompt case key.Matches(msg, m.keyMap.PromptForTable): @@ -344,12 +342,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m Model) Init() tea.Cmd { - // TODO: this should probably be moved somewhere else - rcFilename := os.ExpandEnv(initRCFilename) - if err := m.commandController.ExecuteFile(rcFilename); err != nil { - log.Println(err) - } - return tea.Batch( m.tableReadController.Init, m.root.Init(),