Added ui:set-item-annotator

This commit is contained in:
Leon Mika 2025-11-01 10:39:10 +11:00
parent a733a47d5c
commit c11560e6cd
7 changed files with 205 additions and 20 deletions

View file

@ -110,7 +110,7 @@ func main() {
tableService := tables.NewService(dynamoProvider, settingStore) tableService := tables.NewService(dynamoProvider, settingStore)
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore) 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) jobsService := jobs.NewService(eventBus)
inputHistoryService := inputhistory.New(inputHistoryStore) inputHistoryService := inputhistory.New(inputHistoryStore)
@ -177,6 +177,7 @@ func main() {
keyBindingController, keyBindingController,
pasteboardProvider, pasteboardProvider,
settingsController, settingsController,
itemRendererService,
) )
commandController, err := commandctrl.NewCommandController(inputHistoryService, stdCommands) commandController, err := commandctrl.NewCommandController(inputHistoryService, stdCommands)

View file

@ -2,11 +2,14 @@ package cmdpacks
import ( import (
"context" "context"
"fmt"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" "lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" "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/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" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -16,6 +19,7 @@ type uiModule struct {
state *controllers.State state *controllers.State
ckb *customKeyBinding ckb *customKeyBinding
readController *controllers.TableReadController readController *controllers.TableReadController
itemRenderer *itemrenderer.Service
} }
func (m *uiModule) uiCommand(ctx context.Context, args ucl.CallArgs) (any, error) { 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 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( func moduleUI(
tableService *tables.Service, tableService *tables.Service,
state *controllers.State, state *controllers.State,
readController *controllers.TableReadController, readController *controllers.TableReadController,
itemRenderer *itemrenderer.Service,
) (ucl.Module, controllers.CustomKeyBindingSource) { ) (ucl.Module, controllers.CustomKeyBindingSource) {
m := &uiModule{ m := &uiModule{
tableService: tableService, tableService: tableService,
state: state, state: state,
readController: readController, readController: readController,
itemRenderer: itemRenderer,
ckb: &customKeyBinding{ ckb: &customKeyBinding{
bindings: map[string]tea.Cmd{}, bindings: map[string]tea.Cmd{},
keyBindings: map[string]string{}, keyBindings: map[string]string{},
@ -209,14 +233,15 @@ func moduleUI(
return ucl.Module{ return ucl.Module{
Name: "ui", Name: "ui",
Builtins: map[string]ucl.BuiltinHandler{ Builtins: map[string]ucl.BuiltinHandler{
"command": m.uiCommand, "command": m.uiCommand,
"prompt": m.uiPrompt, "prompt": m.uiPrompt,
"prompt-table": m.uiPromptTable, "prompt-table": m.uiPromptTable,
"prompt-keypress": m.uiInKey, "prompt-keypress": m.uiInKey,
"confirm": m.uiConfirm, "confirm": m.uiConfirm,
"query": m.uiQuery, "query": m.uiQuery,
"filter": m.uiFilter, "filter": m.uiFilter,
"bind": m.uiBind, "bind": m.uiBind,
"set-item-annotator": m.uiSetItemAnnotator,
}, },
}, m.ckb }, m.ckb
} }

View file

@ -234,6 +234,49 @@ func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error {
return nil 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 { type attributeValueProxy struct {
value types.AttributeValue value types.AttributeValue
} }

View 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())
}
})
}
}

View file

@ -9,6 +9,7 @@ import (
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" "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/models"
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" "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" "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
"ucl.lmika.dev/repl" "ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
@ -23,6 +24,7 @@ type StandardCommands struct {
KeyBindingController *controllers.KeyBindingController KeyBindingController *controllers.KeyBindingController
PBProvider services.PasteboardProvider PBProvider services.PasteboardProvider
SettingsController *controllers.SettingsController SettingsController *controllers.SettingsController
ItemRenderer *itemrenderer.Service
modUI ucl.Module modUI ucl.Module
} }
@ -36,8 +38,9 @@ func NewStandardCommands(
keyBindingController *controllers.KeyBindingController, keyBindingController *controllers.KeyBindingController,
pbProvider services.PasteboardProvider, pbProvider services.PasteboardProvider,
settingsController *controllers.SettingsController, settingsController *controllers.SettingsController,
itemRenderer *itemrenderer.Service,
) StandardCommands { ) StandardCommands {
modUI, ckbs := moduleUI(tableService, state, readController) modUI, ckbs := moduleUI(tableService, state, readController, itemRenderer)
keyBindingController.SetCustomKeyBindingSource(ckbs) keyBindingController.SetCustomKeyBindingSource(ckbs)
return StandardCommands{ return StandardCommands{
@ -49,6 +52,7 @@ func NewStandardCommands(
KeyBindingController: keyBindingController, KeyBindingController: keyBindingController,
PBProvider: pbProvider, PBProvider: pbProvider,
SettingsController: settingsController, SettingsController: settingsController,
ItemRenderer: itemRenderer,
modUI: modUI, modUI: modUI,
} }
} }

View file

@ -2,6 +2,8 @@ package cmdpacks_test
import ( import (
"fmt" "fmt"
"testing"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
bus "github.com/lmika/events" bus "github.com/lmika/events"
"github.com/stretchr/testify/assert" "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/internal/dynamo-browse/ui/keybindings"
"lmika.dev/cmd/dynamo-browse/test/testdynamo" "lmika.dev/cmd/dynamo-browse/test/testdynamo"
"lmika.dev/cmd/dynamo-browse/test/testworkspace" "lmika.dev/cmd/dynamo-browse/test/testworkspace"
"testing"
) )
func TestStdCmds_Mark(t *testing.T) { func TestStdCmds_Mark(t *testing.T) {
@ -162,6 +163,7 @@ func newService(t *testing.T, opts ...serviceOpt) *services {
keyBindingController, keyBindingController,
testPB, testPB,
settingsController, settingsController,
itemRendererService,
), ),
) )

