ucl: added rs:set

This commit is contained in:
Leon Mika 2025-05-23 22:04:41 +10:00
parent 5088009672
commit 7ae99b009b
13 changed files with 400 additions and 66 deletions

View file

@ -158,18 +158,18 @@ func main() {
}
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil)
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
TableService: tableService,
State: state,
ReadController: tableReadController,
WriteController: tableWriteController,
ExportController: exportController,
KeyBindingController: keyBindingController,
},
stdCommands := cmdpacks.NewStandardCommands(
tableService,
state,
tableReadController,
tableWriteController,
exportController,
keyBindingController,
)
commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands)
commandController.AddCommandLookupExtension(scriptController)
commandController.SetCommandCompletionProvider(columnsController)

View file

@ -3,6 +3,7 @@ package cmdpacks
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"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/models/queryexpr"
@ -65,21 +66,26 @@ var rsQueryDoc = repl.Doc{
`,
}
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
func parseQuery(
ctx context.Context,
args ucl.CallArgs,
currentRS *models.ResultSet,
tablesService *tables.Service,
) (*queryexpr.QueryExpr, *models.TableInfo, error) {
var expr string
if err := args.Bind(&expr); err != nil {
return nil, err
return nil, nil, err
}
q, err := queryexpr.Parse(expr)
if err != nil {
return nil, err
return nil, nil, err
}
if args.NArgs() > 0 {
var queryArgs ucl.Hashable
if err := args.Bind(&queryArgs); err != nil {
return nil, err
return nil, nil, err
}
queryNames := map[string]string{}
@ -108,17 +114,26 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error)
if args.HasSwitch("table") {
var tblName string
if err := args.BindSwitch("table", &tblName); err != nil {
return nil, err
return nil, nil, err
}
tableInfo, err = rs.tableService.Describe(ctx, tblName)
tableInfo, err = tablesService.Describe(ctx, tblName)
if err != nil {
return nil, err
return nil, nil, err
}
} else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil {
tableInfo = currRs.TableInfo
} else if currentRS != nil && currentRS.TableInfo != nil {
tableInfo = currentRS.TableInfo
} else {
return nil, errors.New("no table specified")
return nil, nil, errors.New("no table specified")
}
return q, tableInfo, nil
}
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService)
if err != nil {
return nil, err
}
newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil)
@ -167,6 +182,20 @@ func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err e
return newResultSetProxy(newResultSet), nil
}
func (rs *rsModule) rsFilter(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
rsProxy SimpleProxy[*models.ResultSet]
filter string
)
if err := args.Bind(&rsProxy, &filter); err != nil {
return nil, err
}
newResultSet := rs.tableService.Filter(rsProxy.ProxyValue(), filter)
return newResultSetProxy(newResultSet), nil
}
var rsNextPageDoc = repl.Doc{
Brief: "Returns the next page of the passed in result-set",
Usage: "RESULT_SET",
@ -207,6 +236,33 @@ func (rs *rsModule) rsUnion(ctx context.Context, args ucl.CallArgs) (_ any, err
return newResultSetProxy(rsProxy1.ProxyValue().MergeWith(rsProxy2.ProxyValue())), nil
}
func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
var (
item itemProxy
expr string
val ucl.Object
)
if err := args.Bind(&item, &expr, &val); err != nil {
return nil, err
}
q, err := queryexpr.Parse(expr)
if err != nil {
return nil, err
}
// TEMP
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 moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module {
m := &rsModule{
tableService: tableService,
@ -219,8 +275,10 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module
"new": m.rsNew,
"query": m.rsQuery,
"scan": m.rsScan,
"filter": m.rsFilter,
"next-page": m.rsNextPage,
"union": m.rsUnion,
"set": m.rsSet,
},
}
}

View file

@ -0,0 +1,195 @@
package cmdpacks
import (
"context"
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/services/tables"
"ucl.lmika.dev/ucl"
)
type uiModule struct {
tableService *tables.Service
state *controllers.State
ckb *customKeyBinding
readController *controllers.TableReadController
}
func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error) {
var prompt string
if err := args.Bind(&prompt); err != nil {
return nil, err
}
resChan := make(chan string)
go func() {
commandctrl.PostMsg(ctx, events.PromptForInput(prompt, nil, func(value string) tea.Msg {
resChan <- value
return nil
}))
}()
select {
case value := <-resChan:
return value, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (m *uiModule) uiConfirm(ctx context.Context, args ucl.CallArgs) (any, error) {
var prompt string
if err := args.Bind(&prompt); err != nil {
return nil, err
}
resChan := make(chan bool)
go func() {
commandctrl.PostMsg(ctx, events.Confirm(prompt, func(value bool) tea.Msg {
resChan <- value
return nil
}))
}()
select {
case value := <-resChan:
return value, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (m *uiModule) uiPromptTable(ctx context.Context, args ucl.CallArgs) (any, error) {
tables, err := m.tableService.ListTables(context.Background())
if err != nil {
return nil, err
}
resChan := make(chan string)
go func() {
commandctrl.PostMsg(ctx, controllers.PromptForTableMsg{
Tables: tables,
OnSelected: func(tableName string) tea.Msg {
resChan <- tableName
return nil
},
})
}()
select {
case value := <-resChan:
return value, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (m *uiModule) uiBind(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
bindName string
key string
inv ucl.Invokable
)
if args.NArgs() == 2 {
if err := args.Bind(&key, &inv); err != nil {
return nil, err
}
bindName = "custom." + key
} else {
if err := args.Bind(&bindName, &key, &inv); err != nil {
return nil, err
}
}
invoker := commandctrl.GetInvoker(ctx)
m.ckb.bindings[bindName] = func() tea.Msg {
return invoker.Invoke(inv, nil)
}
m.ckb.keyBindings[key] = bindName
return nil, nil
}
func (m *uiModule) uiQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService)
if err != nil {
return nil, err
}
commandctrl.PostMsg(ctx, m.readController.RunQuery(q, tableInfo))
return nil, nil
}
func (m *uiModule) uiFilter(ctx context.Context, args ucl.CallArgs) (any, error) {
var filter string
if err := args.Bind(&filter); err != nil {
return nil, err
}
commandctrl.PostMsg(ctx, m.readController.Filter(filter))
return nil, nil
}
func moduleUI(
tableService *tables.Service,
state *controllers.State,
readController *controllers.TableReadController,
) (ucl.Module, controllers.CustomKeyBindingSource) {
m := &uiModule{
tableService: tableService,
state: state,
readController: readController,
ckb: &customKeyBinding{
bindings: map[string]tea.Cmd{},
keyBindings: map[string]string{},
},
}
return ucl.Module{
Name: "ui",
Builtins: map[string]ucl.BuiltinHandler{
"prompt": m.uiPrompt,
"prompt-table": m.uiPromptTable,
"confirm": m.uiConfirm,
"query": m.uiQuery,
"filter": m.uiFilter,
"bind": m.uiBind,
},
}, m.ckb
}
type customKeyBinding struct {
bindings map[string]tea.Cmd
keyBindings map[string]string
}
func (c *customKeyBinding) LookupBinding(theKey string) string {
return c.keyBindings[theKey]
}
func (c *customKeyBinding) CustomKeyCommand(key string) tea.Cmd {
bindingName, ok := c.keyBindings[key]
if !ok {
return nil
}
binding, ok := c.bindings[bindingName]
if !ok {
return nil
}
return binding
}
func (c *customKeyBinding) UnbindKey(key string) {
delete(c.keyBindings, key)
}
func (c *customKeyBinding) Rebind(bindingName string, newKey string) error {
c.keyBindings[newKey] = bindingName
return nil
}

View file

@ -19,6 +19,30 @@ type StandardCommands struct {
WriteController *controllers.TableWriteController
ExportController *controllers.ExportController
KeyBindingController *controllers.KeyBindingController
modUI ucl.Module
}
func NewStandardCommands(
tableService *tables.Service,
state *controllers.State,
readController *controllers.TableReadController,
writeController *controllers.TableWriteController,
exportController *controllers.ExportController,
keyBindingController *controllers.KeyBindingController,
) StandardCommands {
modUI, ckbs := moduleUI(tableService, state, readController)
keyBindingController.SetCustomKeyBindingSource(ckbs)
return StandardCommands{
TableService: tableService,
State: state,
ReadController: readController,
WriteController: writeController,
ExportController: exportController,
KeyBindingController: keyBindingController,
modUI: modUI,
}
}
var cmdQuitDoc = repl.Doc{
@ -364,6 +388,7 @@ func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (an
func (sc StandardCommands) InstOptions() []ucl.InstOption {
return []ucl.InstOption{
ucl.WithModule(moduleRS(sc.TableService, sc.State)),
ucl.WithModule(sc.modUI),
}
}

View file

@ -14,8 +14,10 @@ import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
"github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/lmika/dynamo-browse/test/testworkspace"
bus "github.com/lmika/events"
@ -132,15 +134,12 @@ func newService(t *testing.T, opts ...serviceOpt) *services {
columnsController := controllers.NewColumnsController(readController, eventBus)
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
keyBindingService := keybindings_service.NewService(keybindings.Default())
keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil)
_ = settingsController
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
State: state,
TableService: service,
ReadController: readController,
WriteController: writeController,
ExportController: exportController,
},
cmdpacks.NewStandardCommands(service, state, readController, writeController, exportController, keyBindingController),
)
s.State = state

View file

@ -139,8 +139,26 @@ func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput str
return c.uclInst.Eval(ctx, commandInput)
}
func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea.Msg) {
execCtx := execContext{ctrl: c}
ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx)
res, err := invokable.Invoke(ctx, args)
if err != nil {
msg = events.Error(err)
} else if res != nil {
msg = events.StatusMsg(fmt.Sprint(res))
}
if execCtx.requestRefresh {
c.postMessage(events.ResultSetUpdated{})
}
return msg
}
func (c *CommandController) cmdLooper() {
ctx := context.WithValue(context.Background(), commandCtlKey, c)
execCtx := execContext{ctrl: c}
ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx)
for {
select {
@ -151,6 +169,9 @@ func (c *CommandController) cmdLooper() {
} else if res != nil {
c.postMessage(events.StatusMsg(fmt.Sprint(res)))
}
if execCtx.requestRefresh {
c.postMessage(events.ResultSetUpdated{})
}
}
}
}

View file

@ -3,33 +3,60 @@ package commandctrl
import (
"context"
tea "github.com/charmbracelet/bubbletea"
"ucl.lmika.dev/ucl"
)
type commandCtlKeyType struct{}
var commandCtlKey = commandCtlKeyType{}
type execContext struct {
ctrl *CommandController
requestRefresh bool
}
func PostMsg(ctx context.Context, msg tea.Msg) {
cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController)
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if ok {
cmdCtl.postMessage(msg)
cmdCtl.ctrl.postMessage(msg)
}
}
func SelectedItemIndex(ctx context.Context) (int, bool) {
cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController)
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return 0, false
}
return cmdCtl.uiStateProvider.SelectedItemIndex(), true
return cmdCtl.ctrl.uiStateProvider.SelectedItemIndex(), true
}
func SetSelectedItemIndex(ctx context.Context, newIdx int) tea.Msg {
cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController)
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return nil
}
return cmdCtl.uiStateProvider.SetSelectedItemIndex(newIdx)
return cmdCtl.ctrl.uiStateProvider.SetSelectedItemIndex(newIdx)
}
func GetInvoker(ctx context.Context) Invoker {
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return nil
}
return cmdCtl.ctrl
}
func QueueRefresh(ctx context.Context) {
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return
}
cmdCtl.requestRefresh = true
}
type Invoker interface {
Invoke(invokable ucl.Invokable, args []any) tea.Msg
}

View file

@ -0,0 +1,5 @@
package events
type ResultSetUpdated struct {
StatusMessage string
}

View file

@ -81,14 +81,6 @@ type PromptForTableMsg struct {
OnSelected func(tableName string) tea.Msg
}
type ResultSetUpdated struct {
statusMessage string
}
func (rs ResultSetUpdated) StatusMessage() string {
return rs.statusMessage
}
type ShowColumnOverlay struct{}
type HideColumnOverlay struct{}

View file

@ -20,6 +20,10 @@ func NewKeyBindingController(service *keybindings.Service, customBindingSource C
}
}
func (kb *KeyBindingController) SetCustomKeyBindingSource(customBindingSource CustomKeyBindingSource) {
kb.customBindingSource = customBindingSource
}
func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg {
existingBinding := kb.findExistingBinding(newKey)
if existingBinding == "" {

View file

@ -182,6 +182,10 @@ func (c *TableReadController) PromptForQuery() tea.Msg {
}
}
func (c *TableReadController) RunQuery(q *queryexpr.QueryExpr, table *models.TableInfo) tea.Msg {
return c.runQuery(table, q, "", true, nil)
}
func (c *TableReadController) runQuery(
tableInfo *models.TableInfo,
query *queryexpr.QueryExpr,
@ -339,27 +343,31 @@ func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
}
func (c *TableReadController) Filter() tea.Msg {
func (c *TableReadController) PromptForFilter() tea.Msg {
return events.PromptForInputMsg{
Prompt: "filter: ",
History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
OnDone: func(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
return events.StatusMsg("Result-set is nil")
}
return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) {
newResultSet := c.tableService.Filter(resultSet, value)
return newResultSet, nil
}).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit()
return c.Filter(value)
},
}
}
func (c *TableReadController) Filter(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
return events.StatusMsg("Result-set is nil")
}
return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) {
newResultSet := c.tableService.Filter(resultSet, value)
return newResultSet, nil
}).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit()
}
func (c *TableReadController) handleResultSetFromJobResult(
filter string,
pushbackStack, errIfEmpty bool,

View file

@ -44,7 +44,7 @@ func (twc *TableWriteController) ToggleMark(idx int) tea.Msg {
resultSet.SetMark(idx, !resultSet.Marked(idx))
})
return ResultSetUpdated{}
return events.ResultSetUpdated{}
}
func (twc *TableWriteController) NewItem() tea.Msg {
@ -148,7 +148,7 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
},
}
}
@ -181,7 +181,7 @@ func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.Q
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
},
}
}
@ -205,7 +205,7 @@ func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryEx
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
},
}
}
@ -234,7 +234,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr *queryexpr.QueryExpr
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
},
}
}
@ -255,7 +255,7 @@ func (twc *TableWriteController) setNullValue(idx int, attr *queryexpr.QueryExpr
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
}
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
@ -291,7 +291,7 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
}
func (twc *TableWriteController) PutItems() tea.Msg {
@ -351,8 +351,8 @@ func (twc *TableWriteController) PutItems() tea.Msg {
}
return rs, nil
}).OnDone(func(rs *models.ResultSet) tea.Msg {
return ResultSetUpdated{
statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
return events.ResultSetUpdated{
StatusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
}
}).Submit()
},
@ -379,7 +379,7 @@ func (twc *TableWriteController) TouchItem(idx int) tea.Msg {
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
return events.ResultSetUpdated{}
},
}
}

View file

@ -276,10 +276,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case controllers.SetTableItemView:
cmd := m.setMainViewIndex(msg.ViewIndex)
return m, cmd
case controllers.ResultSetUpdated:
case events.ResultSetUpdated:
return m, tea.Batch(
m.tableView.Refresh(),
events.SetStatus(msg.StatusMessage()),
events.SetStatus(msg.StatusMessage),
)
case tea.KeyMsg:
// TODO: use modes here
@ -302,7 +302,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keyMap.PromptForQuery):
return m, m.tableReadController.PromptForQuery
case key.Matches(msg, m.keyMap.PromptForFilter):
return m, m.tableReadController.Filter
return m, m.tableReadController.PromptForFilter
case key.Matches(msg, m.keyMap.FetchNextPage):
return m, m.tableReadController.NextPage
case key.Matches(msg, m.keyMap.ViewBack):