diff --git a/.gitignore b/.gitignore index 2b59837..b14c548 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ debug.log -.DS_store -.idea diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 43fec27..ac7ac1f 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -4,11 +4,9 @@ import ( "context" "flag" "fmt" - "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" "log" "net" "os" - "strings" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" @@ -46,7 +44,6 @@ 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() @@ -130,7 +127,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 == "" { @@ -160,20 +157,10 @@ func main() { } keyBindingService := keybindings_service.NewService(keyBindings) - keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil) + keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController) - stdCommands := cmdpacks.NewStandardCommands( - tableService, - state, - tableReadController, - tableWriteController, - exportController, - keyBindingController, - pasteboardProvider, - ) - - commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands) - //commandController.AddCommandLookupExtension(scriptController) + commandController := commandctrl.NewCommandController(inputHistoryService) + commandController.AddCommandLookupExtension(scriptController) commandController.SetCommandCompletionProvider(columnsController) model := ui.NewModel( @@ -185,13 +172,12 @@ func main() { jobsController, itemRendererService, commandController, - //scriptController, + scriptController, eventBus, keyBindingController, pasteboardProvider, keyBindings, ) - commandController.SetUIStateProvider(&model) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. osstyle.DetectCurrentScheme() @@ -199,14 +185,9 @@ func main() { p := tea.NewProgram(model, tea.WithAltScreen()) jobsController.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) + scriptController.Init() + scriptController.SetMessageSender(p.Send) + commandController.SetMessageSender(p.Send) log.Println("launching") if err := p.Start(); err != nil { diff --git a/go.mod b/go.mod index 3113de7..bf5197e 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/lmika/dynamo-browse -go 1.24 +go 1.22 -toolchain go1.24.0 +toolchain go1.22.0 require ( github.com/alecthomas/participle/v2 v2.1.1 @@ -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-20250525023717-3076897eb73e // indirect + ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 // indirect ) diff --git a/go.sum b/go.sum index 2b9e341..526e0ec 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ= github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJzKIkbg= -github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= -github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= @@ -432,33 +430,3 @@ ucl.lmika.dev v0.0.0-20240427010304-6315afc54287 h1:llPHrjca54duvQx9PgMTFDhOW2VQ ucl.lmika.dev v0.0.0-20240427010304-6315afc54287/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw= ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4= -ucl.lmika.dev v0.0.0-20240504001444-cf3a12bf0d4d h1:OqGmR0Y+OG6aFIOlXy2QwEHtuUNasYCh/6cxHokYQj4= -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= -ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2 h1:cvguOoQ0HVgLKbHH17ZHvAUFht6HXApLi0o8JOdaaNU= -ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0ZyZ26QjHAbj8Lto= -ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e h1:CQ+qPqI5lYiiEM0tNAr4jS0iMz16bFqOui5mU3AHsCU= -ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675 h1:kGKh3zj6lMzOrGAquFW7ROgx9/6nwJ8DXiSLtceRiak= -ucl.lmika.dev v0.0.0-20250517212052-51e35aa9a675/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4 h1:rnietWu2B+NXLqKfo7jgf6r+srMwxFa5eizywkq4LFk= -ucl.lmika.dev v0.0.0-20250517212757-33d04ba18db4/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250517213937-94aad417121d h1:CMcA8aQV6iiPK75EbHvoIVZhZmSggfrWNhK9BFm2aIg= -ucl.lmika.dev v0.0.0-20250517213937-94aad417121d/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94 h1:x3IRtT1jbedblimi2hesKGBihg243+wNOSvagCPR0KU= -ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8= -ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3 h1:ZMQ1rkcAWa///c3bVvlXbtuqjfAWxDm01abQl3g/YVw= -ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= -ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5oIhwdfqbmZkvX3Q= -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 deleted file mode 100644 index 528499b..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/modpb.go +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 148723c..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ /dev/null @@ -1,308 +0,0 @@ -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" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" - "github.com/pkg/errors" - "time" - "ucl.lmika.dev/repl" - "ucl.lmika.dev/ucl" -) - -type rsModule struct { - tableService *tables.Service - state *controllers.State -} - -var rsNewDoc = repl.Doc{ - Brief: "Creates a new, empty result set", - Usage: "[-table NAME]", - Detailed: ` - The result set assumes the details of the current table. If no table is specified, - the command will return an error. - `, -} - -func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (_ any, err error) { - var tableInfo *models.TableInfo - if args.HasSwitch("table") { - var tblName string - if err := args.BindSwitch("table", &tblName); err != nil { - return nil, err - } - - tableInfo, err = rs.tableService.Describe(ctx, tblName) - if err != nil { - return nil, err - } - } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { - tableInfo = currRs.TableInfo - } else { - return nil, errors.New("no table specified") - } - - return newResultSetProxy(&models.ResultSet{ - TableInfo: tableInfo, - Created: time.Now(), - }), nil -} - -var rsQueryDoc = repl.Doc{ - Brief: "Runs a query and returns the results as a result-set", - Usage: "QUERY [ARGS] [-table NAME]", - Args: []repl.ArgDoc{ - {Name: "query", Brief: "Query expression to run"}, - {Name: "args", Brief: "Hash of argument values to substitute into the query"}, - {Name: "-table", Brief: "Optional table name to use for the query"}, - }, - Detailed: ` - If no table is specified, then the value of @table will be used. If this is unavailable, - the command will return an 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, nil, err - } - - q, err := queryexpr.Parse(expr) - if err != nil { - return nil, nil, err - } - - if args.NArgs() > 0 { - var queryArgs ucl.Hashable - if err := args.Bind(&queryArgs); err != nil { - return nil, nil, err - } - - queryNames := map[string]string{} - queryValues := map[string]types.AttributeValue{} - queryArgs.Each(func(k string, v ucl.Object) error { - if v == nil { - return nil - } - - queryNames[k] = v.String() - - switch v.(type) { - case ucl.StringObject: - queryValues[k] = &types.AttributeValueMemberS{Value: v.String()} - case ucl.IntObject: - queryValues[k] = &types.AttributeValueMemberN{Value: v.String()} - // TODO: other types - } - return nil - }) - - q = q.WithNameParams(queryNames).WithValueParams(queryValues) - } - - var tableInfo *models.TableInfo - if args.HasSwitch("table") { - var tblName string - if err := args.BindSwitch("table", &tblName); err != nil { - return nil, nil, err - } - - tableInfo, err = tablesService.Describe(ctx, tblName) - if err != nil { - return nil, nil, err - } - } else if currentRS != nil && currentRS.TableInfo != nil { - tableInfo = currentRS.TableInfo - } else { - 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) - if err != nil { - return nil, err - } - - return newResultSetProxy(newResultSet), nil -} - -var rsScanDoc = repl.Doc{ - Brief: "Performs a scan of the table and returns the results as a result-set", - Usage: "[-table NAME]", - Args: []repl.ArgDoc{ - {Name: "-table", Brief: "Optional table name to use for the query"}, - }, - Detailed: ` - If no table is specified, then the value of @table will be used. If this is unavailable, - the command will return an error. - `, -} - -func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err error) { - var tableInfo *models.TableInfo - if args.HasSwitch("table") { - var tblName string - if err := args.BindSwitch("table", &tblName); err != nil { - return nil, err - } - - tableInfo, err = rs.tableService.Describe(ctx, tblName) - if err != nil { - return nil, err - } - } else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil { - tableInfo = currRs.TableInfo - } else { - return nil, errors.New("no table specified") - } - - newResultSet, err := rs.tableService.Scan(context.Background(), tableInfo) - if err != nil { - return nil, err - } - - 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", - Args: []repl.ArgDoc{ - {Name: "result-set", Brief: "Result set to fetch the next page of"}, - }, - Detailed: ` - If no next page exists, the command will return nil. - `, -} - -func (rs *rsModule) rsNextPage(ctx context.Context, args ucl.CallArgs) (_ any, err error) { - var rsProxy SimpleProxy[*models.ResultSet] - - if err := args.Bind(&rsProxy); err != nil { - return nil, err - } - - if !rsProxy.value.HasNextPage() { - return nil, nil - } - - nextPage, err := rs.tableService.NextPage(ctx, rsProxy.value) - if err != nil { - return nil, err - } - - return newResultSetProxy(nextPage), nil -} - -func (rs *rsModule) rsUnion(ctx context.Context, args ucl.CallArgs) (_ any, err error) { - var rsProxy1, rsProxy2 SimpleProxy[*models.ResultSet] - - if err := args.Bind(&rsProxy1, &rsProxy2); err != nil { - return nil, 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: 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) - - 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 -} - -func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { - m := &rsModule{ - tableService: tableService, - state: state, - } - - return ucl.Module{ - Name: "rs", - Builtins: map[string]ucl.BuiltinHandler{ - "new": m.rsNew, - "query": m.rsQuery, - "scan": m.rsScan, - "filter": m.rsFilter, - "next-page": m.rsNextPage, - "union": m.rsUnion, - "set": m.rsSet, - "del": m.rsDel, - }, - } -} diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs_test.go b/internal/common/ui/commandctrl/cmdpacks/modrs_test.go deleted file mode 100644 index 878ad3b..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/modrs_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package cmdpacks_test - -import ( - "fmt" - "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestModRS_New(t *testing.T) { - svc := newService(t) - - rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`) - - assert.NoError(t, err) - assert.IsType(t, rsProxy, cmdpacks.SimpleProxy[*models.ResultSet]{}) -} - -func TestModRS_NextPage(t *testing.T) { - t.Run("multiple pages", func(t *testing.T) { - svc := newService(t, withDataGenerator(largeTestData), withTable("large-table"), withDefaultLimit(20)) - - hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`) - assert.NoError(t, err) - assert.True(t, hasNextPage.(bool)) - - // Page 2 - rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`) - - assert.NoError(t, err) - assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) - assert.Equal(t, 20, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items())) - - hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset).HasNextPage`) - assert.NoError(t, err) - assert.True(t, hasNextPage.(bool)) - - // Page 3 - rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset | rs:next-page`) - - assert.NoError(t, err) - assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) - assert.Equal(t, 10, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items())) - - hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset | rs:next-page).HasNextPage`) - assert.NoError(t, err) - assert.False(t, hasNextPage.(bool)) - - // Last page - rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page (rs:next-page @resultset | rs:next-page)`) - assert.NoError(t, err) - assert.Nil(t, rsProxy) - }) - - t.Run("only one page", func(t *testing.T) { - svc := newService(t) - - hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`) - assert.NoError(t, err) - assert.False(t, hasNextPage.(bool)) - - rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`) - assert.NoError(t, err) - assert.Nil(t, rsProxy) - }) -} - -func TestModRS_Union(t *testing.T) { - svc := newService(t, withDefaultLimit(2)) - - rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), ` - $mr = rs:union @resultset (rs:next-page @resultset) - - assert (eq (len $mr.Items) 3) "expected len == 3" - assert (eq $mr.Items.(0).pk "abc") "expected 0.pk" - assert (eq $mr.Items.(0).sk "111") "expected 0.sk" - assert (eq $mr.Items.(1).pk "abc") "expected 1.pk" - assert (eq $mr.Items.(1).sk "222") "expected 1.sk" - assert (eq $mr.Items.(2).pk "bbb") "expected 2.pk" - assert (eq $mr.Items.(2).sk "131") "expected 2.sk" - - $mr - `) - - assert.NoError(t, err) - assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy) - - rs := rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue() - assert.Equal(t, 3, len(rs.Items())) -} - -func TestModRS_Query(t *testing.T) { - tests := []struct { - descr string - cmd string - wantRows []int - }{ - { - descr: "query with pk 1", - cmd: `rs:query 'pk="abc"' -table service-test-data`, - wantRows: []int{0, 1}, - }, - { - descr: "query with pk 2", - cmd: `rs:query 'pk="bbb"' -table service-test-data`, - wantRows: []int{2}, - }, - { - descr: "query with sk 1", - cmd: `rs:query 'sk="222"' -table service-test-data`, - wantRows: []int{1}, - }, - { - descr: "query with args 1", - cmd: `rs:query 'pk=$v' [v:'abc'] -table service-test-data`, - wantRows: []int{0, 1}, - }, - { - descr: "query with args 2", - cmd: `rs:query ':k=$v' [k:'pk' v:'abc'] -table service-test-data`, - wantRows: []int{0, 1}, - }, - { - descr: "query with args 3", - cmd: `rs:query ':k=$v' [k:'beta' v:1231] -table service-test-data`, - wantRows: []int{1}, - }, - { - descr: "query with args with no table set", - cmd: `rs:query ':k=$v' [k:'beta' v:1231]`, - wantRows: []int{1}, - }, - } - for _, tt := range tests { - t.Run(tt.descr, func(t *testing.T) { - svc := newService(t) - - res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) - assert.NoError(t, err) - - rs := res.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue() - assert.Len(t, rs.Items(), len(tt.wantRows)) - - for i, rowIndex := range tt.wantRows { - for key, want := range svc.testData[0].Data[rowIndex] { - have, ok := rs.Items()[i].AttributeValueAsString(key) - assert.True(t, ok) - assert.Equal(t, fmt.Sprint(want), have) - } - } - }) - } -} diff --git a/internal/common/ui/commandctrl/cmdpacks/modui.go b/internal/common/ui/commandctrl/cmdpacks/modui.go deleted file mode 100644 index 26253e6..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/modui.go +++ /dev/null @@ -1,210 +0,0 @@ -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) 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 { - 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{ - "command": m.uiCommand, - "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 -} diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go deleted file mode 100644 index 8dad249..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ /dev/null @@ -1,216 +0,0 @@ -package cmdpacks - -import ( - "fmt" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "maps" - "strconv" - "ucl.lmika.dev/ucl" -) - -type proxyInfo[T comparable] struct { - fields map[string]func(t T) ucl.Object - lenFunc func(t T) int - strFunc func(t T) string -} - -type SimpleProxy[T comparable] struct { - value T - proxyInfo *proxyInfo[T] -} - -func (tp SimpleProxy[T]) ProxyValue() T { - return tp.value -} - -func (tp SimpleProxy[T]) String() string { - if tp.proxyInfo.strFunc != nil { - return tp.proxyInfo.strFunc(tp.value) - } - return fmt.Sprint(tp.value) -} - -func (tp SimpleProxy[T]) Truthy() bool { - var zeroT T - return tp.value != zeroT -} - -func (tp SimpleProxy[T]) Len() int { - if tp.proxyInfo.lenFunc != nil { - return tp.proxyInfo.lenFunc(tp.value) - } - return len(tp.proxyInfo.fields) -} - -func (tp SimpleProxy[T]) Value(k string) ucl.Object { - f, ok := tp.proxyInfo.fields[k] - if !ok { - return nil - } - return f(tp.value) -} - -func (tp SimpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error { - for key := range maps.Keys(tp.proxyInfo.fields) { - if err := fn(key, tp.Value(key)); err != nil { - return err - } - } - return nil -} - -type simpleProxyList[T comparable] struct { - values []T - converter func(T) ucl.Object -} - -func newSimpleProxyList[T comparable](values []T, converter func(T) ucl.Object) simpleProxyList[T] { - return simpleProxyList[T]{values: values, converter: converter} -} - -func (tp simpleProxyList[T]) String() string { - return fmt.Sprint(tp.values) -} - -func (tp simpleProxyList[T]) Truthy() bool { - return len(tp.values) > 0 -} - -func (tp simpleProxyList[T]) Len() int { - return len(tp.values) -} - -func (tp simpleProxyList[T]) Index(k int) ucl.Object { - return tp.converter(tp.values[k]) -} - -func newResultSetProxy(rs *models.ResultSet) ucl.Object { - return SimpleProxy[*models.ResultSet]{value: rs, proxyInfo: resultSetProxyFields} -} - -var resultSetProxyFields = &proxyInfo[*models.ResultSet]{ - lenFunc: func(t *models.ResultSet) int { return len(t.Items()) }, - strFunc: func(t *models.ResultSet) string { - return fmt.Sprintf("ResultSet(%v:%d)", t.TableInfo.Name, len(t.Items())) - }, - fields: map[string]func(t *models.ResultSet) ucl.Object{ - "Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, - "Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, - "HasNextPage": func(t *models.ResultSet) ucl.Object { return ucl.BoolObject(t.HasNextPage()) }, - }, -} - -func newTableProxy(table *models.TableInfo) ucl.Object { - return SimpleProxy[*models.TableInfo]{value: table, proxyInfo: tableProxyFields} -} - -var tableProxyFields = &proxyInfo[*models.TableInfo]{ - strFunc: func(t *models.TableInfo) string { - return fmt.Sprintf("Table(%v)", t.Name) - }, - fields: map[string]func(t *models.TableInfo) ucl.Object{ - "Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) }, - "Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) }, - "DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) }, - "GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) }, - }, -} - -func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object { - return SimpleProxy[models.KeyAttribute]{value: keyAttrs, proxyInfo: keyAttributeProxyFields} -} - -var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{ - strFunc: func(t models.KeyAttribute) string { - return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey) - }, - fields: map[string]func(t models.KeyAttribute) ucl.Object{ - "PK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, - "SK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, - }, -} - -func newGSIProxy(gsi models.TableGSI) ucl.Object { - return SimpleProxy[models.TableGSI]{value: gsi, proxyInfo: gsiProxyFields} -} - -var gsiProxyFields = &proxyInfo[models.TableGSI]{ - strFunc: func(t models.TableGSI) string { - return fmt.Sprintf("TableGSI(%v,(%v,%v))", t.Name, t.Keys.PartitionKey, t.Keys.SortKey) - }, - fields: map[string]func(t models.TableGSI) ucl.Object{ - "Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) }, - "Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) }, - }, -} - -type resultSetItemsProxy struct { - resultSet *models.ResultSet -} - -func (ip resultSetItemsProxy) String() string { - return "RSItem()" -} - -func (ip resultSetItemsProxy) Truthy() bool { - return len(ip.resultSet.Items()) > 0 -} - -func (tp resultSetItemsProxy) Len() int { - return len(tp.resultSet.Items()) -} - -func (tp resultSetItemsProxy) Index(k int) ucl.Object { - return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]} -} - -type itemProxy struct { - resultSet *models.ResultSet - idx int - item models.Item -} - -func (ip itemProxy) String() string { - return fmt.Sprintf("RSItems(%v)", len(ip.item)) -} - -func (ip itemProxy) Truthy() bool { - return len(ip.item) > 0 -} - -func (tp itemProxy) Len() int { - return len(tp.item) -} - -func (tp itemProxy) Value(k string) ucl.Object { - f, ok := tp.item[k] - if !ok { - return nil - } - return convertAttributeValueToUCLObject(f) -} - -func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error { - for key := range maps.Keys(tp.item) { - if err := fn(key, tp.Value(key)); err != nil { - return err - } - } - return nil -} - -func convertAttributeValueToUCLObject(attrValue types.AttributeValue) ucl.Object { - switch t := attrValue.(type) { - case *types.AttributeValueMemberS: - return ucl.StringObject(t.Value) - case *types.AttributeValueMemberN: - i, err := strconv.ParseInt(t.Value, 10, 64) - if err != nil { - return nil - } - return ucl.IntObject(i) - } - // TODO: the rest - return nil -} diff --git a/internal/common/ui/commandctrl/cmdpacks/pvars.go b/internal/common/ui/commandctrl/cmdpacks/pvars.go deleted file mode 100644 index f07965a..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/pvars.go +++ /dev/null @@ -1,62 +0,0 @@ -package cmdpacks - -import ( - "context" - "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" -) - -type tablePVar struct { - state *controllers.State -} - -func (rs tablePVar) Get(ctx context.Context) (any, error) { - return newTableProxy(rs.state.ResultSet().TableInfo), nil -} - -type resultSetPVar struct { - state *controllers.State - readController *controllers.TableReadController -} - -func (rs resultSetPVar) Get(ctx context.Context) (any, error) { - return newResultSetProxy(rs.state.ResultSet()), nil -} - -func (rs resultSetPVar) Set(ctx context.Context, value any) error { - rsVal, ok := value.(SimpleProxy[*models.ResultSet]) - if !ok { - return errors.New("new value to @resultset is nil or not a result set") - } - - msg := rs.readController.SetResultSet(rsVal.value) - commandctrl.PostMsg(ctx, msg) - return nil -} - -type itemPVar struct { - state *controllers.State -} - -func (rs itemPVar) Get(ctx context.Context) (any, error) { - selItem, ok := commandctrl.SelectedItemIndex(ctx) - if !ok { - return nil, errors.New("no item selected") - } - return itemProxy{rs.state.ResultSet(), selItem, rs.state.ResultSet().Items()[selItem]}, nil -} - -func (rs itemPVar) Set(ctx context.Context, value any) error { - rsVal, ok := value.(itemProxy) - if !ok { - return errors.New("new value to @item is not an item") - } - - if msg := commandctrl.SetSelectedItemIndex(ctx, rsVal.idx); msg != nil { - commandctrl.PostMsg(ctx, msg) - } - - return nil -} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go deleted file mode 100644 index 9019ec6..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ /dev/null @@ -1,421 +0,0 @@ -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" - "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" - "ucl.lmika.dev/ucl" -) - -type StandardCommands struct { - TableService *tables.Service - State *controllers.State - ReadController *controllers.TableReadController - WriteController *controllers.TableWriteController - ExportController *controllers.ExportController - KeyBindingController *controllers.KeyBindingController - PBProvider *pasteboardprovider.Provider - - modUI ucl.Module -} - -func NewStandardCommands( - tableService *tables.Service, - state *controllers.State, - readController *controllers.TableReadController, - writeController *controllers.TableWriteController, - exportController *controllers.ExportController, - keyBindingController *controllers.KeyBindingController, - pbProvider *pasteboardprovider.Provider, -) StandardCommands { - modUI, ckbs := moduleUI(tableService, state, readController) - keyBindingController.SetCustomKeyBindingSource(ckbs) - - return StandardCommands{ - TableService: tableService, - State: state, - ReadController: readController, - WriteController: writeController, - ExportController: exportController, - KeyBindingController: keyBindingController, - PBProvider: pbProvider, - modUI: modUI, - } -} - -var cmdQuitDoc = repl.Doc{ - Brief: "Quits dynamo-browse", - 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: "[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 -} - -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) InstOptions() []ucl.InstOption { - return []ucl.InstOption{ - ucl.WithModule(moduleRS(sc.TableService, sc.State)), - ucl.WithModule(sc.modUI), - ucl.WithModule(modulePB(sc.PBProvider)), - } -} - -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 - - ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) - ucl.SetPseudoVar("table", tablePVar{sc.State}) - ucl.SetPseudoVar("item", itemPVar{sc.State}) -} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go deleted file mode 100644 index 228c6be..0000000 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package cmdpacks_test - -import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" - "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" - "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" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestStdCmds_Mark(t *testing.T) { - tests := []struct { - descr string - cmd string - wantMarks []bool - }{ - {descr: "mark default", cmd: "mark", wantMarks: []bool{true, true, true}}, - {descr: "mark all", cmd: "mark all", wantMarks: []bool{true, true, true}}, - {descr: "mark none", cmd: "mark none", wantMarks: []bool{false, false, false}}, - {descr: "mark where", cmd: `mark -where 'sk="222"'`, wantMarks: []bool{false, true, false}}, - {descr: "mark toggle", cmd: "mark ; mark toggle", wantMarks: []bool{false, false, false}}, - } - - for _, tt := range tests { - t.Run(tt.descr, func(t *testing.T) { - svc := newService(t) - - _, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) - assert.NoError(t, err) - - for i, want := range tt.wantMarks { - assert.Equal(t, want, svc.State.ResultSet().Marked(i)) - } - }) - } -} - -type testDataGenerator func() []testdynamo.TestData -type services struct { - CommandController *commandctrl.CommandController - SelItemIndex int - - State *controllers.State - - settingStore *settingstore.SettingStore - table string - - testDataGenerator testDataGenerator - testData []testdynamo.TestData -} - -type serviceOpt func(*services) - -func withDataGenerator(tg testDataGenerator) serviceOpt { - return func(s *services) { - s.testDataGenerator = tg - } -} - -func withTable(table string) serviceOpt { - return func(s *services) { - s.table = table - } -} - -func withDefaultLimit(limit int) serviceOpt { - return func(s *services) { - s.settingStore.SetDefaultLimit(limit) - } -} - -func newService(t *testing.T, opts ...serviceOpt) *services { - ws := testworkspace.New(t) - - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) - settingStore := settingstore.New(ws) - inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws) - - workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - inputHistoryService := inputhistory.New(inputHistoryStore) - - s := &services{ - table: "service-test-data", - settingStore: settingStore, - testDataGenerator: normalTestData, - } - - for _, opt := range opts { - opt(s) - } - - s.testData = s.testDataGenerator() - client := testdynamo.SetupTestTable(t, s.testData) - - provider := dynamo.NewProvider(client) - service := tables.NewService(provider, settingStore) - eventBus := bus.New() - - state := controllers.NewState() - jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true) - - readController := controllers.NewTableReadController( - state, - service, - workspaceService, - itemRendererService, - jobsController, - inputHistoryService, - eventBus, - pasteboardprovider.NilProvider{}, - nil, - s.table, - ) - writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) - settingsController := controllers.NewSettingsController(settingStore, eventBus) - 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.NewStandardCommands(service, state, readController, writeController, exportController, keyBindingController), - ) - - s.State = state - s.CommandController = commandController - - commandController.SetUIStateProvider(s) - readController.Init() - - return s -} - -func (s *services) SelectedItemIndex() int { - return s.SelItemIndex -} - -func (s *services) SetSelectedItemIndex(newIdx int) tea.Msg { - s.SelItemIndex = newIdx - return nil -} - -func normalTestData() []testdynamo.TestData { - return []testdynamo.TestData{ - { - TableName: "service-test-data", - Data: []map[string]interface{}{ - { - "pk": "abc", - "sk": "111", - "alpha": "This is some value", - }, - { - "pk": "abc", - "sk": "222", - "alpha": "This is another some value", - "beta": 1231, - }, - { - "pk": "bbb", - "sk": "131", - "beta": 2468, - "gamma": "foobar", - }, - }, - }, - } -} - -func largeTestData() []testdynamo.TestData { - return []testdynamo.TestData{ - { - TableName: "large-table", - Data: genRow(50, func(i int) map[string]interface{} { - return map[string]interface{}{ - "pk": fmt.Sprint(i), - "sk": fmt.Sprint(i), - "alpha": fmt.Sprintf("row %v", i), - } - }), - }, - } -} - -func genRow(count int, mapFn func(int) map[string]interface{}) []map[string]interface{} { - result := make([]map[string]interface{}, count) - for i := 0; i < count; i++ { - result[i] = mapFn(i) - } - return result -} diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index bfbe79b..a1360d5 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -1,9 +1,9 @@ package commandctrl import ( + "bufio" "bytes" "context" - "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/pkg/errors" "log" @@ -11,7 +11,6 @@ import ( "path/filepath" "strings" "ucl.lmika.dev/ucl" - "ucl.lmika.dev/ucl/builtins" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/shellwords" @@ -19,49 +18,25 @@ import ( const commandsCategory = "commands" -type cmdMessage struct { - cmd string -} - type CommandController struct { uclInst *ucl.Inst historyProvider IterProvider commandList *CommandList + msgSender func(tea.Msg) lookupExtensions []CommandLookupExtension completionProvider CommandCompletionProvider - uiStateProvider UIStateProvider - cmdChan chan cmdMessage - msgChan chan tea.Msg - interactive bool } -func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController { +func NewCommandController(historyProvider IterProvider) *CommandController { cc := &CommandController{ historyProvider: historyProvider, commandList: nil, lookupExtensions: nil, - cmdChan: make(chan cmdMessage), - msgChan: make(chan tea.Msg), - interactive: true, } - - options := []ucl.InstOption{ + cc.uclInst = ucl.New( ucl.WithOut(ucl.LineHandler(cc.printLine)), - ucl.WithModule(builtins.OS()), - ucl.WithModule(builtins.FS(nil)), - } - for _, pkg := range pkgs { - options = append(options, pkg.InstOptions()...) - } - - cc.uclInst = ucl.New(options...) - - for _, pkg := range pkgs { - pkg.ConfigureUCL(cc.uclInst) - } - - go cc.cmdLooper() - + ucl.WithMissingBuiltinHandler(cc.cmdInvoker), + ) return cc } @@ -70,14 +45,8 @@ func (c *CommandController) AddCommands(ctx *CommandList) { c.commandList = ctx } -func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) { - for msg := range c.msgChan { - msgSender(msg) - } -} - -func (c *CommandController) SetUIStateProvider(provider UIStateProvider) { - c.uiStateProvider = provider +func (c *CommandController) SetMessageSender(msg func(tea.Msg)) { + c.msgSender = msg } func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) { @@ -126,55 +95,15 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms return nil } - select { - case c.cmdChan <- cmdMessage{cmd: input}: - // good - default: - return events.Error(errors.New("command currently running")) - } - - return nil -} - -func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) { - return c.uclInst.EvalString(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) + res, err := c.uclInst.Eval(context.Background(), commandInput) 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 events.Error(err) } - return msg -} - -func (c *CommandController) cmdLooper() { - execCtx := execContext{ctrl: c} - ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx) - - for { - select { - case cmdChan := <-c.cmdChan: - res, err := c.ExecuteAndWait(ctx, cmdChan.cmd) - if err != nil { - c.postMessage(events.Error(err)) - } else if res != nil { - c.postMessage(events.StatusMsg(fmt.Sprint(res))) - } - if execCtx.requestRefresh { - c.postMessage(events.ResultSetUpdated{}) - } - } + if teaMsg, ok := res.(teaMsgWrapper); ok { + return teaMsg.msg } + return nil } func (c *CommandController) Alias(commandName string) Command { @@ -203,78 +132,41 @@ func (c *CommandController) lookupCommand(name string) Command { return nil } -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, ".") +func (c *CommandController) ExecuteFile(filename string) error { + baseFilename := filepath.Base(filename) - if stat, err := os.Stat(baseDir); err != nil { - if os.IsNotExist(err) { - continue - } - return err - } else if !stat.IsDir() { + if rcFile, err := os.ReadFile(filename); err == nil { + if err := c.executeFile(rcFile, baseFilename); err != nil { + return errors.Wrapf(err, "error executing %v", filename) + } + } else { + return errors.Wrapf(err, "error loading %v", filename) + } + return nil +} + +func (c *CommandController) executeFile(file []byte, filename string) error { + scnr := bufio.NewScanner(bytes.NewReader(file)) + + lineNo := 0 + for scnr.Scan() { + lineNo++ + line := strings.TrimSpace(scnr.Text()) + if line == "" { + continue + } else if line[0] == '#' { 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 + msg := c.execute(ExecContext{FromFile: true}, line) + switch m := msg.(type) { + case events.ErrorMsg: + log.Printf("%v:%v: error - %v", filename, lineNo, m.Error()) + case events.StatusMsg: + log.Printf("%v:%v: %v", filename, lineNo, string(m)) } } - return nil -} - -func (c *CommandController) ExecuteFile(ctx context.Context, filename string) error { - oldInteractive := c.interactive - c.interactive = false - defer func() { - c.interactive = oldInteractive - }() - - 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(ctx, rcFile); err != nil { - return errors.Wrapf(err, "error executing %v", baseFilename) - } - } else { - return errors.Wrapf(err, "error loading %v", baseFilename) - } - return nil -} - -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 + return scnr.Err() } func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) { @@ -291,24 +183,9 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc } func (c *CommandController) printLine(s string) { - if c.msgChan == nil || !c.interactive { - log.Println(s) - return + if c.msgSender != nil { + c.msgSender(events.StatusMsg(s)) } - - select { - case c.msgChan <- events.StatusMsg(s): - default: - log.Println(s) - } -} - -func (c *CommandController) postMessage(msg tea.Msg) { - if c.msgChan == nil { - return - } - - c.msgChan <- msg } type teaMsgWrapper struct { diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go deleted file mode 100644 index dffff70..0000000 --- a/internal/common/ui/commandctrl/ctx.go +++ /dev/null @@ -1,63 +0,0 @@ -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).(*execContext) - if ok { - cmdCtl.ctrl.postMessage(msg) - } -} - -func SelectedItemIndex(ctx context.Context) (int, bool) { - cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) - if !ok { - return 0, false - } - - return cmdCtl.ctrl.uiStateProvider.SelectedItemIndex(), true -} - -func SetSelectedItemIndex(ctx context.Context, newIdx int) tea.Msg { - cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext) - if !ok { - return nil - } - - 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 - Inst() *ucl.Inst -} diff --git a/internal/common/ui/commandctrl/iface.go b/internal/common/ui/commandctrl/iface.go index e708d24..1cb834a 100644 --- a/internal/common/ui/commandctrl/iface.go +++ b/internal/common/ui/commandctrl/iface.go @@ -2,15 +2,9 @@ package commandctrl import ( "context" - tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" ) type IterProvider interface { Iter(ctx context.Context, category string) services.HistoryProvider } - -type UIStateProvider interface { - SelectedItemIndex() int - SetSelectedItemIndex(newIdx int) tea.Msg -} diff --git a/internal/common/ui/commandctrl/packs.go b/internal/common/ui/commandctrl/packs.go deleted file mode 100644 index 6f5dbc6..0000000 --- a/internal/common/ui/commandctrl/packs.go +++ /dev/null @@ -1,8 +0,0 @@ -package commandctrl - -import "ucl.lmika.dev/ucl" - -type CommandPack interface { - InstOptions() []ucl.InstOption - ConfigureUCL(ucl *ucl.Inst) -} diff --git a/internal/common/ui/events/resultset.go b/internal/common/ui/events/resultset.go deleted file mode 100644 index cce739a..0000000 --- a/internal/common/ui/events/resultset.go +++ /dev/null @@ -1,5 +0,0 @@ -package events - -type ResultSetUpdated struct { - StatusMessage string -} diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index ac0e74a..7af1c41 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -81,6 +81,14 @@ 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{} diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index 23c91a9..7f4ce8e 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 events.StatusMsg("Table copied to clipboard") + return nil } // TODO: this really needs to be a service! diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go index 3b9dc76..043248f 100644 --- a/internal/dynamo-browse/controllers/keybinding.go +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -20,10 +20,6 @@ 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 == "" { diff --git a/internal/dynamo-browse/controllers/state.go b/internal/dynamo-browse/controllers/state.go index 8f7ea82..6a886d2 100644 --- a/internal/dynamo-browse/controllers/state.go +++ b/internal/dynamo-browse/controllers/state.go @@ -29,14 +29,6 @@ func (s *State) Filter() string { return s.filter } -func (s *State) SetResultSet(resultSet *models.ResultSet) { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.resultSet = resultSet - s.filter = "" -} - func (s *State) withResultSet(rs func(*models.ResultSet)) { s.mutex.Lock() defer s.mutex.Unlock() diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 38946f3..6ef6e18 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -182,10 +182,6 @@ 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, @@ -295,12 +291,6 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, return c.state.buildNewResultSetMessage("") } -func (c *TableReadController) SetResultSet(resultSet *models.ResultSet) tea.Msg { - c.state.setResultSetAndFilter(resultSet, "") - c.eventBus.Fire(newResultSetEvent, resultSet, resultSetUpdateScript) - return c.state.buildNewResultSetMessage("") -} - func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg { var ( whereExpr *queryexpr.QueryExpr @@ -343,31 +333,27 @@ func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg { }); err != nil { return events.Error(err) } - return events.ResultSetUpdated{} + return ResultSetUpdated{} } -func (c *TableReadController) PromptForFilter() tea.Msg { +func (c *TableReadController) Filter() tea.Msg { return events.PromptForInputMsg{ Prompt: "filter: ", History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory), OnDone: func(value string) tea.Msg { - return c.Filter(value) + 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) 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, diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 8f248c5..033a0cb 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -44,7 +44,7 @@ func (twc *TableWriteController) ToggleMark(idx int) tea.Msg { resultSet.SetMark(idx, !resultSet.Marked(idx)) }) - return events.ResultSetUpdated{} + return 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 events.ResultSetUpdated{} + return ResultSetUpdated{} }, } } @@ -181,7 +181,7 @@ func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.Q }); err != nil { return events.Error(err) } - return events.ResultSetUpdated{} + return ResultSetUpdated{} }, } } @@ -205,7 +205,7 @@ func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryEx }); err != nil { return events.Error(err) } - return events.ResultSetUpdated{} + return ResultSetUpdated{} }, } } @@ -234,7 +234,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr *queryexpr.QueryExpr }); err != nil { return events.Error(err) } - return events.ResultSetUpdated{} + return ResultSetUpdated{} }, } } @@ -255,7 +255,7 @@ func (twc *TableWriteController) setNullValue(idx int, attr *queryexpr.QueryExpr }); err != nil { return events.Error(err) } - return events.ResultSetUpdated{} + return 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 events.ResultSetUpdated{} + return 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 events.ResultSetUpdated{ - StatusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"), + return 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 events.ResultSetUpdated{} + return ResultSetUpdated{} }, } } diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index ff4e398..03d0421 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -151,37 +151,3 @@ func (rs *ResultSet) Sort(criteria SortCriteria) { rs.sortCriteria = criteria Sort(rs.items, criteria) } - -func (rs *ResultSet) MergeWith(otherRS *ResultSet) *ResultSet { - type pksk struct { - pk types.AttributeValue - sk types.AttributeValue - } - - if !rs.TableInfo.Equal(otherRS.TableInfo) { - return nil - } - - itemsInI := make(map[pksk]Item) - newItems := make([]Item, 0, len(rs.Items())+len(otherRS.Items())) - for _, item := range rs.Items() { - pk, sk := item.PKSK(rs.TableInfo) - itemsInI[pksk{pk, sk}] = item - newItems = append(newItems, item) - } - - for _, item := range otherRS.Items() { - pk, sk := item.PKSK(rs.TableInfo) - if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem { - newItems = append(newItems, item) - } - } - - newResultSet := &ResultSet{ - Created: time.Now(), - TableInfo: rs.TableInfo, - } - newResultSet.SetItems(newItems) - - return newResultSet -} diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 011931e..2e55c7a 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" "math/big" "strconv" - "strings" ) type exprValue interface { @@ -63,14 +62,6 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) case *types.AttributeValueMemberS: return stringExprValue(xVal.Value), nil case *types.AttributeValueMemberN: - if !strings.Contains(xVal.Value, ".") { - iVal, err := strconv.ParseInt(xVal.Value, 10, 64) - if err != nil { - return nil, err - } - return int64ExprValue(iVal), nil - } - xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven) if err != nil { return nil, err @@ -148,32 +139,6 @@ func (s int64ExprValue) typeName() string { return "N" } -type bigIntExprValue struct { - num *big.Int -} - -func (i bigIntExprValue) asGoValue() any { - return i.num -} - -func (i bigIntExprValue) asAttributeValue() types.AttributeValue { - return &types.AttributeValueMemberN{Value: i.num.String()} -} - -func (i bigIntExprValue) asInt() int64 { - return i.num.Int64() -} - -func (i bigIntExprValue) asBigFloat() *big.Float { - var f big.Float - f.SetInt64(i.num.Int64()) - return &f -} - -func (s bigIntExprValue) typeName() string { - return "N" -} - type bigNumExprValue struct { num *big.Float } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 90dfa0f..8d3297e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,11 +1,16 @@ 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" @@ -21,7 +26,7 @@ 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" - "log" + "github.com/pkg/errors" ) const ( @@ -32,6 +37,8 @@ const ( ViewModeTableOnly = 4 ViewModeCount = 5 + + initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc" ) type Model struct { @@ -40,14 +47,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 @@ -68,7 +75,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, @@ -87,164 +94,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) - } + 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")) - } - } - - 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"), + 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")) + } + } + + 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 + default: + return events.Error(errors.New("unrecognised item type")) + } + + 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"), + }, + }) root := layout.FullScreen(tableSelect) @@ -252,8 +259,7 @@ func NewModel( tableReadController: rc, tableWriteController: wc, commandController: cc, - //scriptController: scriptController, - exportController: exportController, + scriptController: scriptController, jobController: jobController, itemEdit: itemEdit, colSelector: colSelector, @@ -274,10 +280,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case controllers.SetTableItemView: cmd := m.setMainViewIndex(msg.ViewIndex) return m, cmd - case events.ResultSetUpdated: + case controllers.ResultSetUpdated: return m, tea.Batch( m.tableView.Refresh(), - events.SetStatus(msg.StatusMessage), + events.SetStatus(msg.StatusMessage()), ) case tea.KeyMsg: // TODO: use modes here @@ -300,7 +306,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.PromptForFilter + return m, m.tableReadController.Filter case key.Matches(msg, m.keyMap.FetchNextPage): return m, m.tableReadController.NextPage case key.Matches(msg, m.keyMap.ViewBack): @@ -316,10 +322,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): @@ -342,6 +348,12 @@ 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(), @@ -385,11 +397,3 @@ func (m *Model) promptToQuit() tea.Msg { return nil }) } - -func (m *Model) SelectedItemIndex() int { - return m.tableView.SelectedItemIndex() -} - -func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg { - return m.tableView.SetSelectedItemIndex(newIdx) -} diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index 163de6f..cf61d72 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -208,27 +208,6 @@ func (m *Model) SelectedItemIndex() int { return selectedItem.itemIndex } -func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg { - cursor := m.table.Cursor() - switch { - case newIdx <= 0: - m.table.GoTop() - case newIdx >= len(m.rows)-1: - m.table.GoBottom() - case newIdx < cursor: - delta := cursor - newIdx - for d := 0; d < delta; d++ { - m.table.GoUp() - } - case newIdx > cursor: - delta := newIdx - cursor - for d := 0; d < delta; d++ { - m.table.GoDown() - } - } - return m.postSelectedItemChanged() -} - func (m *Model) selectedItem() (itemTableRow, bool) { resultSet := m.resultSet diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go index 688ea25..68c9065 100644 --- a/test/testdynamo/client.go +++ b/test/testdynamo/client.go @@ -2,7 +2,6 @@ package testdynamo import ( "context" - "os" "testing" "github.com/aws/aws-sdk-go-v2/aws" @@ -29,13 +28,8 @@ func SetupTestTable(t *testing.T, testData []TestData) *dynamodb.Client { config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", ""))) assert.NoError(t, err) - testDynamoURL, ok := os.LookupEnv("TEST_DYNAMO_URL") - if !ok { - testDynamoURL = "http://localhost:4566" - } - dynamoClient := dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL(testDynamoURL))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:4566"))) for _, table := range testData { tableInput := &dynamodb.CreateTableInput{