Compare commits
10 commits
b2ddc62555
...
cae7509a76
Author | SHA1 | Date | |
---|---|---|---|
|
cae7509a76 | ||
|
7ae99b009b | ||
|
5088009672 | ||
|
40f8dd76e2 | ||
|
18ffe85a56 | ||
|
6bf721873b | ||
|
cb908ec4eb | ||
|
17381f3d0b | ||
|
94b58e2168 | ||
|
29d425c77e |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
debug.log
|
debug.log
|
||||||
|
.DS_store
|
||||||
|
.idea
|
||||||
|
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||||
|
@ -44,6 +46,7 @@ func main() {
|
||||||
var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
|
var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
|
||||||
var flagWorkspace = flag.String("w", "", "workspace file")
|
var flagWorkspace = flag.String("w", "", "workspace file")
|
||||||
var flagQuery = flag.String("q", "", "run query")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -127,7 +130,7 @@ func main() {
|
||||||
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
||||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
keyBindings := keybindings.Default()
|
keyBindings := keybindings.Default()
|
||||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
|
//scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
|
||||||
|
|
||||||
if *flagQuery != "" {
|
if *flagQuery != "" {
|
||||||
if *flagTable == "" {
|
if *flagTable == "" {
|
||||||
|
@ -157,10 +160,20 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBindingService := keybindings_service.NewService(keyBindings)
|
keyBindingService := keybindings_service.NewService(keyBindings)
|
||||||
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
|
keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil)
|
||||||
|
|
||||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
stdCommands := cmdpacks.NewStandardCommands(
|
||||||
commandController.AddCommandLookupExtension(scriptController)
|
tableService,
|
||||||
|
state,
|
||||||
|
tableReadController,
|
||||||
|
tableWriteController,
|
||||||
|
exportController,
|
||||||
|
keyBindingController,
|
||||||
|
pasteboardProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
commandController := commandctrl.NewCommandController(inputHistoryService, stdCommands)
|
||||||
|
//commandController.AddCommandLookupExtension(scriptController)
|
||||||
commandController.SetCommandCompletionProvider(columnsController)
|
commandController.SetCommandCompletionProvider(columnsController)
|
||||||
|
|
||||||
model := ui.NewModel(
|
model := ui.NewModel(
|
||||||
|
@ -172,12 +185,13 @@ func main() {
|
||||||
jobsController,
|
jobsController,
|
||||||
itemRendererService,
|
itemRendererService,
|
||||||
commandController,
|
commandController,
|
||||||
scriptController,
|
//scriptController,
|
||||||
eventBus,
|
eventBus,
|
||||||
keyBindingController,
|
keyBindingController,
|
||||||
pasteboardProvider,
|
pasteboardProvider,
|
||||||
keyBindings,
|
keyBindings,
|
||||||
)
|
)
|
||||||
|
commandController.SetUIStateProvider(&model)
|
||||||
|
|
||||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||||
osstyle.DetectCurrentScheme()
|
osstyle.DetectCurrentScheme()
|
||||||
|
@ -185,9 +199,14 @@ func main() {
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
|
||||||
jobsController.SetMessageSender(p.Send)
|
jobsController.SetMessageSender(p.Send)
|
||||||
scriptController.Init()
|
//scriptController.Init()
|
||||||
scriptController.SetMessageSender(p.Send)
|
//scriptController.SetMessageSender(p.Send)
|
||||||
commandController.SetMessageSender(p.Send)
|
|
||||||
|
if err := commandController.LoadExtensions(context.Background(), strings.Split(*flagExtDir, string(os.PathListSeparator))); err != nil {
|
||||||
|
fmt.Printf("Unable to load extensions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go commandController.StartMessageSender(p.Send)
|
||||||
|
|
||||||
log.Println("launching")
|
log.Println("launching")
|
||||||
if err := p.Start(); err != nil {
|
if err := p.Start(); err != nil {
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -1,8 +1,8 @@
|
||||||
module github.com/lmika/dynamo-browse
|
module github.com/lmika/dynamo-browse
|
||||||
|
|
||||||
go 1.22
|
go 1.24
|
||||||
|
|
||||||
toolchain go1.22.0
|
toolchain go1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/participle/v2 v2.1.1
|
github.com/alecthomas/participle/v2 v2.1.1
|
||||||
|
@ -117,5 +117,5 @@ require (
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
ucl.lmika.dev v0.0.0-20240501110514-25594c80d273 // indirect
|
ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e // indirect
|
||||||
)
|
)
|
||||||
|
|
32
go.sum
32
go.sum
|
@ -5,6 +5,8 @@ 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 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ=
|
||||||
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
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/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 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo=
|
||||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
|
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
|
||||||
github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
|
github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
|
||||||
|
@ -430,3 +432,33 @@ 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-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 h1:+JpKw02VTAcOjJw7Q6juun/9hk9ypNSdTRlf+E4M5Nw=
|
||||||
ucl.lmika.dev v0.0.0-20240501110514-25594c80d273/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4=
|
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=
|
||||||
|
|
46
internal/common/ui/commandctrl/cmdpacks/modpb.go
Normal file
46
internal/common/ui/commandctrl/cmdpacks/modpb.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package cmdpacks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
|
||||||
|
"ucl.lmika.dev/ucl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pbModule struct {
|
||||||
|
pasteboardProvider *pasteboardprovider.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m pbModule) pbGet(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
s, ok := m.pasteboardProvider.ReadText()
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m pbModule) pbPut(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
var s string
|
||||||
|
if err := args.Bind(&s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := m.pasteboardProvider.WriteText([]byte(s)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modulePB(
|
||||||
|
pasteboardProvider *pasteboardprovider.Provider,
|
||||||
|
) ucl.Module {
|
||||||
|
m := &pbModule{
|
||||||
|
pasteboardProvider: pasteboardProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ucl.Module{
|
||||||
|
Name: "pb",
|
||||||
|
Builtins: map[string]ucl.BuiltinHandler{
|
||||||
|
"get": m.pbGet,
|
||||||
|
"put": m.pbPut,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
308
internal/common/ui/commandctrl/cmdpacks/modrs.go
Normal file
308
internal/common/ui/commandctrl/cmdpacks/modrs.go
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
154
internal/common/ui/commandctrl/cmdpacks/modrs_test.go
Normal file
154
internal/common/ui/commandctrl/cmdpacks/modrs_test.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
210
internal/common/ui/commandctrl/cmdpacks/modui.go
Normal file
210
internal/common/ui/commandctrl/cmdpacks/modui.go
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
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
|
||||||
|
}
|
216
internal/common/ui/commandctrl/cmdpacks/proxy.go
Normal file
216
internal/common/ui/commandctrl/cmdpacks/proxy.go
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
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
|
||||||
|
}
|
62
internal/common/ui/commandctrl/cmdpacks/pvars.go
Normal file
62
internal/common/ui/commandctrl/cmdpacks/pvars.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
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
|
||||||
|
}
|
421
internal/common/ui/commandctrl/cmdpacks/stdcmds.go
Normal file
421
internal/common/ui/commandctrl/cmdpacks/stdcmds.go
Normal file
|
@ -0,0 +1,421 @@
|
||||||
|
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})
|
||||||
|
}
|
211
internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go
Normal file
211
internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package commandctrl
|
package commandctrl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"log"
|
"log"
|
||||||
|
@ -11,6 +11,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"ucl.lmika.dev/ucl"
|
"ucl.lmika.dev/ucl"
|
||||||
|
"ucl.lmika.dev/ucl/builtins"
|
||||||
|
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||||
"github.com/lmika/shellwords"
|
"github.com/lmika/shellwords"
|
||||||
|
@ -18,25 +19,49 @@ import (
|
||||||
|
|
||||||
const commandsCategory = "commands"
|
const commandsCategory = "commands"
|
||||||
|
|
||||||
|
type cmdMessage struct {
|
||||||
|
cmd string
|
||||||
|
}
|
||||||
|
|
||||||
type CommandController struct {
|
type CommandController struct {
|
||||||
uclInst *ucl.Inst
|
uclInst *ucl.Inst
|
||||||
historyProvider IterProvider
|
historyProvider IterProvider
|
||||||
commandList *CommandList
|
commandList *CommandList
|
||||||
msgSender func(tea.Msg)
|
|
||||||
lookupExtensions []CommandLookupExtension
|
lookupExtensions []CommandLookupExtension
|
||||||
completionProvider CommandCompletionProvider
|
completionProvider CommandCompletionProvider
|
||||||
|
uiStateProvider UIStateProvider
|
||||||
|
cmdChan chan cmdMessage
|
||||||
|
msgChan chan tea.Msg
|
||||||
|
interactive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandController(historyProvider IterProvider) *CommandController {
|
func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *CommandController {
|
||||||
cc := &CommandController{
|
cc := &CommandController{
|
||||||
historyProvider: historyProvider,
|
historyProvider: historyProvider,
|
||||||
commandList: nil,
|
commandList: nil,
|
||||||
lookupExtensions: nil,
|
lookupExtensions: nil,
|
||||||
|
cmdChan: make(chan cmdMessage),
|
||||||
|
msgChan: make(chan tea.Msg),
|
||||||
|
interactive: true,
|
||||||
}
|
}
|
||||||
cc.uclInst = ucl.New(
|
|
||||||
|
options := []ucl.InstOption{
|
||||||
ucl.WithOut(ucl.LineHandler(cc.printLine)),
|
ucl.WithOut(ucl.LineHandler(cc.printLine)),
|
||||||
ucl.WithMissingBuiltinHandler(cc.cmdInvoker),
|
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()
|
||||||
|
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +70,14 @@ func (c *CommandController) AddCommands(ctx *CommandList) {
|
||||||
c.commandList = ctx
|
c.commandList = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) SetMessageSender(msg func(tea.Msg)) {
|
func (c *CommandController) StartMessageSender(msgSender func(tea.Msg)) {
|
||||||
c.msgSender = msg
|
for msg := range c.msgChan {
|
||||||
|
msgSender(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandController) SetUIStateProvider(provider UIStateProvider) {
|
||||||
|
c.uiStateProvider = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) {
|
func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) {
|
||||||
|
@ -95,17 +126,57 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := c.uclInst.Eval(context.Background(), commandInput)
|
select {
|
||||||
if err != nil {
|
case c.cmdChan <- cmdMessage{cmd: input}:
|
||||||
return events.Error(err)
|
// good
|
||||||
|
default:
|
||||||
|
return events.Error(errors.New("command currently running"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if teaMsg, ok := res.(teaMsgWrapper); ok {
|
|
||||||
return teaMsg.msg
|
|
||||||
}
|
|
||||||
return nil
|
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)
|
||||||
|
if err != nil {
|
||||||
|
msg = events.Error(err)
|
||||||
|
} else if res != nil {
|
||||||
|
msg = events.StatusMsg(fmt.Sprint(res))
|
||||||
|
}
|
||||||
|
if execCtx.requestRefresh {
|
||||||
|
c.postMessage(events.ResultSetUpdated{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandController) cmdLooper() {
|
||||||
|
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{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CommandController) Alias(commandName string) Command {
|
func (c *CommandController) Alias(commandName string) Command {
|
||||||
return func(ctx ExecContext, args ucl.CallArgs) tea.Msg {
|
return func(ctx ExecContext, args ucl.CallArgs) tea.Msg {
|
||||||
command := c.lookupCommand(commandName)
|
command := c.lookupCommand(commandName)
|
||||||
|
@ -132,41 +203,78 @@ func (c *CommandController) lookupCommand(name string) Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) ExecuteFile(filename string) error {
|
func (c *CommandController) LoadExtensions(ctx context.Context, baseDirs []string) error {
|
||||||
baseFilename := filepath.Base(filename)
|
log.Printf("loading extensions: %v", baseDirs)
|
||||||
|
for _, baseDir := range baseDirs {
|
||||||
|
baseDir = os.ExpandEnv(baseDir)
|
||||||
|
descendIntoSubDirs := !strings.HasSuffix(baseDir, ".")
|
||||||
|
|
||||||
if rcFile, err := os.ReadFile(filename); err == nil {
|
if stat, err := os.Stat(baseDir); err != nil {
|
||||||
if err := c.executeFile(rcFile, baseFilename); err != nil {
|
if os.IsNotExist(err) {
|
||||||
return errors.Wrapf(err, "error executing %v", filename)
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
} else if !stat.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("walking %v", baseDir)
|
||||||
|
if err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
if !descendIntoSubDirs && path != baseDir {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(info.Name(), ".ucl") {
|
||||||
|
if err := c.ExecuteFile(ctx, path); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
log.Printf("loaded %v\n", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return errors.Wrapf(err, "error loading %v", filename)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) executeFile(file []byte, filename string) error {
|
func (c *CommandController) ExecuteFile(ctx context.Context, filename string) error {
|
||||||
scnr := bufio.NewScanner(bytes.NewReader(file))
|
oldInteractive := c.interactive
|
||||||
|
c.interactive = false
|
||||||
|
defer func() {
|
||||||
|
c.interactive = oldInteractive
|
||||||
|
}()
|
||||||
|
|
||||||
lineNo := 0
|
baseFilename := filepath.Base(filename)
|
||||||
for scnr.Scan() {
|
|
||||||
lineNo++
|
execCtx := execContext{ctrl: c}
|
||||||
line := strings.TrimSpace(scnr.Text())
|
ctx = context.WithValue(context.Background(), commandCtlKey, &execCtx)
|
||||||
if line == "" {
|
|
||||||
continue
|
if rcFile, err := os.ReadFile(filename); err == nil {
|
||||||
} else if line[0] == '#' {
|
if err := c.executeFile(ctx, rcFile); err != nil {
|
||||||
continue
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := c.execute(ExecContext{FromFile: true}, line)
|
return nil
|
||||||
switch m := msg.(type) {
|
}
|
||||||
case events.ErrorMsg:
|
|
||||||
log.Printf("%v:%v: error - %v", filename, lineNo, m.Error())
|
func (c *CommandController) Inst() *ucl.Inst {
|
||||||
case events.StatusMsg:
|
return c.uclInst
|
||||||
log.Printf("%v:%v: %v", filename, lineNo, string(m))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return scnr.Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) {
|
func (c *CommandController) cmdInvoker(ctx context.Context, name string, args ucl.CallArgs) (any, error) {
|
||||||
|
@ -183,9 +291,24 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandController) printLine(s string) {
|
func (c *CommandController) printLine(s string) {
|
||||||
if c.msgSender != nil {
|
if c.msgChan == nil || !c.interactive {
|
||||||
c.msgSender(events.StatusMsg(s))
|
log.Println(s)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
type teaMsgWrapper struct {
|
||||||
|
|
63
internal/common/ui/commandctrl/ctx.go
Normal file
63
internal/common/ui/commandctrl/ctx.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -2,9 +2,15 @@ package commandctrl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IterProvider interface {
|
type IterProvider interface {
|
||||||
Iter(ctx context.Context, category string) services.HistoryProvider
|
Iter(ctx context.Context, category string) services.HistoryProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UIStateProvider interface {
|
||||||
|
SelectedItemIndex() int
|
||||||
|
SetSelectedItemIndex(newIdx int) tea.Msg
|
||||||
|
}
|
||||||
|
|
8
internal/common/ui/commandctrl/packs.go
Normal file
8
internal/common/ui/commandctrl/packs.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package commandctrl
|
||||||
|
|
||||||
|
import "ucl.lmika.dev/ucl"
|
||||||
|
|
||||||
|
type CommandPack interface {
|
||||||
|
InstOptions() []ucl.InstOption
|
||||||
|
ConfigureUCL(ucl *ucl.Inst)
|
||||||
|
}
|
5
internal/common/ui/events/resultset.go
Normal file
5
internal/common/ui/events/resultset.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package events
|
||||||
|
|
||||||
|
type ResultSetUpdated struct {
|
||||||
|
StatusMessage string
|
||||||
|
}
|
|
@ -81,14 +81,6 @@ type PromptForTableMsg struct {
|
||||||
OnSelected func(tableName string) tea.Msg
|
OnSelected func(tableName string) tea.Msg
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultSetUpdated struct {
|
|
||||||
statusMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs ResultSetUpdated) StatusMessage() string {
|
|
||||||
return rs.statusMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShowColumnOverlay struct{}
|
type ShowColumnOverlay struct{}
|
||||||
type HideColumnOverlay struct{}
|
type HideColumnOverlay struct{}
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ func (c *ExportController) ExportCSVToClipboard() tea.Msg {
|
||||||
if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil {
|
if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return nil
|
return events.StatusMsg("Table copied to clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this really needs to be a service!
|
// TODO: this really needs to be a service!
|
||||||
|
|
|
@ -20,6 +20,10 @@ func NewKeyBindingController(service *keybindings.Service, customBindingSource C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (kb *KeyBindingController) SetCustomKeyBindingSource(customBindingSource CustomKeyBindingSource) {
|
||||||
|
kb.customBindingSource = customBindingSource
|
||||||
|
}
|
||||||
|
|
||||||
func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg {
|
func (kb *KeyBindingController) Rebind(bindingName string, newKey string, force bool) tea.Msg {
|
||||||
existingBinding := kb.findExistingBinding(newKey)
|
existingBinding := kb.findExistingBinding(newKey)
|
||||||
if existingBinding == "" {
|
if existingBinding == "" {
|
||||||
|
|
|
@ -29,6 +29,14 @@ func (s *State) Filter() string {
|
||||||
return s.filter
|
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)) {
|
func (s *State) withResultSet(rs func(*models.ResultSet)) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
|
@ -182,6 +182,10 @@ func (c *TableReadController) PromptForQuery() tea.Msg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TableReadController) RunQuery(q *queryexpr.QueryExpr, table *models.TableInfo) tea.Msg {
|
||||||
|
return c.runQuery(table, q, "", true, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TableReadController) runQuery(
|
func (c *TableReadController) runQuery(
|
||||||
tableInfo *models.TableInfo,
|
tableInfo *models.TableInfo,
|
||||||
query *queryexpr.QueryExpr,
|
query *queryexpr.QueryExpr,
|
||||||
|
@ -291,6 +295,12 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
|
||||||
return c.state.buildNewResultSetMessage("")
|
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 {
|
func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
|
||||||
var (
|
var (
|
||||||
whereExpr *queryexpr.QueryExpr
|
whereExpr *queryexpr.QueryExpr
|
||||||
|
@ -333,14 +343,20 @@ func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) Filter() tea.Msg {
|
func (c *TableReadController) PromptForFilter() tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: "filter: ",
|
Prompt: "filter: ",
|
||||||
History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
|
History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
|
return c.Filter(value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TableReadController) Filter(value string) tea.Msg {
|
||||||
resultSet := c.state.ResultSet()
|
resultSet := c.state.ResultSet()
|
||||||
if resultSet == nil {
|
if resultSet == nil {
|
||||||
return events.StatusMsg("Result-set is nil")
|
return events.StatusMsg("Result-set is nil")
|
||||||
|
@ -350,8 +366,6 @@ func (c *TableReadController) Filter() tea.Msg {
|
||||||
newResultSet := c.tableService.Filter(resultSet, value)
|
newResultSet := c.tableService.Filter(resultSet, value)
|
||||||
return newResultSet, nil
|
return newResultSet, nil
|
||||||
}).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit()
|
}).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit()
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) handleResultSetFromJobResult(
|
func (c *TableReadController) handleResultSetFromJobResult(
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (twc *TableWriteController) ToggleMark(idx int) tea.Msg {
|
||||||
resultSet.SetMark(idx, !resultSet.Marked(idx))
|
resultSet.SetMark(idx, !resultSet.Marked(idx))
|
||||||
})
|
})
|
||||||
|
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) NewItem() tea.Msg {
|
func (twc *TableWriteController) NewItem() tea.Msg {
|
||||||
|
@ -148,7 +148,7 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.Q
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,7 +205,7 @@ func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryEx
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr *queryexpr.QueryExpr
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,7 +255,7 @@ func (twc *TableWriteController) setNullValue(idx int, attr *queryexpr.QueryExpr
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
|
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.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) PutItems() tea.Msg {
|
func (twc *TableWriteController) PutItems() tea.Msg {
|
||||||
|
@ -351,8 +351,8 @@ func (twc *TableWriteController) PutItems() tea.Msg {
|
||||||
}
|
}
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
||||||
return ResultSetUpdated{
|
return events.ResultSetUpdated{
|
||||||
statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
|
StatusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
|
||||||
}
|
}
|
||||||
}).Submit()
|
}).Submit()
|
||||||
},
|
},
|
||||||
|
@ -379,7 +379,7 @@ func (twc *TableWriteController) TouchItem(idx int) tea.Msg {
|
||||||
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
return ResultSetUpdated{}
|
return events.ResultSetUpdated{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,3 +151,37 @@ func (rs *ResultSet) Sort(criteria SortCriteria) {
|
||||||
rs.sortCriteria = criteria
|
rs.sortCriteria = criteria
|
||||||
Sort(rs.items, 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
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type exprValue interface {
|
type exprValue interface {
|
||||||
|
@ -62,6 +63,14 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error)
|
||||||
case *types.AttributeValueMemberS:
|
case *types.AttributeValueMemberS:
|
||||||
return stringExprValue(xVal.Value), nil
|
return stringExprValue(xVal.Value), nil
|
||||||
case *types.AttributeValueMemberN:
|
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)
|
xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -139,6 +148,32 @@ func (s int64ExprValue) typeName() string {
|
||||||
return "N"
|
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 {
|
type bigNumExprValue struct {
|
||||||
num *big.Float
|
num *big.Float
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"ucl.lmika.dev/ucl"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
"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/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"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
||||||
|
@ -26,7 +21,7 @@ import (
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
"github.com/pkg/errors"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -37,8 +32,6 @@ const (
|
||||||
ViewModeTableOnly = 4
|
ViewModeTableOnly = 4
|
||||||
|
|
||||||
ViewModeCount = 5
|
ViewModeCount = 5
|
||||||
|
|
||||||
initRCFilename = "$HOME/.config/audax/dynamo-browse/init.rc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
@ -47,7 +40,7 @@ type Model struct {
|
||||||
settingsController *controllers.SettingsController
|
settingsController *controllers.SettingsController
|
||||||
exportController *controllers.ExportController
|
exportController *controllers.ExportController
|
||||||
commandController *commandctrl.CommandController
|
commandController *commandctrl.CommandController
|
||||||
scriptController *controllers.ScriptController
|
//scriptController *controllers.ScriptController
|
||||||
jobController *controllers.JobsController
|
jobController *controllers.JobsController
|
||||||
colSelector *colselector.Model
|
colSelector *colselector.Model
|
||||||
relSelector *relselector.Model
|
relSelector *relselector.Model
|
||||||
|
@ -75,7 +68,7 @@ func NewModel(
|
||||||
jobController *controllers.JobsController,
|
jobController *controllers.JobsController,
|
||||||
itemRendererService *itemrenderer.Service,
|
itemRendererService *itemrenderer.Service,
|
||||||
cc *commandctrl.CommandController,
|
cc *commandctrl.CommandController,
|
||||||
scriptController *controllers.ScriptController,
|
//scriptController *controllers.ScriptController,
|
||||||
eventBus *bus.Bus,
|
eventBus *bus.Bus,
|
||||||
keyBindingController *controllers.KeyBindingController,
|
keyBindingController *controllers.KeyBindingController,
|
||||||
pasteboardProvider services.PasteboardProvider,
|
pasteboardProvider services.PasteboardProvider,
|
||||||
|
@ -94,6 +87,7 @@ func NewModel(
|
||||||
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
||||||
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
||||||
|
|
||||||
|
/*
|
||||||
cc.AddCommands(&commandctrl.CommandList{
|
cc.AddCommands(&commandctrl.CommandList{
|
||||||
Commands: map[string]commandctrl.Command{
|
Commands: map[string]commandctrl.Command{
|
||||||
"quit": commandctrl.NoArgCommand(tea.Quit),
|
"quit": commandctrl.NoArgCommand(tea.Quit),
|
||||||
|
@ -170,8 +164,6 @@ func NewModel(
|
||||||
itemType = models.NullItemType
|
itemType = models.NullItemType
|
||||||
case args.HasSwitch("TO"):
|
case args.HasSwitch("TO"):
|
||||||
itemType = models.ExprValueItemType
|
itemType = models.ExprValueItemType
|
||||||
default:
|
|
||||||
return events.Error(errors.New("unrecognised item type"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName)
|
return wc.SetAttributeValue(dtv.SelectedItemIndex(), itemType, fieldName)
|
||||||
|
@ -196,7 +188,7 @@ func NewModel(
|
||||||
return wc.NoisyTouchItem(dtv.SelectedItemIndex())
|
return wc.NoisyTouchItem(dtv.SelectedItemIndex())
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
|
||||||
"echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
|
"echo": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
|
||||||
s := new(strings.Builder)
|
s := new(strings.Builder)
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
|
@ -204,7 +196,6 @@ func NewModel(
|
||||||
}
|
}
|
||||||
return events.StatusMsg(s.String())
|
return events.StatusMsg(s.String())
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
"set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
|
"set-opt": func(ctx commandctrl.ExecContext, args ucl.CallArgs) tea.Msg {
|
||||||
var name string
|
var name string
|
||||||
if err := args.Bind(&name); err != nil {
|
if err := args.Bind(&name); err != nil {
|
||||||
|
@ -253,13 +244,16 @@ func NewModel(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
root := layout.FullScreen(tableSelect)
|
root := layout.FullScreen(tableSelect)
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
tableReadController: rc,
|
tableReadController: rc,
|
||||||
tableWriteController: wc,
|
tableWriteController: wc,
|
||||||
commandController: cc,
|
commandController: cc,
|
||||||
scriptController: scriptController,
|
//scriptController: scriptController,
|
||||||
|
exportController: exportController,
|
||||||
jobController: jobController,
|
jobController: jobController,
|
||||||
itemEdit: itemEdit,
|
itemEdit: itemEdit,
|
||||||
colSelector: colSelector,
|
colSelector: colSelector,
|
||||||
|
@ -280,10 +274,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case controllers.SetTableItemView:
|
case controllers.SetTableItemView:
|
||||||
cmd := m.setMainViewIndex(msg.ViewIndex)
|
cmd := m.setMainViewIndex(msg.ViewIndex)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
case controllers.ResultSetUpdated:
|
case events.ResultSetUpdated:
|
||||||
return m, tea.Batch(
|
return m, tea.Batch(
|
||||||
m.tableView.Refresh(),
|
m.tableView.Refresh(),
|
||||||
events.SetStatus(msg.StatusMessage()),
|
events.SetStatus(msg.StatusMessage),
|
||||||
)
|
)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// TODO: use modes here
|
// TODO: use modes here
|
||||||
|
@ -306,7 +300,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keyMap.PromptForQuery):
|
case key.Matches(msg, m.keyMap.PromptForQuery):
|
||||||
return m, m.tableReadController.PromptForQuery
|
return m, m.tableReadController.PromptForQuery
|
||||||
case key.Matches(msg, m.keyMap.PromptForFilter):
|
case key.Matches(msg, m.keyMap.PromptForFilter):
|
||||||
return m, m.tableReadController.Filter
|
return m, m.tableReadController.PromptForFilter
|
||||||
case key.Matches(msg, m.keyMap.FetchNextPage):
|
case key.Matches(msg, m.keyMap.FetchNextPage):
|
||||||
return m, m.tableReadController.NextPage
|
return m, m.tableReadController.NextPage
|
||||||
case key.Matches(msg, m.keyMap.ViewBack):
|
case key.Matches(msg, m.keyMap.ViewBack):
|
||||||
|
@ -322,10 +316,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
// return m, nil
|
// return m, nil
|
||||||
case key.Matches(msg, m.keyMap.ShowColumnOverlay):
|
case key.Matches(msg, m.keyMap.ShowColumnOverlay):
|
||||||
return m, events.SetTeaMessage(controllers.ShowColumnOverlay{})
|
return m, events.SetTeaMessage(controllers.ShowColumnOverlay{})
|
||||||
case key.Matches(msg, m.keyMap.ShowRelItemsOverlay):
|
//case key.Matches(msg, m.keyMap.ShowRelItemsOverlay):
|
||||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
// if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||||
return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx))
|
// return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx))
|
||||||
}
|
// }
|
||||||
case key.Matches(msg, m.keyMap.PromptForCommand):
|
case key.Matches(msg, m.keyMap.PromptForCommand):
|
||||||
return m, m.commandController.Prompt
|
return m, m.commandController.Prompt
|
||||||
case key.Matches(msg, m.keyMap.PromptForTable):
|
case key.Matches(msg, m.keyMap.PromptForTable):
|
||||||
|
@ -348,12 +342,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() 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(
|
return tea.Batch(
|
||||||
m.tableReadController.Init,
|
m.tableReadController.Init,
|
||||||
m.root.Init(),
|
m.root.Init(),
|
||||||
|
@ -397,3 +385,11 @@ func (m *Model) promptToQuit() tea.Msg {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) SelectedItemIndex() int {
|
||||||
|
return m.tableView.SelectedItemIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg {
|
||||||
|
return m.tableView.SetSelectedItemIndex(newIdx)
|
||||||
|
}
|
||||||
|
|
|
@ -208,6 +208,27 @@ func (m *Model) SelectedItemIndex() int {
|
||||||
return selectedItem.itemIndex
|
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) {
|
func (m *Model) selectedItem() (itemTableRow, bool) {
|
||||||
resultSet := m.resultSet
|
resultSet := m.resultSet
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package testdynamo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
@ -28,8 +29,13 @@ func SetupTestTable(t *testing.T, testData []TestData) *dynamodb.Client {
|
||||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", "")))
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("abc", "123", "")))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testDynamoURL, ok := os.LookupEnv("TEST_DYNAMO_URL")
|
||||||
|
if !ok {
|
||||||
|
testDynamoURL = "http://localhost:4566"
|
||||||
|
}
|
||||||
|
|
||||||
dynamoClient := dynamodb.NewFromConfig(cfg,
|
dynamoClient := dynamodb.NewFromConfig(cfg,
|
||||||
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:4566")))
|
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL(testDynamoURL)))
|
||||||
|
|
||||||
for _, table := range testData {
|
for _, table := range testData {
|
||||||
tableInput := &dynamodb.CreateTableInput{
|
tableInput := &dynamodb.CreateTableInput{
|
||||||
|
|
Loading…
Reference in a new issue