diff --git a/go.mod b/go.mod index 51c142d..887aaf1 100644 --- a/go.mod +++ b/go.mod @@ -117,5 +117,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ucl.lmika.dev v0.0.0-20250517115116-0f1ceba0902e // indirect + ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 // indirect ) diff --git a/go.sum b/go.sum index 1e50447..d77e7b4 100644 --- a/go.sum +++ b/go.sum @@ -444,3 +444,13 @@ ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0 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= diff --git a/internal/common/ui/commandctrl/cmdpacks/modrs.go b/internal/common/ui/commandctrl/cmdpacks/modrs.go index 4af1513..5f3060b 100644 --- a/internal/common/ui/commandctrl/cmdpacks/modrs.go +++ b/internal/common/ui/commandctrl/cmdpacks/modrs.go @@ -45,12 +45,10 @@ func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (_ any, err er return nil, errors.New("no table specified") } - return ResultSetProxy{ - RS: &models.ResultSet{ - TableInfo: tableInfo, - Created: time.Now(), - }, - }, nil + return newResultSetProxy(&models.ResultSet{ + TableInfo: tableInfo, + Created: time.Now(), + }), nil } var rsQueryDoc = repl.Doc{ @@ -128,9 +126,7 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) return nil, err } - return ResultSetProxy{ - RS: newResultSet, - }, nil + return newResultSetProxy(newResultSet), nil } func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module { diff --git a/internal/common/ui/commandctrl/cmdpacks/proxy.go b/internal/common/ui/commandctrl/cmdpacks/proxy.go index b80aab2..5606f94 100644 --- a/internal/common/ui/commandctrl/cmdpacks/proxy.go +++ b/internal/common/ui/commandctrl/cmdpacks/proxy.go @@ -1,7 +1,180 @@ package cmdpacks -import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" +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 ResultSetProxy struct { - RS *models.ResultSet +type proxyFields[T any] map[string]func(t T) ucl.Object + +type simpleProxy[T comparable] struct { + value T + fields proxyFields[T] +} + +func (tp simpleProxy[T]) String() string { + return fmt.Sprint(tp.value) +} + +func (tp simpleProxy[T]) Truthy() bool { + var zeroT T + return tp.value != zeroT +} + +func (tp simpleProxy[T]) Len() int { + return len(tp.fields) +} + +func (tp simpleProxy[T]) Value(k string) ucl.Object { + f, ok := tp.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.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, fields: resultSetProxyFields} +} + +var resultSetProxyFields = proxyFields[*models.ResultSet]{ + "Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, + "Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, +} + +func newTableProxy(table *models.TableInfo) ucl.Object { + return simpleProxy[*models.TableInfo]{value: table, fields: tableProxyFields} +} + +var tableProxyFields = proxyFields[*models.TableInfo]{ + "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, fields: keyAttributeProxyFields} +} + +var keyAttributeProxyFields = proxyFields[models.KeyAttribute]{ + "PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) }, + "SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) }, +} + +func newGSIProxy(gsi models.TableGSI) ucl.Object { + return simpleProxy[models.TableGSI]{value: gsi, fields: gsiProxyFields} +} + +var gsiProxyFields = proxyFields[models.TableGSI]{ + "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 "items" +} + +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 "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 } diff --git a/internal/common/ui/commandctrl/cmdpacks/pvars.go b/internal/common/ui/commandctrl/cmdpacks/pvars.go index 5f5020e..d297a98 100644 --- a/internal/common/ui/commandctrl/cmdpacks/pvars.go +++ b/internal/common/ui/commandctrl/cmdpacks/pvars.go @@ -4,28 +4,59 @@ 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" - "log" ) +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 ResultSetProxy{rs.state.ResultSet()}, nil + return newResultSetProxy(rs.state.ResultSet()), nil } func (rs resultSetPVar) Set(ctx context.Context, value any) error { - rsVal, ok := value.(ResultSetProxy) + rsVal, ok := value.(simpleProxy[*models.ResultSet]) if !ok { return errors.New("new value to @resultset is not a result set") } - log.Printf("type = %T", rsVal.RS) - - msg := rs.readController.SetResultSet(rsVal.RS) + 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 +} diff --git a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go index 4682926..e5c1aab 100644 --- a/internal/common/ui/commandctrl/cmdpacks/stdcmds.go +++ b/internal/common/ui/commandctrl/cmdpacks/stdcmds.go @@ -386,4 +386,6 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { // 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}) } diff --git a/internal/common/ui/commandctrl/ctx.go b/internal/common/ui/commandctrl/ctx.go index 7c6f193..7fc70ea 100644 --- a/internal/common/ui/commandctrl/ctx.go +++ b/internal/common/ui/commandctrl/ctx.go @@ -24,3 +24,12 @@ func SelectedItemIndex(ctx context.Context) (int, bool) { return cmdCtl.uiStateProvider.SelectedItemIndex(), true } + +func SetSelectedItemIndex(ctx context.Context, newIdx int) tea.Msg { + cmdCtl, ok := ctx.Value(commandCtlKey).(*CommandController) + if !ok { + return nil + } + + return cmdCtl.uiStateProvider.SetSelectedItemIndex(newIdx) +} diff --git a/internal/common/ui/commandctrl/iface.go b/internal/common/ui/commandctrl/iface.go index 41d3a7f..e708d24 100644 --- a/internal/common/ui/commandctrl/iface.go +++ b/internal/common/ui/commandctrl/iface.go @@ -2,6 +2,7 @@ package commandctrl import ( "context" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" ) @@ -11,4 +12,5 @@ type IterProvider interface { type UIStateProvider interface { SelectedItemIndex() int + SetSelectedItemIndex(newIdx int) tea.Msg } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index d57c816..9acb70e 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -397,3 +397,7 @@ func (m *Model) promptToQuit() tea.Msg { func (m *Model) SelectedItemIndex() int { return m.tableView.SelectedItemIndex() } + +func (m *Model) SetSelectedItemIndex(newIdx int) tea.Msg { + return m.tableView.SetSelectedItemIndex(newIdx) +} diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index cf61d72..163de6f 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -208,6 +208,27 @@ func (m *Model) SelectedItemIndex() int { 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) { resultSet := m.resultSet