Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8b65f6b0a | ||
|
|
85a4f0b5e9 | ||
|
|
a33f441d45 | ||
|
|
c36290da24 | ||
|
|
e92e817a77 |
2
go.mod
2
go.mod
|
|
@ -28,7 +28,7 @@ require (
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
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.1
|
ucl.lmika.dev v0.1.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -249,3 +249,5 @@ lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c=
|
||||||
lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
ucl.lmika.dev v0.1.1 h1:P8nEqJPKS+wmXZiSjEmJkOUeWQF9YxWSymDkLXt9mvg=
|
ucl.lmika.dev v0.1.1 h1:P8nEqJPKS+wmXZiSjEmJkOUeWQF9YxWSymDkLXt9mvg=
|
||||||
ucl.lmika.dev v0.1.1/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k=
|
ucl.lmika.dev v0.1.1/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k=
|
||||||
|
ucl.lmika.dev v0.1.2 h1:dTqLKGw/pPqE7UrkrJd5qPu2i6BTDzJLaM0cRkJGn6A=
|
||||||
|
ucl.lmika.dev v0.1.2/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k=
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,40 @@ func TestModRS_Query(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModRS_Filter(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
descr string
|
||||||
|
cmd string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
descr: "returns filtered items 1",
|
||||||
|
cmd: `
|
||||||
|
rs = rs:scan -table service-test-data
|
||||||
|
rs = rs:filter $rs 'pk="abc"'
|
||||||
|
assert (len $rs) "expected len == 2"
|
||||||
|
assert (eq $rs.First.pk "abc") "expected First.pk == abc"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
//{
|
||||||
|
// descr: "returns filtered items 2",
|
||||||
|
// cmd: `
|
||||||
|
// rs = rs:scan -table service-test-data
|
||||||
|
// rs = rs:filter $rs 'pk="bbb"'
|
||||||
|
// assert (len $rs) "expected len == 1"
|
||||||
|
// assert (eq $rs.First.pk "bbb") "expected First.pk == bbb"
|
||||||
|
// `,
|
||||||
|
//},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.descr, func(t *testing.T) {
|
||||||
|
svc := newService(t)
|
||||||
|
|
||||||
|
_, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestModRS_First(t *testing.T) {
|
func TestModRS_First(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
descr string
|
descr string
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,31 @@ func (tp resultSetItemsProxy) Index(k int) ucl.Object {
|
||||||
return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]}
|
return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resultSetMarkedItemsProxy struct {
|
||||||
|
resultSet *models.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ip resultSetMarkedItemsProxy) String() string {
|
||||||
|
return fmt.Sprintf("MarkedItems(%v)", len(ip.resultSet.MarkedItems()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ip resultSetMarkedItemsProxy) Truthy() bool {
|
||||||
|
return len(ip.resultSet.MarkedItems()) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp resultSetMarkedItemsProxy) Len() int {
|
||||||
|
return len(tp.resultSet.MarkedItems())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp resultSetMarkedItemsProxy) Index(k int) ucl.Object {
|
||||||
|
markedItems := tp.resultSet.MarkedItems()
|
||||||
|
if k >= len(markedItems) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actualItem := tp.resultSet.Items()[markedItems[k].Index]
|
||||||
|
return itemProxy{resultSet: tp.resultSet, idx: markedItems[k].Index, item: actualItem}
|
||||||
|
}
|
||||||
|
|
||||||
type itemProxy struct {
|
type itemProxy struct {
|
||||||
resultSet *models.ResultSet
|
resultSet *models.ResultSet
|
||||||
idx int
|
idx int
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,53 @@ package cmdpacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
"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/controllers"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||||
"github.com/pkg/errors"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tablePVar struct {
|
type tablePVar struct {
|
||||||
state *controllers.State
|
state *controllers.State
|
||||||
|
tableService *tables.Service
|
||||||
|
readController *controllers.TableReadController
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs tablePVar) Get(ctx context.Context) (any, error) {
|
func (rs tablePVar) Get(ctx context.Context) (any, error) {
|
||||||
return newTableProxy(rs.state.ResultSet().TableInfo), nil
|
return newTableProxy(rs.state.ResultSet().TableInfo), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs tablePVar) Set(ctx context.Context, value any) error {
|
||||||
|
scanNewTable := func(name string) error {
|
||||||
|
tableInfo, err := rs.tableService.Describe(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "cannot describe %v", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSet, err := rs.tableService.Scan(ctx, tableInfo)
|
||||||
|
if resultSet != nil {
|
||||||
|
resultSet = rs.tableService.Filter(resultSet, rs.state.Filter())
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := rs.readController.SetResultSet(resultSet)
|
||||||
|
commandctrl.PostMsg(ctx, msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tblVal, ok := value.(SimpleProxy[*models.TableInfo])
|
||||||
|
if ok {
|
||||||
|
return scanNewTable(tblVal.value.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
strVal, ok := value.(string)
|
||||||
|
if ok {
|
||||||
|
return scanNewTable(strVal)
|
||||||
|
}
|
||||||
|
return errors.New("new value to @table is not a table name")
|
||||||
|
}
|
||||||
|
|
||||||
type resultSetPVar struct {
|
type resultSetPVar struct {
|
||||||
state *controllers.State
|
state *controllers.State
|
||||||
readController *controllers.TableReadController
|
readController *controllers.TableReadController
|
||||||
|
|
@ -36,6 +69,15 @@ func (rs resultSetPVar) Set(ctx context.Context, value any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type markedSetPVar struct {
|
||||||
|
state *controllers.State
|
||||||
|
readController *controllers.TableReadController
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs markedSetPVar) Get(ctx context.Context) (any, error) {
|
||||||
|
return resultSetMarkedItemsProxy{rs.state.ResultSet()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type itemPVar struct {
|
type itemPVar struct {
|
||||||
state *controllers.State
|
state *controllers.State
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +85,14 @@ type itemPVar struct {
|
||||||
func (rs itemPVar) Get(ctx context.Context) (any, error) {
|
func (rs itemPVar) Get(ctx context.Context) (any, error) {
|
||||||
selItem, ok := commandctrl.SelectedItemIndex(ctx)
|
selItem, ok := commandctrl.SelectedItemIndex(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("no item selected")
|
return nil, nil
|
||||||
}
|
}
|
||||||
return itemProxy{rs.state.ResultSet(), selItem, rs.state.ResultSet().Items()[selItem]}, nil
|
rset := rs.state.ResultSet()
|
||||||
|
if selItem < 0 || selItem >= len(rs.state.ResultSet().Items()) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemProxy{rset, selItem, rset.Items()[selItem]}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs itemPVar) Set(ctx context.Context, value any) error {
|
func (rs itemPVar) Set(ctx context.Context, value any) error {
|
||||||
|
|
|
||||||
32
internal/common/ui/commandctrl/cmdpacks/pvars_test.go
Normal file
32
internal/common/ui/commandctrl/cmdpacks/pvars_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cmdpacks_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPVars(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
descr string
|
||||||
|
cmd string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
descr: "returns item on empty result set",
|
||||||
|
cmd: `
|
||||||
|
ui:query '"a"="1"' -table service-test-data
|
||||||
|
@item
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.descr, func(t *testing.T) {
|
||||||
|
svc := newService(t)
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
_, err := svc.CommandController.ExecuteAndWait(ctx, tt.cmd)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package cmdpacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"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"
|
||||||
|
|
@ -426,8 +427,9 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) {
|
||||||
ucl.SetBuiltin("q", sc.cmdQuit)
|
ucl.SetBuiltin("q", sc.cmdQuit)
|
||||||
|
|
||||||
ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController})
|
ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController})
|
||||||
ucl.SetPseudoVar("table", tablePVar{sc.State})
|
ucl.SetPseudoVar("table", tablePVar{sc.State, sc.TableService, sc.ReadController})
|
||||||
ucl.SetPseudoVar("item", itemPVar{sc.State})
|
ucl.SetPseudoVar("item", itemPVar{sc.State})
|
||||||
|
ucl.SetPseudoVar("marked", markedSetPVar{sc.State, sc.ReadController})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error {
|
func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error {
|
||||||
|
|
@ -438,4 +440,13 @@ func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error
|
||||||
const uclPrelude = `
|
const uclPrelude = `
|
||||||
ui:command unmark { mark none }
|
ui:command unmark { mark none }
|
||||||
ui:command set-opt { |n k| opt:set $n $k }
|
ui:command set-opt { |n k| opt:set $n $k }
|
||||||
|
|
||||||
|
ui:bind "view.toggle-marked-items" "M" {
|
||||||
|
markedCount = len @marked
|
||||||
|
if (eq $markedCount (len @resultset)) {
|
||||||
|
mark none
|
||||||
|
} else {
|
||||||
|
mark all
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import (
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"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/models"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrcodec"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrcodec"
|
||||||
|
|
@ -20,8 +22,6 @@ import (
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
|
||||||
bus "github.com/lmika/events"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type resultSetUpdateOp int
|
type resultSetUpdateOp int
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResultSet struct {
|
type ResultSet struct {
|
||||||
|
|
@ -20,6 +22,10 @@ type ResultSet struct {
|
||||||
|
|
||||||
columns []string
|
columns []string
|
||||||
sortCriteria SortCriteria
|
sortCriteria SortCriteria
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
cachedMarkedItems []ItemIndex
|
||||||
|
hasCachedMarkedItems bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Queryable interface {
|
type Queryable interface {
|
||||||
|
|
@ -47,6 +53,11 @@ func (rs *ResultSet) Items() []Item {
|
||||||
func (rs *ResultSet) SetItems(items []Item) {
|
func (rs *ResultSet) SetItems(items []Item) {
|
||||||
rs.items = items
|
rs.items = items
|
||||||
rs.attributes = make([]ItemAttribute, len(items))
|
rs.attributes = make([]ItemAttribute, len(items))
|
||||||
|
|
||||||
|
rs.mutex.Lock()
|
||||||
|
defer rs.mutex.Unlock()
|
||||||
|
rs.hasCachedMarkedItems = false
|
||||||
|
rs.cachedMarkedItems = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResultSet) SortCriteria() SortCriteria {
|
func (rs *ResultSet) SortCriteria() SortCriteria {
|
||||||
|
|
@ -56,10 +67,24 @@ func (rs *ResultSet) SortCriteria() SortCriteria {
|
||||||
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
|
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
|
||||||
rs.items = append(rs.items, item)
|
rs.items = append(rs.items, item)
|
||||||
rs.attributes = append(rs.attributes, attrs)
|
rs.attributes = append(rs.attributes, attrs)
|
||||||
|
|
||||||
|
rs.mutex.Lock()
|
||||||
|
defer rs.mutex.Unlock()
|
||||||
|
rs.hasCachedMarkedItems = false
|
||||||
|
rs.cachedMarkedItems = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResultSet) SetMark(idx int, marked bool) {
|
func (rs *ResultSet) SetMark(idx int, marked bool) {
|
||||||
rs.attributes[idx].Marked = marked
|
rs.attributes[idx].Marked = marked
|
||||||
|
|
||||||
|
if !rs.hasCachedMarkedItems {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.mutex.Lock()
|
||||||
|
defer rs.mutex.Unlock()
|
||||||
|
rs.hasCachedMarkedItems = false
|
||||||
|
rs.cachedMarkedItems = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResultSet) SetHidden(idx int, hidden bool) {
|
func (rs *ResultSet) SetHidden(idx int, hidden bool) {
|
||||||
|
|
@ -91,12 +116,20 @@ func (rs *ResultSet) IsNew(idx int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ResultSet) MarkedItems() []ItemIndex {
|
func (rs *ResultSet) MarkedItems() []ItemIndex {
|
||||||
|
rs.mutex.Lock()
|
||||||
|
defer rs.mutex.Unlock()
|
||||||
|
if rs.hasCachedMarkedItems {
|
||||||
|
return rs.cachedMarkedItems
|
||||||
|
}
|
||||||
|
|
||||||
items := make([]ItemIndex, 0)
|
items := make([]ItemIndex, 0)
|
||||||
for i, itemAttr := range rs.attributes {
|
for i, itemAttr := range rs.attributes {
|
||||||
if itemAttr.Marked && !itemAttr.Hidden {
|
if itemAttr.Marked && !itemAttr.Hidden {
|
||||||
items = append(items, ItemIndex{Index: i, Item: rs.items[i]})
|
items = append(items, ItemIndex{Index: i, Item: rs.items[i]})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rs.cachedMarkedItems = items
|
||||||
|
rs.hasCachedMarkedItems = true
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
183
internal/dynamo-browse/models/models_test.go
Normal file
183
internal/dynamo-browse/models/models_test.go
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarkedItems(t *testing.T) {
|
||||||
|
t.Run("SetMark properly reflected in MarkedItems", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initially, no items should be marked
|
||||||
|
assert.Len(t, rs.MarkedItems(), 0)
|
||||||
|
|
||||||
|
// Mark the first item
|
||||||
|
rs.SetMark(0, true)
|
||||||
|
markedItems := rs.MarkedItems()
|
||||||
|
assert.Len(t, markedItems, 1)
|
||||||
|
assert.Equal(t, 0, markedItems[0].Index)
|
||||||
|
|
||||||
|
// Mark the third item
|
||||||
|
rs.SetMark(2, true)
|
||||||
|
markedItems = rs.MarkedItems()
|
||||||
|
assert.Len(t, markedItems, 2)
|
||||||
|
assert.Equal(t, 0, markedItems[0].Index)
|
||||||
|
assert.Equal(t, 2, markedItems[1].Index)
|
||||||
|
|
||||||
|
// Verify the items themselves are correct
|
||||||
|
item1, ok1 := markedItems[0].Item.AttributeValueAsString("id")
|
||||||
|
item2, ok2 := markedItems[1].Item.AttributeValueAsString("id")
|
||||||
|
assert.True(t, ok1)
|
||||||
|
assert.True(t, ok2)
|
||||||
|
assert.Equal(t, "item1", item1)
|
||||||
|
assert.Equal(t, "item3", item2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("item with Marked=true is in MarkedItems", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Directly set the Marked attribute to true for item at index 1
|
||||||
|
rs.SetMark(1, true)
|
||||||
|
|
||||||
|
markedItems := rs.MarkedItems()
|
||||||
|
assert.Len(t, markedItems, 1)
|
||||||
|
assert.Equal(t, 1, markedItems[0].Index)
|
||||||
|
|
||||||
|
item, ok := markedItems[0].Item.AttributeValueAsString("id")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "item2", item)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding marked items affects result of MarkedItems", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark all items
|
||||||
|
rs.SetMark(0, true)
|
||||||
|
rs.SetMark(1, true)
|
||||||
|
assert.Len(t, rs.MarkedItems(), 2)
|
||||||
|
|
||||||
|
markedItems := rs.MarkedItems()
|
||||||
|
expectedIndices := []int{0, 1}
|
||||||
|
for i, expected := range expectedIndices {
|
||||||
|
assert.Equal(t, expected, markedItems[i].Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new unmarked item
|
||||||
|
rs.AddNewItem(Item{"id": &types.AttributeValueMemberS{Value: "item4"}}, ItemAttribute{})
|
||||||
|
assert.Len(t, rs.MarkedItems(), 2)
|
||||||
|
|
||||||
|
// Add a new marked item
|
||||||
|
rs.AddNewItem(Item{"id": &types.AttributeValueMemberS{Value: "item5"}}, ItemAttribute{Marked: true})
|
||||||
|
assert.Len(t, rs.MarkedItems(), 3)
|
||||||
|
|
||||||
|
markedItems = rs.MarkedItems()
|
||||||
|
expectedIndices = []int{0, 1, 4}
|
||||||
|
for i, expected := range expectedIndices {
|
||||||
|
assert.Equal(t, expected, markedItems[i].Index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("changing SetMark updates length of MarkedItems", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item4"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark all items
|
||||||
|
rs.SetMark(0, true)
|
||||||
|
rs.SetMark(1, true)
|
||||||
|
rs.SetMark(2, true)
|
||||||
|
rs.SetMark(3, true)
|
||||||
|
assert.Len(t, rs.MarkedItems(), 4)
|
||||||
|
|
||||||
|
// Unmark one item
|
||||||
|
rs.SetMark(1, false)
|
||||||
|
assert.Len(t, rs.MarkedItems(), 3)
|
||||||
|
|
||||||
|
// Verify the correct items are marked
|
||||||
|
markedItems := rs.MarkedItems()
|
||||||
|
expectedIndices := []int{0, 2, 3}
|
||||||
|
for i, expected := range expectedIndices {
|
||||||
|
assert.Equal(t, expected, markedItems[i].Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmark all remaining items
|
||||||
|
rs.SetMark(0, false)
|
||||||
|
rs.SetMark(2, false)
|
||||||
|
rs.SetMark(3, false)
|
||||||
|
assert.Len(t, rs.MarkedItems(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("changing items clears all marked items", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark all items
|
||||||
|
rs.SetMark(0, true)
|
||||||
|
rs.SetMark(1, true)
|
||||||
|
rs.SetMark(2, true)
|
||||||
|
assert.Len(t, rs.MarkedItems(), 3)
|
||||||
|
|
||||||
|
// Call SetItems with new items
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "newitem1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "newitem2"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// All marks should be cleared
|
||||||
|
assert.Len(t, rs.MarkedItems(), 0)
|
||||||
|
|
||||||
|
// Verify none of the new items are marked
|
||||||
|
assert.False(t, rs.Marked(0))
|
||||||
|
assert.False(t, rs.Marked(1))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("hidden items are excluded from MarkedItems", func(t *testing.T) {
|
||||||
|
rs := &ResultSet{}
|
||||||
|
rs.SetItems([]Item{
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item1"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item2"}},
|
||||||
|
{"id": &types.AttributeValueMemberS{Value: "item3"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark all items
|
||||||
|
rs.SetMark(0, true)
|
||||||
|
rs.SetMark(1, true)
|
||||||
|
rs.SetMark(2, true)
|
||||||
|
|
||||||
|
// Hide the second item
|
||||||
|
rs.SetHidden(1, true)
|
||||||
|
|
||||||
|
markedItems := rs.MarkedItems()
|
||||||
|
assert.Len(t, markedItems, 2)
|
||||||
|
|
||||||
|
// Verify only items 0 and 2 are in the marked items
|
||||||
|
assert.Equal(t, 0, markedItems[0].Index)
|
||||||
|
assert.Equal(t, 2, markedItems[1].Index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
"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/models"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestModExpr_Query(t *testing.T) {
|
func TestModExpr_Query(t *testing.T) {
|
||||||
|
|
@ -502,6 +502,9 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
||||||
{expr: `alpha^="al"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
{expr: `alpha^="al"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
{expr: `alpha="foobar"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
{expr: `alpha="foobar"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||||
{expr: `alpha^="need-something"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
{expr: `alpha^="need-something"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||||
|
{expr: `""=""`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{expr: `"abc"="abc"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{expr: `""="abc"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
{expr: "three > 4", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
{expr: "three > 4", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ func Default() *KeyBindings {
|
||||||
},
|
},
|
||||||
View: &ViewKeyBindings{
|
View: &ViewKeyBindings{
|
||||||
Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")),
|
Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")),
|
||||||
ToggleMarkedItems: key.NewBinding(key.WithKeys("M"), key.WithHelp("M", "toggle marged items")),
|
|
||||||
CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")),
|
CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")),
|
||||||
CopyTableToClipboard: key.NewBinding(key.WithKeys("C"), key.WithHelp("C", "copy table to clipboard")),
|
CopyTableToClipboard: key.NewBinding(key.WithKeys("C"), key.WithHelp("C", "copy table to clipboard")),
|
||||||
Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")),
|
Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")),
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ type TableKeyBinding struct {
|
||||||
|
|
||||||
type ViewKeyBindings struct {
|
type ViewKeyBindings struct {
|
||||||
Mark key.Binding `keymap:"mark"`
|
Mark key.Binding `keymap:"mark"`
|
||||||
ToggleMarkedItems key.Binding `keymap:"toggle-marked-items"`
|
|
||||||
CopyItemToClipboard key.Binding `keymap:"copy-item-to-clipboard"`
|
CopyItemToClipboard key.Binding `keymap:"copy-item-to-clipboard"`
|
||||||
CopyTableToClipboard key.Binding `keymap:"copy-table-to-clipboard"`
|
CopyTableToClipboard key.Binding `keymap:"copy-table-to-clipboard"`
|
||||||
Rescan key.Binding `keymap:"rescan"`
|
Rescan key.Binding `keymap:"rescan"`
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
"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"
|
||||||
|
|
@ -20,8 +23,6 @@ import (
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
bus "github.com/lmika/events"
|
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -125,8 +126,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||||
return m, events.SetTeaMessage(m.tableWriteController.ToggleMark(idx))
|
return m, events.SetTeaMessage(m.tableWriteController.ToggleMark(idx))
|
||||||
}
|
}
|
||||||
case key.Matches(msg, m.keyMap.ToggleMarkedItems):
|
|
||||||
return m, events.SetTeaMessage(m.tableReadController.Mark(controllers.MarkOpToggle, ""))
|
|
||||||
case key.Matches(msg, m.keyMap.CopyItemToClipboard):
|
case key.Matches(msg, m.keyMap.CopyItemToClipboard):
|
||||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||||
return m, events.SetTeaMessage(m.tableReadController.CopyItemToClipboard(idx))
|
return m, events.SetTeaMessage(m.tableReadController.CopyItemToClipboard(idx))
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ builds:
|
||||||
binary: dynamo-browse
|
binary: dynamo-browse
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: zip
|
- id: tgz
|
||||||
wrap_in_directory: true
|
wrap_in_directory: false
|
||||||
formats:
|
formats:
|
||||||
- tar.gz
|
- tar.gz
|
||||||
|
|
||||||
|
|
@ -21,23 +21,29 @@ release:
|
||||||
owner: cmd
|
owner: cmd
|
||||||
name: dynamo-browse
|
name: dynamo-browse
|
||||||
ids:
|
ids:
|
||||||
- zip
|
- tgz
|
||||||
|
|
||||||
#homebrew_casks:
|
homebrew_casks:
|
||||||
# - name: dynamo-browse
|
- name: dynamo-browse
|
||||||
# repository:
|
repository:
|
||||||
# owner: casks
|
owner: casks
|
||||||
# name: dynamo-browse
|
name: dynamo-browse
|
||||||
# git:
|
git:
|
||||||
# url: 'ssh://forgejo@lmika.dev:casks/dynamo-browse.git'
|
url: 'forgejo@lmika.dev:casks/dynamo-browse.git'
|
||||||
# private_key: "{{ .Env.HOMEBREW_TAP_PRIVATE_KEY }}"
|
private_key: "{{ .Env.HOMEBREW_TAP_PRIVATE_KEY }}"
|
||||||
# directory: Casks
|
directory: Casks
|
||||||
# homepage: https://dynamo-browse.lmika.dev/
|
homepage: https://dynamo-browse.lmika.dev/
|
||||||
# description: TUI tools for working with DynamoDB
|
description: TUI tools for working with DynamoDB
|
||||||
# license: MIT
|
license: MIT
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums-macos.txt'
|
name_template: 'checksums-macos.txt'
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
version_template: "{{ .Tag }}-next"
|
version_template: "{{ .Tag }}-next"
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://lmika.dev/api/v1
|
||||||
|
download: https://lmika.dev
|
||||||
|
# set to true if you use a self-signed certificate
|
||||||
|
skip_tls_verify: false
|
||||||
Loading…
Reference in a new issue