View file

@ -10,17 +10,22 @@ import (
) )
type Service struct { type Service struct {
annotations Annotation annotation Annotation
styles styleRenderer styles styleRenderer
} }
func NewService( func NewService(
annotations Annotation,
fileTypeStyle StyleRenderer, fileTypeStyle StyleRenderer,
metaInfoStyle StyleRenderer, metaInfoStyle StyleRenderer,
) *Service { ) *Service {
if fileTypeStyle == nil {
fileTypeStyle = plainTextStyleRenderer{}
}
if metaInfoStyle == nil {
metaInfoStyle = plainTextStyleRenderer{}
}
return &Service{ return &Service{
annotations: testAnnotation{}, annotation: nil,
styles: styleRenderer{ styles: styleRenderer{
fileTypeRenderer: fileTypeStyle, fileTypeRenderer: fileTypeStyle,
metaInfoRenderer: metaInfoStyle, 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) { func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.ResultSet, plainText bool) {
styles := s.styles styles := s.styles
if plainText { if plainText {
@ -71,9 +80,9 @@ func (m *Service) renderItem(
fmt.Fprint(w, "\t") fmt.Fprint(w, "\t")
fmt.Fprint(w, r.StringValue()) fmt.Fprint(w, r.StringValue())
fmt.Fprint(w, sr.metaInfoRenderer.Render(r.MetaInfo())) fmt.Fprint(w, sr.metaInfoRenderer.Render(r.MetaInfo()))
if m.annotations != nil { if m.annotation != nil {
fmt.Fprint(w, " ") 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") fmt.Fprint(w, "\n")
@ -94,8 +103,8 @@ type Annotation interface {
AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string 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 { func (af AnnotationFunc) AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string {
return "( annotation of " + path.Key + " )" return af(rs, item, path)
} }