Merge pull request #13 from lmika/feature/issue-10

issue-10: copy item to clipboard
This commit is contained in:
Leon Mika 2022-08-20 10:51:03 +10:00 committed by GitHub
commit 7e87b3e5a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 279 additions and 111 deletions

View file

@ -15,9 +15,11 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/ui"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
"github.com/lmika/gopkgs/cli"
"log"
"net"
@ -66,18 +68,20 @@ func main() {
dynamoClient = dynamodb.NewFromConfig(cfg)
}
uiStyles := styles.DefaultStyles
dynamoProvider := dynamo.NewProvider(dynamoClient)
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
tableService := tables.NewService(dynamoProvider)
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
state := controllers.NewState()
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, *flagTable)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable)
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController)
commandController := commandctrl.NewCommandController()
model := ui.NewModel(tableReadController, tableWriteController, commandController)
model := ui.NewModel(tableReadController, tableWriteController, itemRendererService, commandController)
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
lipgloss.HasDarkBackground()

5
go.mod
View file

@ -25,6 +25,7 @@ require (
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.1
golang.design/x/clipboard v0.6.2
)
require (
@ -61,6 +62,10 @@ require (
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.design/x/clipboard v0.6.2 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
google.golang.org/appengine v1.6.7 // indirect

36
go.sum
View file

@ -1,3 +1,4 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ=
@ -146,29 +147,64 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw=
golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

View file

@ -3,32 +3,45 @@ package controllers
import (
"context"
"encoding/csv"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
"github.com/pkg/errors"
"golang.design/x/clipboard"
"log"
"os"
"strings"
"sync"
)
type TableReadController struct {
tableService TableReadService
workspaceService *workspaces.ViewSnapshotService
itemRendererService *itemrenderer.Service
tableName string
// state
mutex *sync.Mutex
state *State
clipboardInit bool
}
func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.ViewSnapshotService, tableName string) *TableReadController {
func NewTableReadController(
state *State,
tableService TableReadService,
workspaceService *workspaces.ViewSnapshotService,
itemRendererService *itemrenderer.Service,
tableName string,
) *TableReadController {
return &TableReadController{
state: state,
tableService: tableService,
workspaceService: workspaceService,
itemRendererService: itemRendererService,
tableName: tableName,
mutex: new(sync.Mutex),
}
@ -254,3 +267,40 @@ func (c *TableReadController) ViewBack() tea.Msg {
tableInfo.Name, viewSnapshot.Query, viewSnapshot.Filter)
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
}
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
if err := c.initClipboard(); err != nil {
return events.Error(err)
}
itemCount := 0
c.state.withResultSet(func(resultSet *models.ResultSet) {
sb := new(strings.Builder)
_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
if sb.Len() > 0 {
fmt.Fprintln(sb, "---")
}
c.itemRendererService.RenderItem(sb, resultSet.Items()[idx], resultSet, true)
itemCount += 1
return nil
})
clipboard.Write(clipboard.FmtText, []byte(sb.String()))
})
return events.SetStatus(applyToN("", itemCount, "item", "items", " copied to clipboard"))
}
func (c *TableReadController) initClipboard() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.clipboardInit {
return nil
}
if err := clipboard.Init(); err != nil {
return errors.Wrap(err, "unable to enable clipboard")
}
c.clipboardInit = true
return nil
}

View file

@ -8,6 +8,7 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
"github.com/lmika/audax/test/testdynamo"
@ -22,12 +23,13 @@ func TestTableReadController_InitTable(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
t.Run("should prompt for table if no table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
event := readController.Init()
@ -35,7 +37,7 @@ func TestTableReadController_InitTable(t *testing.T) {
})
t.Run("should scan table if table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
event := readController.Init()
@ -48,10 +50,11 @@ func TestTableReadController_ListTables(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "")
t.Run("returns a list of tables", func(t *testing.T) {
event := readController.ListTables().(controllers.PromptForTableMsg)
@ -72,11 +75,12 @@ func TestTableReadController_Rescan(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "bravo-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "bravo-table")
t.Run("should perform a rescan", func(t *testing.T) {
invokeCommand(t, readController.Init())
@ -109,10 +113,11 @@ func TestTableReadController_ExportCSV(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "bravo-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table")
t.Run("should export result set to CSV file", func(t *testing.T) {
tempFile := tempFile(t)
@ -133,7 +138,7 @@ func TestTableReadController_ExportCSV(t *testing.T) {
t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "non-existant-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table")
invokeCommandExpectingError(t, readController.Init())
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
@ -147,10 +152,11 @@ func TestTableReadController_Query(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "bravo-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table")
t.Run("should run scan with filter based on user query", func(t *testing.T) {
tempFile := tempFile(t)
@ -170,7 +176,7 @@ func TestTableReadController_Query(t *testing.T) {
t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t)
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "non-existant-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table")
invokeCommandExpectingError(t, readController.Init())
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))

View file

@ -107,7 +107,7 @@ func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Msg
Prompt: "string value: ",
OnDone: func(value string) tea.Msg {
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err := attr.setAt(item, &types.AttributeValueMemberS{Value: value}); err != nil {
return err
}
@ -126,25 +126,12 @@ func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Msg
}
}
func (twc *TableWriteController) applyToItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {
for _, mi := range markedItems {
if err := applyFn(mi.Index, mi.Item); err != nil {
return err
}
}
return nil
}
return applyFn(selectedIndex, rs.Items()[selectedIndex])
}
func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Msg {
return events.PromptForInputMsg{
Prompt: "number value: ",
OnDone: func(value string) tea.Msg {
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err := attr.setAt(item, &types.AttributeValueMemberN{Value: value}); err != nil {
return err
}
@ -173,7 +160,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Msg {
}
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err := attr.setAt(item, &types.AttributeValueMemberBOOL{Value: b}); err != nil {
return err
}
@ -194,7 +181,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Msg {
func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Msg {
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err := attr.setAt(item, &types.AttributeValueMemberNULL{Value: true}); err != nil {
return err
}

View file

@ -7,6 +7,7 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
"github.com/lmika/audax/test/testdynamo"
@ -17,6 +18,7 @@ import (
func TestTableWriteController_NewItem(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should add an item with pk and sk set at the end of the result set", func(t *testing.T) {
client := testdynamo.SetupTestTable(t, testData)
@ -25,7 +27,7 @@ func TestTableWriteController_NewItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -50,6 +52,7 @@ func TestTableWriteController_NewItem(t *testing.T) {
func TestTableWriteController_SetAttributeValue(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should preserve the type of the field if unspecified", func(t *testing.T) {
@ -88,7 +91,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -108,7 +111,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -162,7 +165,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
for _, scenario := range scenarios {
t.Run(fmt.Sprintf("should change the value of a field to type %v", scenario.attrType), func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -183,7 +186,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
t.Run(fmt.Sprintf("should change value of nested field to type %v", scenario.attrType), func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -215,13 +218,14 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
t.Run("should delete top level attribute", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -237,7 +241,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
t.Run("should delete attribute of map", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -260,6 +264,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
func TestTableWriteController_PutItem(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should put the selected item if dirty", func(t *testing.T) {
client := testdynamo.SetupTestTable(t, testData)
@ -268,7 +273,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -295,7 +300,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -326,7 +331,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -342,6 +347,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
func TestTableWriteController_PutItems(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should put all dirty items if none are marked", func(t *testing.T) {
client := testdynamo.SetupTestTable(t, testData)
@ -350,7 +356,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -378,7 +384,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -414,7 +420,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
@ -447,6 +453,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
func TestTableWriteController_TouchItem(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should put the selected item if unmodified", func(t *testing.T) {
client := testdynamo.SetupTestTable(t, testData)
@ -455,7 +462,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -481,7 +488,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -499,6 +506,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
func TestTableWriteController_NoisyTouchItem(t *testing.T) {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
t.Run("should delete and put the selected item if unmodified", func(t *testing.T) {
client := testdynamo.SetupTestTable(t, testData)
@ -507,7 +515,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table
@ -533,7 +541,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
service := tables.NewService(provider)
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
// Read the table

View file

@ -0,0 +1,16 @@
package controllers
import "github.com/lmika/audax/internal/dynamo-browse/models"
func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {
for _, mi := range markedItems {
if err := applyFn(mi.Index, mi.Item); err != nil {
return err
}
}
return nil
}
return applyFn(selectedIndex, rs.Items()[selectedIndex])
}

View file

@ -0,0 +1,62 @@
package itemrenderer
import (
"fmt"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
"io"
"text/tabwriter"
)
type Service struct {
styles styleRenderer
}
func NewService(fileTypeStyle StyleRenderer, metaInfoStyle StyleRenderer) *Service {
return &Service{
styles: styleRenderer{
fileTypeRenderer: fileTypeStyle,
metaInfoRenderer: metaInfoStyle,
},
}
}
func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.ResultSet, plainText bool) {
styles := s.styles
if plainText {
styles = styleRenderer{plainTextStyleRenderer{}, plainTextStyleRenderer{}}
}
tabWriter := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0)
seenColumns := make(map[string]struct{})
for _, colName := range resultSet.Columns() {
seenColumns[colName] = struct{}{}
if r := item.Renderer(colName); r != nil {
s.renderItem(tabWriter, "", colName, r, styles)
}
}
for k, _ := range item {
if _, seen := seenColumns[k]; !seen {
if r := item.Renderer(k); r != nil {
s.renderItem(tabWriter, "", k, 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()))
if subitems := r.SubItems(); len(subitems) > 0 {
for _, si := range subitems {
m.renderItem(w, prefix+" ", si.Key, si.Value, sr)
}
}
}
type styleRenderer struct {
fileTypeRenderer StyleRenderer
metaInfoRenderer StyleRenderer
}

View file

@ -0,0 +1,15 @@
package itemrenderer
type StyleRenderer interface {
Render(str string) string
}
func PlainTextRenderer() StyleRenderer {
return plainTextStyleRenderer{}
}
type plainTextStyleRenderer struct{}
func (plainTextStyleRenderer) Render(str string) string {
return str
}

View file

@ -6,6 +6,7 @@ import (
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dialogprompt"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemedit"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview"
@ -31,11 +32,16 @@ type Model struct {
itemView *dynamoitemview.Model
}
func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteController, cc *commandctrl.CommandController) Model {
func NewModel(
rc *controllers.TableReadController,
wc *controllers.TableWriteController,
itemRendererService *itemrenderer.Service,
cc *commandctrl.CommandController,
) Model {
uiStyles := styles.DefaultStyles
dtv := dynamotableview.New(uiStyles)
div := dynamoitemview.New(uiStyles)
div := dynamoitemview.New(itemRendererService, uiStyles)
mainView := layout.NewVBox(layout.LastChildFixedAt(13), dtv, div)
itemEdit := dynamoitemedit.NewModel(mainView)
@ -143,6 +149,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
return m, func() tea.Msg { return m.tableWriteController.ToggleMark(idx) }
}
case "c":
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
return m, func() tea.Msg { return m.tableReadController.CopyItemToClipboard(idx) }
}
case "R":
return m, m.tableReadController.Rescan
case "?":

View file

@ -1,37 +1,22 @@
package dynamoitemview
import (
"fmt"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
"io"
"strings"
"text/tabwriter"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
)
var (
activeHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#ffffff")).
Background(lipgloss.Color("#4479ff"))
fieldTypeStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"})
metaInfoStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888"))
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
"strings"
)
type Model struct {
ready bool
frameTitle frame.FrameTitle
viewport viewport.Model
itemRendererService *itemrenderer.Service
w, h int
// model state
@ -39,8 +24,9 @@ type Model struct {
selectedItem models.Item
}
func New(uiStyles styles.Styles) *Model {
func New(itemRendererService *itemrenderer.Service, uiStyles styles.Styles) *Model {
return &Model{
itemRendererService: itemRendererService,
frameTitle: frame.NewFrameTitle("Item", false, uiStyles.Frames),
viewport: viewport.New(100, 100),
}
@ -89,35 +75,8 @@ func (m *Model) updateViewportToSelectedMessage() {
}
viewportContent := &strings.Builder{}
tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
seenColumns := make(map[string]struct{})
for _, colName := range m.currentResultSet.Columns() {
seenColumns[colName] = struct{}{}
if r := m.selectedItem.Renderer(colName); r != nil {
m.renderItem(tabWriter, "", colName, r)
}
}
for k, _ := range m.selectedItem {
if _, seen := seenColumns[k]; !seen {
if r := m.selectedItem.Renderer(k); r != nil {
m.renderItem(tabWriter, "", k, r)
}
}
}
tabWriter.Flush()
m.itemRendererService.RenderItem(viewportContent, m.selectedItem, m.currentResultSet, false)
m.viewport.Width = m.w
m.viewport.Height = m.h - m.frameTitle.HeaderHeight()
m.viewport.SetContent(viewportContent.String())
}
func (m *Model) renderItem(w io.Writer, prefix string, name string, r itemrender.Renderer) {
fmt.Fprintf(w, "%s%v\t%s\t%s%s\n",
prefix, name, fieldTypeStyle.Render(r.TypeName()), r.StringValue(), metaInfoStyle.Render(r.MetaInfo()))
if subitems := r.SubItems(); len(subitems) > 0 {
for _, si := range subitems {
m.renderItem(w, prefix+" ", si.Key, si.Value)
}
}
}

View file

@ -7,11 +7,21 @@ import (
)
type Styles struct {
ItemView ItemViewStyle
Frames frame.Style
StatusAndPrompt statusandprompt.Style
}
type ItemViewStyle struct {
FieldType lipgloss.Style
MetaInfo lipgloss.Style
}
var DefaultStyles = Styles{
ItemView: ItemViewStyle{
FieldType: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"}),
MetaInfo: lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")),
},
Frames: frame.Style{
ActiveTitle: lipgloss.NewStyle().
Bold(true).