Item annotations and async methods #4

Merged
lmika merged 4 commits from feature/item-annotations into main 2025-11-04 03:30:44 +00:00
12 changed files with 270 additions and 32 deletions
Showing only changes of commit 08a3c162a2 - Show all commits

6
go.mod
View file

@ -25,7 +25,7 @@ require (
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
golang.design/x/clipboard v0.6.2 golang.design/x/clipboard v0.6.2
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a
ucl.lmika.dev v0.1.2 ucl.lmika.dev v0.1.2
@ -50,9 +50,12 @@ require (
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-co-op/gocron/v2 v2.17.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@ -63,6 +66,7 @@ require (
github.com/muesli/termenv v0.13.0 // indirect github.com/muesli/termenv v0.13.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect github.com/rivo/uniseg v0.4.2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect

10
go.sum
View file

@ -75,6 +75,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
@ -84,12 +86,16 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o= github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o=
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -146,6 +152,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@ -154,6 +162,8 @@ github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=

View file

@ -0,0 +1,96 @@
package cmdpacks
import (
"context"
"time"
"github.com/go-co-op/gocron/v2"
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
"ucl.lmika.dev/ucl"
)
type asyncModule struct {
tableService *tables.Service
state *controllers.State
}
func (m asyncModule) asyncDo(ctx context.Context, args ucl.CallArgs) (any, error) {
var block ucl.Invokable
if err := args.Bind(&block); err != nil {
return nil, err
}
return nil, commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
_, err := block.Invoke(ctx)
return err
})
}
func (m asyncModule) asyncIn(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
duration int
block ucl.Invokable
)
if err := args.Bind(&duration, &block); err != nil {
return nil, err
}
_, err := commandctrl.CronScheduler(ctx).NewJob(
gocron.OneTimeJob(
gocron.OneTimeJobStartDateTime(time.Now().Add(time.Duration(duration)*time.Second)),
),
gocron.NewTask(func(ctx context.Context) {
commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
_, err := block.Invoke(ctx)
return err
})
}),
gocron.WithContext(ctx),
)
return nil, err
}
func (m asyncModule) asyncQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
var (
block ucl.Invokable
)
args, q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService, 1)
if err != nil {
return nil, err
}
if err := args.Bind(&block); err != nil {
return nil, err
}
return nil, commandctrl.ScheduleAuxTask(ctx, "query: "+q.String(), func(ctx context.Context) error {
newResultSet, err := m.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil)
if err != nil {
return err
}
return commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
_, err := block.Invoke(ctx, newResultSetProxy(newResultSet))
return err
})
})
}
func moduleAsync(tableService *tables.Service, state *controllers.State) ucl.Module {
m := asyncModule{
state: state,
tableService: tableService,
}
return ucl.Module{
Name: "async",
Builtins: map[string]ucl.BuiltinHandler{
"do": m.asyncDo,
"in": m.asyncIn,
"query": m.asyncQuery,
},
}
}

View file

@ -2,6 +2,7 @@ package cmdpacks
import ( import (
"context" "context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )

View file

@ -2,6 +2,8 @@ package cmdpacks
import ( import (
"context" "context"
"time"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" "lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
@ -9,7 +11,6 @@ import (
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
"time"
"ucl.lmika.dev/repl" "ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -71,21 +72,22 @@ func parseQuery(
args ucl.CallArgs, args ucl.CallArgs,
currentRS *models.ResultSet, currentRS *models.ResultSet,
tablesService *tables.Service, tablesService *tables.Service,
) (*queryexpr.QueryExpr, *models.TableInfo, error) { extraArgs int,
) (ucl.CallArgs, *queryexpr.QueryExpr, *models.TableInfo, error) {
var expr string var expr string
if err := args.Bind(&expr); err != nil { if err := args.Bind(&expr); err != nil {
return nil, nil, err return args, nil, nil, err
} }
q, err := queryexpr.Parse(expr) q, err := queryexpr.Parse(expr)
if err != nil { if err != nil {
return nil, nil, err return args, nil, nil, err
} }
if args.NArgs() > 0 { if args.NArgs() > extraArgs {
var queryArgs ucl.Hashable var queryArgs ucl.Hashable
if err := args.Bind(&queryArgs); err != nil { if err := args.Bind(&queryArgs); err != nil {
return nil, nil, err return args, nil, nil, err
} }
queryNames := map[string]string{} queryNames := map[string]string{}
@ -97,12 +99,15 @@ func parseQuery(
queryNames[k] = v.String() queryNames[k] = v.String()
switch v.(type) { switch t := v.(type) {
case ucl.StringObject: case ucl.StringObject:
queryValues[k] = &types.AttributeValueMemberS{Value: v.String()} queryValues[k] = &types.AttributeValueMemberS{Value: v.String()}
case ucl.IntObject: case ucl.IntObject:
queryValues[k] = &types.AttributeValueMemberN{Value: v.String()} queryValues[k] = &types.AttributeValueMemberN{Value: v.String()}
// TODO: other types case ucl.BoolObject:
queryValues[k] = &types.AttributeValueMemberBOOL{Value: t.Truthy()}
case attributeValueProxy:
queryValues[k] = t.value
} }
return nil return nil
}) })
@ -114,24 +119,24 @@ func parseQuery(
if args.HasSwitch("table") { if args.HasSwitch("table") {
var tblName string var tblName string
if err := args.BindSwitch("table", &tblName); err != nil { if err := args.BindSwitch("table", &tblName); err != nil {
return nil, nil, err return args, nil, nil, err
} }
tableInfo, err = tablesService.Describe(ctx, tblName) tableInfo, err = tablesService.Describe(ctx, tblName)
if err != nil { if err != nil {
return nil, nil, err return args, nil, nil, err
} }
} else if currentRS != nil && currentRS.TableInfo != nil { } else if currentRS != nil && currentRS.TableInfo != nil {
tableInfo = currentRS.TableInfo tableInfo = currentRS.TableInfo
} else { } else {
return nil, nil, errors.New("no table specified") return args, nil, nil, errors.New("no table specified")
} }
return q, tableInfo, nil return args, q, tableInfo, nil
} }
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) { func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService) _, q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -214,6 +214,14 @@ func TestModRS_First(t *testing.T) {
rs = rs:query 'pk="zzz"' -table service-test-data rs = rs:query 'pk="zzz"' -table service-test-data
assert (eq $rs.First ()) "expected First to be nil" assert (eq $rs.First ()) "expected First to be nil"
`, `,
}, {
descr: "returns the first item using placeholders",
cmd: `
rs = rs:query 'pk=$v and sk=$u' [v:"abc" u:"222"] -table service-test-data
assert (eq $rs.First.pk "abc") "expected First.pk == abc"
assert (eq $rs.First.sk "222") "expected First.sk == 222"
assert (eq $rs.First.beta 1231) "expected First.beta == 1231"
`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -175,7 +175,7 @@ func (m *uiModule) uiBind(ctx context.Context, args ucl.CallArgs) (any, error) {
} }
func (m *uiModule) uiQuery(ctx context.Context, args ucl.CallArgs) (any, error) { func (m *uiModule) uiQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService) _, q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -210,6 +210,7 @@ func (m *uiModule) uiSetItemAnnotator(ctx context.Context, args ucl.CallArgs) (a
} }
return fmt.Sprint(v) return fmt.Sprint(v)
})) }))
commandctrl.QueueRefresh(ctx)
return nil, nil return nil, nil
} }

View file

@ -404,6 +404,7 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption {
ucl.WithModule(modulePB(sc.PBProvider)), ucl.WithModule(modulePB(sc.PBProvider)),
ucl.WithModule(moduleOpt(sc.SettingsController)), ucl.WithModule(moduleOpt(sc.SettingsController)),
ucl.WithModule(moduleAttrValue()), ucl.WithModule(moduleAttrValue()),
ucl.WithModule(moduleAsync(sc.TableService, sc.State)),
} }
} }
@ -453,4 +454,30 @@ ui:bind "view.toggle-marked-items" "M" {
mark all mark all
} }
} }
proc _prep_officeCount {
openedOffices = 0
closedOffices = 0
async:query 'officeOpened=$v' [v:(av:true)] { |rs|
openedOffices = len $rs
async:query 'officeOpened=$u' [u:(av:false)] { |rs|
closedOffices = len $rs
ui:set-item-annotator { |rs item path|
if (eq $path.(-1) "officeOpened") {
if $item.officeOpened {
"Count = ${openedOffices}"
} else {
"Count = ${closedOffices}"
}
} else {
""
}
}
} -table business-addresses
} -table business-addresses
}
_prep_officeCount
` `

View file

@ -4,12 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/pkg/errors"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/go-co-op/gocron/v2"
"github.com/pkg/errors"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
"ucl.lmika.dev/ucl/builtins" "ucl.lmika.dev/ucl/builtins"
@ -17,12 +19,22 @@ import (
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" "lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
) )
const commandsCategory = "commands" const (
commandsCategory = "commands"
pendingTaskBuffer = 50
pendingAuxTaskBuffer = 50
auxWorkers = 4
)
type cmdMessage struct { type cmdMessage struct {
cmd string cmd string
} }
type pendingTask struct {
descr string
task func(ctx context.Context) error
}
type CommandController struct { type CommandController struct {
uclInst *ucl.Inst uclInst *ucl.Inst
historyProvider IterProvider historyProvider IterProvider
@ -30,19 +42,29 @@ type CommandController struct {
lookupExtensions []CommandLookupExtension lookupExtensions []CommandLookupExtension
completionProvider CommandCompletionProvider completionProvider CommandCompletionProvider
uiStateProvider UIStateProvider uiStateProvider UIStateProvider
cronScheduler gocron.Scheduler
cmdChan chan cmdMessage cmdChan chan cmdMessage
pendingTaskChan chan pendingTask
pendingAuxTaskChan chan pendingTask
msgChan chan tea.Msg msgChan chan tea.Msg
interactive bool interactive bool
} }
func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*CommandController, error) { func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*CommandController, error) {
sched, err := gocron.NewScheduler()
if err != nil {
return nil, err
}
cc := &CommandController{ cc := &CommandController{
historyProvider: historyProvider, historyProvider: historyProvider,
commandList: nil, commandList: nil,
lookupExtensions: nil, lookupExtensions: nil,
cmdChan: make(chan cmdMessage), cronScheduler: sched,
msgChan: make(chan tea.Msg), cmdChan: make(chan cmdMessage),
interactive: true, pendingTaskChan: make(chan pendingTask, pendingTaskBuffer),
pendingAuxTaskChan: make(chan pendingTask, pendingAuxTaskBuffer),
msgChan: make(chan tea.Msg),
interactive: true,
} }
options := []ucl.InstOption{ options := []ucl.InstOption{
@ -75,6 +97,8 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*C
} }
go cc.cmdLooper() go cc.cmdLooper()
go cc.auxCmdLooper()
sched.Start()
return cc, nil return cc, nil
} }
@ -172,12 +196,13 @@ func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea
} }
func (c *CommandController) cmdLooper() { func (c *CommandController) cmdLooper() {
execCtx := execContext{ctrl: c} ctx := context.Background()
ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx)
for { for {
select { select {
case cmdChan := <-c.cmdChan: case cmdChan := <-c.cmdChan:
execCtx := execContext{ctrl: c}
ctx := context.WithValue(ctx, commandCtlKey, &execCtx)
res, err := c.ExecuteAndWait(ctx, cmdChan.cmd) res, err := c.ExecuteAndWait(ctx, cmdChan.cmd)
if err != nil { if err != nil {
c.postMessage(events.Error(err)) c.postMessage(events.Error(err))
@ -187,6 +212,16 @@ func (c *CommandController) cmdLooper() {
if execCtx.requestRefresh { if execCtx.requestRefresh {
c.postMessage(events.ResultSetUpdated{}) c.postMessage(events.ResultSetUpdated{})
} }
case task := <-c.pendingTaskChan:
execCtx := execContext{ctrl: c}
ctx := context.WithValue(ctx, commandCtlKey, &execCtx)
if err := task.task(ctx); err != nil {
c.postMessage(events.Error(err))
}
if execCtx.requestRefresh {
c.postMessage(events.ResultSetUpdated{})
}
} }
} }
} }
@ -305,15 +340,13 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc
} }
func (c *CommandController) printLine(s string) { func (c *CommandController) printLine(s string) {
log.Println(s)
if c.msgChan == nil || !c.interactive { if c.msgChan == nil || !c.interactive {
log.Println(s)
return return
} }
select { select {
case c.msgChan <- events.StatusMsg(s): case c.msgChan <- events.StatusMsg(s):
default:
log.Println(s)
} }
} }
@ -325,6 +358,21 @@ func (c *CommandController) postMessage(msg tea.Msg) {
c.msgChan <- msg c.msgChan <- msg
} }
func (c *CommandController) auxCmdLooper() {
ctx := context.WithValue(context.Background(), commandCtlKey, &execContext{ctrl: c})
for i := 0; i < auxWorkers; i++ {
go func() {
for auxTask := range c.pendingAuxTaskChan {
log.Printf("running aux task: %v", auxTask.descr)
if err := auxTask.task(ctx); err != nil {
log.Printf("aux task error: %v", err)
}
}
}()
}
}
type teaMsgWrapper struct { type teaMsgWrapper struct {
msg tea.Msg msg tea.Msg
} }

View file

@ -2,7 +2,10 @@ package commandctrl
import ( import (
"context" "context"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/go-co-op/gocron/v2"
"github.com/pkg/errors"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -57,6 +60,40 @@ func QueueRefresh(ctx context.Context) {
cmdCtl.requestRefresh = true cmdCtl.requestRefresh = true
} }
func CronScheduler(ctx context.Context) gocron.Scheduler {
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return nil
}
return cmdCtl.ctrl.cronScheduler
}
func ScheduleTask(ctx context.Context, task func(ctx context.Context) error) error {
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return errors.New("no command controller")
}
select {
case cmdCtl.ctrl.pendingTaskChan <- pendingTask{task: task}:
return nil
default:
return errors.New("task queue is full")
}
}
func ScheduleAuxTask(ctx context.Context, descr string, task func(ctx context.Context) error) error {
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
if !ok {
return errors.New("no command controller")
}
select {
case cmdCtl.ctrl.pendingAuxTaskChan <- pendingTask{descr: descr, task: task}:
return nil
default:
return errors.New("aux task queue is full")
}
}
type Invoker interface { type Invoker interface {
Invoke(invokable ucl.Invokable, args []any) tea.Msg Invoke(invokable ucl.Invokable, args []any) tea.Msg
Inst() *ucl.Inst Inst() *ucl.Inst

View file

@ -1,6 +1,8 @@
package dynamoitemview package dynamoitemview
import ( import (
"strings"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@ -9,7 +11,6 @@ import (
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
"strings"
) )
type Model struct { type Model struct {

View file

@ -20,7 +20,7 @@ type ItemViewStyle struct {
var DefaultStyles = Styles{ var DefaultStyles = Styles{
ItemView: ItemViewStyle{ ItemView: ItemViewStyle{
FieldType: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"}), FieldType: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"}),
MetaInfo: lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")), MetaInfo: lipgloss.NewStyle().Foreground(lipgloss.Color("#707070")),
}, },
Frames: frame.Style{ Frames: frame.Style{
ActiveTitle: lipgloss.NewStyle(). ActiveTitle: lipgloss.NewStyle().