issue-10: copy item to clipboard
Added key binding to copy selected, or marked, items to clipboard.
This commit is contained in:
parent
f0bd3022cc
commit
90ec88d360
11 changed files with 236 additions and 86 deletions
|
|
@ -3,34 +3,47 @@ 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
|
||||
tableName string
|
||||
tableService TableReadService
|
||||
workspaceService *workspaces.ViewSnapshotService
|
||||
itemRendererService *itemrenderer.Service
|
||||
tableName string
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
state *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,
|
||||
tableName: tableName,
|
||||
mutex: new(sync.Mutex),
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
16
internal/dynamo-browse/controllers/utils.go
Normal file
16
internal/dynamo-browse/controllers/utils.go
Normal 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])
|
||||
}
|
||||
62
internal/dynamo-browse/services/itemrenderer/service.go
Normal file
62
internal/dynamo-browse/services/itemrenderer/service.go
Normal 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
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package itemrenderer
|
||||
|
||||
type StyleRenderer interface {
|
||||
Render(str string) string
|
||||
}
|
||||
|
||||
type plainTextStyleRenderer struct{}
|
||||
|
||||
func (plainTextStyleRenderer) Render(str string) string {
|
||||
return str
|
||||
}
|
||||
|
|
@ -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 "?":
|
||||
|
|
|
|||
|
|
@ -1,48 +1,34 @@
|
|||
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
|
||||
w, h int
|
||||
ready bool
|
||||
frameTitle frame.FrameTitle
|
||||
viewport viewport.Model
|
||||
itemRendererService *itemrenderer.Service
|
||||
w, h int
|
||||
|
||||
// model state
|
||||
currentResultSet *models.ResultSet
|
||||
selectedItem models.Item
|
||||
}
|
||||
|
||||
func New(uiStyles styles.Styles) *Model {
|
||||
func New(itemRendererService *itemrenderer.Service, uiStyles styles.Styles) *Model {
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, uiStyles.Frames),
|
||||
viewport: viewport.New(100, 100),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue