From c11560e6cd6f3654cd6c9fd6804c1d49eff1ff1a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 1 Nov 2025 10:39:10 +1100 Subject: [PATCH] Added ui:set-item-annotator --- cmd/dynamo-browse/main.go | 3 +- .../common/ui/commandctrl/cmdpacks/modui.go | 41 +++++-- .../common/ui/commandctrl/cmdpacks/proxy.go | 43 ++++++++ .../ui/commandctrl/cmdpacks/proxy_test.go | 101 ++++++++++++++++++ .../common/ui/commandctrl/cmdpacks/stdcmds.go | 6 +- .../ui/commandctrl/cmdpacks/stdcmds_test.go | 4 +- .../services/itemrenderer/service.go | 27 +++-- 7 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 internal/common/ui/commandctrl/cmdpacks/proxy_test.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index c7957e6..c0efd12 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -110,7 +110,7 @@ func main() { tableService := tables.NewService(dynamoProvider, settingStore) workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(nil, uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo) + itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo) jobsService := jobs.NewService(eventBus) inputHistoryService := inputhistory.New(inputHistoryStore) @@ -177,6 +177,7 @@ func main() { keyBindingController, pasteboardProvider, settingsController, + itemRendererService, ) commandController, err := commandctrl.NewCommandController(inputHistoryService, stdCommands) diff --git a/internal/common/ui/commandctrl/cmdpacks/modui.go b/internal/common/ui/commandctrl/cmdpacks/modui.go index 8075639..993d02e 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modui.go +++ b/internal/common/ui/commandctrl/cmdpacks/modui.go @@ -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) { @@ -191,15 +195,35 @@ 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) + })) + 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 +233,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 } diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index 9aa9fb3..02f2325 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -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 } diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy_test.go b/internal/common/ui/commandctrl/cmdpacks/proxy_test.go new file mode 100644 index 0000000..372e211 --- /dev/null +++ b/internal/common/ui/commandctrl/cmdpacks/proxy_test.go @@ -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()) + } + }) + } +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 326543b..5231506 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -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, } } diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go index 2d65be8..8863ca4 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds_test.go @@ -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, ), ) diff --git a/internal/dynamo-browse/services/itemrenderer/service.go b/internal/dynamo-browse/services/itemrenderer/service.go index d0de447..f4cee9e 100644 --- a/internal/dynamo-browse/services/itemrenderer/service.go +++ b/internal/dynamo-browse/services/itemrenderer/service.go @@ -10,17 +10,22 @@ import ( ) type Service struct { - annotations Annotation - styles styleRenderer + annotation Annotation + styles styleRenderer } func NewService( - annotations Annotation, fileTypeStyle StyleRenderer, metaInfoStyle StyleRenderer, ) *Service { + if fileTypeStyle == nil { + fileTypeStyle = plainTextStyleRenderer{} + } + if metaInfoStyle == nil { + metaInfoStyle = plainTextStyleRenderer{} + } return &Service{ - annotations: testAnnotation{}, + annotation: nil, styles: styleRenderer{ fileTypeRenderer: fileTypeStyle, metaInfoRenderer: metaInfoStyle, @@ -28,6 +33,10 @@ func NewService( } } +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 { @@ -71,9 +80,9 @@ func (m *Service) renderItem( fmt.Fprint(w, "\t") fmt.Fprint(w, r.StringValue()) fmt.Fprint(w, sr.metaInfoRenderer.Render(r.MetaInfo())) - if m.annotations != nil { + if m.annotation != nil { fmt.Fprint(w, " ") - fmt.Fprint(w, sr.metaInfoRenderer.Render(m.annotations.AnnotateAttribute(resultSet, item, path))) + fmt.Fprint(w, sr.metaInfoRenderer.Render(m.annotation.AnnotateAttribute(resultSet, item, path))) } fmt.Fprint(w, "\n") @@ -94,8 +103,8 @@ type Annotation interface { AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string } -type testAnnotation struct{} +type AnnotationFunc func(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string -func (t testAnnotation) AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string { - return "( annotation of " + path.Key + " )" +func (af AnnotationFunc) AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string { + return af(rs, item, path) }