Item annotations and async methods (#4)
All checks were successful
ci / Build (push) Successful in 3m7s
All checks were successful
ci / Build (push) Successful in 3m7s
- New UCL method for setting up item annotations - New UCL package for running commands asynchronously Reviewed-on: #4 Co-authored-by: Leon Mika <lmika@lmika.org> Co-committed-by: Leon Mika <lmika@lmika.org>
This commit is contained in:
parent
c8b65f6b0a
commit
4fb9dd0b5b
18 changed files with 494 additions and 52 deletions
96
internal/common/ui/commandctrl/cmdpacks/modasync.go
Normal file
96
internal/common/ui/commandctrl/cmdpacks/modasync.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/pkg/errors"
|
||||
"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/queryexpr"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"time"
|
||||
"ucl.lmika.dev/repl"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
|
@ -71,21 +72,22 @@ func parseQuery(
|
|||
args ucl.CallArgs,
|
||||
currentRS *models.ResultSet,
|
||||
tablesService *tables.Service,
|
||||
) (*queryexpr.QueryExpr, *models.TableInfo, error) {
|
||||
extraArgs int,
|
||||
) (ucl.CallArgs, *queryexpr.QueryExpr, *models.TableInfo, error) {
|
||||
var expr string
|
||||
if err := args.Bind(&expr); err != nil {
|
||||
return nil, nil, err
|
||||
return args, nil, nil, err
|
||||
}
|
||||
|
||||
q, err := queryexpr.Parse(expr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return args, nil, nil, err
|
||||
}
|
||||
|
||||
if args.NArgs() > 0 {
|
||||
if args.NArgs() > extraArgs {
|
||||
var queryArgs ucl.Hashable
|
||||
if err := args.Bind(&queryArgs); err != nil {
|
||||
return nil, nil, err
|
||||
return args, nil, nil, err
|
||||
}
|
||||
|
||||
queryNames := map[string]string{}
|
||||
|
|
@ -97,12 +99,15 @@ func parseQuery(
|
|||
|
||||
queryNames[k] = v.String()
|
||||
|
||||
switch v.(type) {
|
||||
switch t := 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
|
||||
case ucl.BoolObject:
|
||||
queryValues[k] = &types.AttributeValueMemberBOOL{Value: t.Truthy()}
|
||||
case attributeValueProxy:
|
||||
queryValues[k] = t.value
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -114,24 +119,24 @@ func parseQuery(
|
|||
if args.HasSwitch("table") {
|
||||
var tblName string
|
||||
if err := args.BindSwitch("table", &tblName); err != nil {
|
||||
return nil, nil, err
|
||||
return args, nil, nil, err
|
||||
}
|
||||
|
||||
tableInfo, err = tablesService.Describe(ctx, tblName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return args, nil, nil, err
|
||||
}
|
||||
} else if currentRS != nil && currentRS.TableInfo != nil {
|
||||
tableInfo = currentRS.TableInfo
|
||||
} 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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,14 @@ func TestModRS_First(t *testing.T) {
|
|||
rs = rs:query 'pk="zzz"' -table service-test-data
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
|
@ -16,6 +19,7 @@ type uiModule struct {
|
|||
state *controllers.State
|
||||
ckb *customKeyBinding
|
||||
readController *controllers.TableReadController
|
||||
itemRenderer *itemrenderer.Service
|
||||
}
|
||||
|
||||
func (m *uiModule) uiCommand(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
|
|
@ -171,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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -191,15 +195,36 @@ func (m *uiModule) uiFilter(ctx context.Context, args ucl.CallArgs) (any, error)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *uiModule) uiSetItemAnnotator(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var inv ucl.Invokable
|
||||
if err := args.Bind(&inv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.itemRenderer.SetAnnotation(itemrenderer.AnnotationFunc(func(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string {
|
||||
v, err := inv.Invoke(ctx, newResultSetProxy(rs), itemProxy{rs, 0, item}, attrPathProxy{attrPath: &path})
|
||||
if err != nil {
|
||||
return ""
|
||||
} else if v == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}))
|
||||
commandctrl.QueueRefresh(ctx)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func moduleUI(
|
||||
tableService *tables.Service,
|
||||
state *controllers.State,
|
||||
readController *controllers.TableReadController,
|
||||
itemRenderer *itemrenderer.Service,
|
||||
) (ucl.Module, controllers.CustomKeyBindingSource) {
|
||||
m := &uiModule{
|
||||
tableService: tableService,
|
||||
state: state,
|
||||
readController: readController,
|
||||
itemRenderer: itemRenderer,
|
||||
ckb: &customKeyBinding{
|
||||
bindings: map[string]tea.Cmd{},
|
||||
keyBindings: map[string]string{},
|
||||
|
|
@ -209,14 +234,15 @@ func moduleUI(
|
|||
return ucl.Module{
|
||||
Name: "ui",
|
||||
Builtins: map[string]ucl.BuiltinHandler{
|
||||
"command": m.uiCommand,
|
||||
"prompt": m.uiPrompt,
|
||||
"prompt-table": m.uiPromptTable,
|
||||
"prompt-keypress": m.uiInKey,
|
||||
"confirm": m.uiConfirm,
|
||||
"query": m.uiQuery,
|
||||
"filter": m.uiFilter,
|
||||
"bind": m.uiBind,
|
||||
"command": m.uiCommand,
|
||||
"prompt": m.uiPrompt,
|
||||
"prompt-table": m.uiPromptTable,
|
||||
"prompt-keypress": m.uiInKey,
|
||||
"confirm": m.uiConfirm,
|
||||
"query": m.uiQuery,
|
||||
"filter": m.uiFilter,
|
||||
"bind": m.uiBind,
|
||||
"set-item-annotator": m.uiSetItemAnnotator,
|
||||
},
|
||||
}, m.ckb
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,49 @@ func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type attrPathProxy struct {
|
||||
attrPath *models.AttrPathNode
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) String() string {
|
||||
return "RSItem()"
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Truthy() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Len() (l int) {
|
||||
for p := ap.attrPath; p != nil; p = p.Parent {
|
||||
l++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Index(k int) ucl.Object {
|
||||
if k == -1 {
|
||||
return ucl.StringObject(ap.attrPath.Key)
|
||||
}
|
||||
|
||||
if k >= 0 {
|
||||
k = ap.Len() - k - 1
|
||||
} else {
|
||||
k = -k - 1
|
||||
}
|
||||
|
||||
if k < 0 {
|
||||
return nil
|
||||
}
|
||||
for p := ap.attrPath; p != nil; p = p.Parent {
|
||||
if k <= 0 {
|
||||
return ucl.StringObject(p.Key)
|
||||
}
|
||||
k -= 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type attributeValueProxy struct {
|
||||
value types.AttributeValue
|
||||
}
|
||||
|
|
|
|||
101
internal/common/ui/commandctrl/cmdpacks/proxy_test.go
Normal file
101
internal/common/ui/commandctrl/cmdpacks/proxy_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package cmdpacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
func TestAttrPathProxy_Index(t *testing.T) {
|
||||
tests := []struct {
|
||||
descr string
|
||||
attrPath models.AttrPathNode
|
||||
index int
|
||||
want string
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
descr: "return leaf 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf"},
|
||||
index: -1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return leaf 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: -1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return parent 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: -2,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return parent 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -2,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return parent 3",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -3,
|
||||
want: "grandparent",
|
||||
},
|
||||
{
|
||||
descr: "return root 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: 0,
|
||||
want: "grandparent",
|
||||
},
|
||||
{
|
||||
descr: "return root 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 0,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return root 3",
|
||||
attrPath: models.AttrPathNode{Key: "leaf"},
|
||||
index: 0,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return first child 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: 1,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return first child 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return nil 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -5,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
descr: "return nil 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 56,
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.descr, func(t *testing.T) {
|
||||
proxy := attrPathProxy{&tt.attrPath}
|
||||
if tt.wantNil {
|
||||
assert.Nil(t, proxy.Index(tt.index))
|
||||
} else {
|
||||
assert.Equal(t, tt.want, proxy.Index(tt.index).String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"ucl.lmika.dev/repl"
|
||||
"ucl.lmika.dev/ucl"
|
||||
|
|
@ -23,6 +24,7 @@ type StandardCommands struct {
|
|||
KeyBindingController *controllers.KeyBindingController
|
||||
PBProvider services.PasteboardProvider
|
||||
SettingsController *controllers.SettingsController
|
||||
ItemRenderer *itemrenderer.Service
|
||||
|
||||
modUI ucl.Module
|
||||
}
|
||||
|
|
@ -36,8 +38,9 @@ func NewStandardCommands(
|
|||
keyBindingController *controllers.KeyBindingController,
|
||||
pbProvider services.PasteboardProvider,
|
||||
settingsController *controllers.SettingsController,
|
||||
itemRenderer *itemrenderer.Service,
|
||||
) StandardCommands {
|
||||
modUI, ckbs := moduleUI(tableService, state, readController)
|
||||
modUI, ckbs := moduleUI(tableService, state, readController, itemRenderer)
|
||||
keyBindingController.SetCustomKeyBindingSource(ckbs)
|
||||
|
||||
return StandardCommands{
|
||||
|
|
@ -49,6 +52,7 @@ func NewStandardCommands(
|
|||
KeyBindingController: keyBindingController,
|
||||
PBProvider: pbProvider,
|
||||
SettingsController: settingsController,
|
||||
ItemRenderer: itemRenderer,
|
||||
modUI: modUI,
|
||||
}
|
||||
}
|
||||
|
|
@ -400,6 +404,7 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption {
|
|||
ucl.WithModule(modulePB(sc.PBProvider)),
|
||||
ucl.WithModule(moduleOpt(sc.SettingsController)),
|
||||
ucl.WithModule(moduleAttrValue()),
|
||||
ucl.WithModule(moduleAsync(sc.TableService, sc.State)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package cmdpacks_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -21,7 +23,6 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
||||
"lmika.dev/cmd/dynamo-browse/test/testdynamo"
|
||||
"lmika.dev/cmd/dynamo-browse/test/testworkspace"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStdCmds_Mark(t *testing.T) {
|
||||
|
|
@ -162,6 +163,7 @@ func newService(t *testing.T, opts ...serviceOpt) *services {
|
|||
keyBindingController,
|
||||
testPB,
|
||||
settingsController,
|
||||
itemRendererService,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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/builtins"
|
||||
|
||||
|
|
@ -17,12 +19,22 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
)
|
||||
|
||||
const commandsCategory = "commands"
|
||||
const (
|
||||
commandsCategory = "commands"
|
||||
pendingTaskBuffer = 50
|
||||
pendingAuxTaskBuffer = 50
|
||||
auxWorkers = 4
|
||||
)
|
||||
|
||||
type cmdMessage struct {
|
||||
cmd string
|
||||
}
|
||||
|
||||
type pendingTask struct {
|
||||
descr string
|
||||
task func(ctx context.Context) error
|
||||
}
|
||||
|
||||
type CommandController struct {
|
||||
uclInst *ucl.Inst
|
||||
historyProvider IterProvider
|
||||
|
|
@ -30,19 +42,29 @@ type CommandController struct {
|
|||
lookupExtensions []CommandLookupExtension
|
||||
completionProvider CommandCompletionProvider
|
||||
uiStateProvider UIStateProvider
|
||||
cronScheduler gocron.Scheduler
|
||||
cmdChan chan cmdMessage
|
||||
pendingTaskChan chan pendingTask
|
||||
pendingAuxTaskChan chan pendingTask
|
||||
msgChan chan tea.Msg
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*CommandController, error) {
|
||||
sched, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := &CommandController{
|
||||
historyProvider: historyProvider,
|
||||
commandList: nil,
|
||||
lookupExtensions: nil,
|
||||
cmdChan: make(chan cmdMessage),
|
||||
msgChan: make(chan tea.Msg),
|
||||
interactive: true,
|
||||
historyProvider: historyProvider,
|
||||
commandList: nil,
|
||||
lookupExtensions: nil,
|
||||
cronScheduler: sched,
|
||||
cmdChan: make(chan cmdMessage),
|
||||
pendingTaskChan: make(chan pendingTask, pendingTaskBuffer),
|
||||
pendingAuxTaskChan: make(chan pendingTask, pendingAuxTaskBuffer),
|
||||
msgChan: make(chan tea.Msg),
|
||||
interactive: true,
|
||||
}
|
||||
|
||||
options := []ucl.InstOption{
|
||||
|
|
@ -75,6 +97,8 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*C
|
|||
}
|
||||
|
||||
go cc.cmdLooper()
|
||||
go cc.auxCmdLooper()
|
||||
sched.Start()
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
|
@ -172,12 +196,13 @@ func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea
|
|||
}
|
||||
|
||||
func (c *CommandController) cmdLooper() {
|
||||
execCtx := execContext{ctrl: c}
|
||||
ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx)
|
||||
|
||||
ctx := context.Background()
|
||||
for {
|
||||
select {
|
||||
case cmdChan := <-c.cmdChan:
|
||||
execCtx := execContext{ctrl: c}
|
||||
ctx := context.WithValue(ctx, commandCtlKey, &execCtx)
|
||||
|
||||
res, err := c.ExecuteAndWait(ctx, cmdChan.cmd)
|
||||
if err != nil {
|
||||
c.postMessage(events.Error(err))
|
||||
|
|
@ -187,6 +212,16 @@ func (c *CommandController) cmdLooper() {
|
|||
if execCtx.requestRefresh {
|
||||
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) {
|
||||
log.Println(s)
|
||||
if c.msgChan == nil || !c.interactive {
|
||||
log.Println(s)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case c.msgChan <- events.StatusMsg(s):
|
||||
default:
|
||||
log.Println(s)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +358,21 @@ func (c *CommandController) postMessage(msg tea.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 {
|
||||
msg tea.Msg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package commandctrl
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pkg/errors"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
||||
|
|
@ -57,6 +60,40 @@ func QueueRefresh(ctx context.Context) {
|
|||
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 {
|
||||
Invoke(invokable ucl.Invokable, args []any) tea.Msg
|
||||
Inst() *ucl.Inst
|
||||
|
|
|
|||
6
internal/dynamo-browse/models/attrpath.go
Normal file
6
internal/dynamo-browse/models/attrpath.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package models
|
||||
|
||||
type AttrPathNode struct {
|
||||
Key string
|
||||
Parent *AttrPathNode
|
||||
}
|
||||
|
|
@ -2,18 +2,30 @@ package itemrenderer
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
styles styleRenderer
|
||||
annotation Annotation
|
||||
styles styleRenderer
|
||||
}
|
||||
|
||||
func NewService(fileTypeStyle StyleRenderer, metaInfoStyle StyleRenderer) *Service {
|
||||
func NewService(
|
||||
fileTypeStyle StyleRenderer,
|
||||
metaInfoStyle StyleRenderer,
|
||||
) *Service {
|
||||
if fileTypeStyle == nil {
|
||||
fileTypeStyle = plainTextStyleRenderer{}
|
||||
}
|
||||
if metaInfoStyle == nil {
|
||||
metaInfoStyle = plainTextStyleRenderer{}
|
||||
}
|
||||
return &Service{
|
||||
annotation: nil,
|
||||
styles: styleRenderer{
|
||||
fileTypeRenderer: fileTypeStyle,
|
||||
metaInfoRenderer: metaInfoStyle,
|
||||
|
|
@ -21,6 +33,10 @@ func NewService(fileTypeStyle StyleRenderer, metaInfoStyle StyleRenderer) *Servi
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetAnnotation(a Annotation) {
|
||||
s.annotation = a
|
||||
}
|
||||
|
||||
func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.ResultSet, plainText bool) {
|
||||
styles := s.styles
|
||||
if plainText {
|
||||
|
|
@ -33,25 +49,47 @@ func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.Re
|
|||
for _, colName := range resultSet.Columns() {
|
||||
seenColumns[colName] = struct{}{}
|
||||
if r := itemrender.ToRenderer(item[colName]); r != nil {
|
||||
s.renderItem(tabWriter, "", colName, r, styles)
|
||||
p := models.AttrPathNode{Key: colName}
|
||||
s.renderItem(tabWriter, resultSet, item, p, "", r, styles)
|
||||
}
|
||||
}
|
||||
for k, _ := range item {
|
||||
if _, seen := seenColumns[k]; !seen {
|
||||
if r := itemrender.ToRenderer(item[k]); r != nil {
|
||||
s.renderItem(tabWriter, "", k, r, styles)
|
||||
p := models.AttrPathNode{Key: k}
|
||||
s.renderItem(tabWriter, resultSet, item, p, "", r, styles)
|
||||
}
|
||||
}
|
||||
}
|
||||
tabWriter.Flush()
|
||||
}
|
||||
|
||||
func (m *Service) renderItem(w io.Writer, prefix string, name string, r itemrender.Renderer, sr styleRenderer) {
|
||||
fmt.Fprintf(w, "%s%v\t%s\t%s%s\n",
|
||||
prefix, name, sr.fileTypeRenderer.Render(r.TypeName()), r.StringValue(), sr.metaInfoRenderer.Render(r.MetaInfo()))
|
||||
func (m *Service) renderItem(
|
||||
w io.Writer,
|
||||
resultSet *models.ResultSet,
|
||||
item models.Item,
|
||||
path models.AttrPathNode,
|
||||
prefix string,
|
||||
r itemrender.Renderer,
|
||||
sr styleRenderer,
|
||||
) {
|
||||
fmt.Fprint(w, prefix)
|
||||
fmt.Fprint(w, path.Key)
|
||||
fmt.Fprint(w, "\t")
|
||||
fmt.Fprint(w, sr.fileTypeRenderer.Render(r.TypeName()))
|
||||
fmt.Fprint(w, "\t")
|
||||
fmt.Fprint(w, r.StringValue())
|
||||
fmt.Fprint(w, sr.metaInfoRenderer.Render(r.MetaInfo()))
|
||||
if m.annotation != nil {
|
||||
fmt.Fprint(w, " ")
|
||||
fmt.Fprint(w, sr.metaInfoRenderer.Render(m.annotation.AnnotateAttribute(resultSet, item, path)))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
|
||||
if subitems := r.SubItems(); len(subitems) > 0 {
|
||||
for _, si := range subitems {
|
||||
m.renderItem(w, prefix+" ", si.Key, si.Value, sr)
|
||||
p := models.AttrPathNode{Key: si.Key, Parent: &path}
|
||||
m.renderItem(w, resultSet, item, p, prefix+" ", si.Value, sr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,3 +98,13 @@ type styleRenderer struct {
|
|||
fileTypeRenderer StyleRenderer
|
||||
metaInfoRenderer StyleRenderer
|
||||
}
|
||||
|
||||
type Annotation interface {
|
||||
AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string
|
||||
}
|
||||
|
||||
type AnnotationFunc func(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string
|
||||
|
||||
func (af AnnotationFunc) AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string {
|
||||
return af(rs, item, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package dynamoitemview
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"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/layout"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type ItemViewStyle struct {
|
|||
var DefaultStyles = Styles{
|
||||
ItemView: ItemViewStyle{
|
||||
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{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue