table-select: cleanup

This commit is contained in:
Leon Mika 2022-03-28 21:36:47 +11:00
parent 6f323fa4cf
commit 9709e6aed1
12 changed files with 68 additions and 639 deletions

View file

@ -4,25 +4,18 @@ import (
"context"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/common/ui/commandctrl"
"github.com/lmika/awstools/internal/common/ui/dispatcher"
"github.com/lmika/awstools/internal/common/ui/uimodels"
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
"github.com/lmika/awstools/internal/dynamo-browse/ui"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/modal"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
"github.com/lmika/gopkgs/cli"
"log"
"os"
)
func main() {
@ -32,10 +25,7 @@ func main() {
ctx := context.Background()
// TEMP
cfg, err := config.LoadDefaultConfig(ctx)
// END TEMP
if err != nil {
cli.Fatalf("cannot load AWS config: %v", err)
}
@ -52,70 +42,22 @@ func main() {
tableService := tables.NewService(dynamoProvider)
loopback := &msgLoopback{}
uiDispatcher := dispatcher.NewDispatcher(loopback)
tableReadController := controllers.NewTableReadController(tableService, *flagTable)
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, *flagTable)
_ = tableWriteController
commandController := commandctrl.NewCommandController(map[string]uimodels.Operation{
// "scan": tableReadController.Scan(),
"rw": tableWriteController.ToggleReadWrite(),
"dup": tableWriteController.Duplicate(),
commandController := commandctrl.NewCommandController(map[string]commandctrl.Command{
"q": commandctrl.NoArgCommand(tea.Quit),
//"rw": tableWriteController.ToggleReadWrite(),
//"dup": tableWriteController.Duplicate(),
})
_ = uiDispatcher
_ = commandController
// uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController)
// TEMP
// _ = uiModel
// END TEMP
/*
var model tea.Model = statusandprompt.New(
layout.NewVBox(
layout.LastChildFixedAt(11),
dynamotableview.New(tableReadController),
dynamoitemview.New(),
),
"Hello world",
)
model = layout.FullScreen(tableselect.New(model))
*/
model := ui.NewModel(tableReadController)
model := ui.NewModel(tableReadController, commandController)
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
lipgloss.HasDarkBackground()
//frameSet := frameset.New([]frameset.Frame{
// {
// Header: "Frame 1",
// Model: newTestModel("this is model 1"),
// },
// {
// Header: "Frame 2",
// Model: newTestModel("this is model 2"),
// },
//})
//
//modal := modal.New(frameSet)
p := tea.NewProgram(model, tea.WithAltScreen())
//loopback.program = p
// TEMP -- profiling
//cf, err := os.Create("trace.out")
//if err != nil {
// log.Fatal("could not create CPU profile: ", err)
//}
//defer cf.Close() // error handling omitted for example
//if err := trace.Start(cf); err != nil {
// log.Fatal("could not start CPU profile: ", err)
//}
//defer trace.Stop()
// END TEMP
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
@ -131,37 +73,11 @@ func main() {
}
}
type msgLoopback struct {
program *tea.Program
}
func (m *msgLoopback) Send(msg tea.Msg) {
m.program.Send(msg)
}
func newTestModel(descr string) tea.Model {
return teamodels.TestModel{
Message: descr,
OnKeyPressed: func(k string) tea.Cmd {
log.Println("got key press: " + k)
if k == "enter" {
return tea.Batch(
tableselect.IndicateLoadingTables(),
tea.Sequentially(
func() tea.Msg {
<-time.After(2 * time.Second)
return nil
},
tableselect.ShowTableSelect(func(n string) tea.Cmd {
// return statusandprompt.SetStatus("New table = " + n)
return nil
}),
),
)
} else if k == "k" {
return modal.PopMode
}
return nil
},
}
}
//
//type msgLoopback struct {
// program *tea.Program
//}
//
//func (m *msgLoopback) Send(msg tea.Msg) {
// m.program.Send(msg)
//}

View file

@ -1,39 +1,36 @@
package commandctrl
import (
"context"
tea "github.com/charmbracelet/bubbletea"
"strings"
"github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/common/ui/uimodels"
"github.com/lmika/shellwords"
"github.com/pkg/errors"
)
type CommandController struct {
commands map[string]uimodels.Operation
commands map[string]Command
}
func NewCommandController(commands map[string]uimodels.Operation) *CommandController {
func NewCommandController(commands map[string]Command) *CommandController {
return &CommandController{
commands: commands,
}
}
func (c *CommandController) Prompt() uimodels.Operation {
return uimodels.OperationFn(func(ctx context.Context) error {
uiCtx := uimodels.Ctx(ctx)
uiCtx.Send(events.PromptForInputMsg{
func (c *CommandController) Prompt() tea.Cmd {
return func() tea.Msg {
return events.PromptForInputMsg{
Prompt: ":",
// OnDone: c.Execute(),
})
return nil
})
OnDone: func(value string) tea.Cmd {
return c.Execute(value)
},
}
}
}
func (c *CommandController) Execute() uimodels.Operation {
return uimodels.OperationFn(func(ctx context.Context) error {
input := strings.TrimSpace(uimodels.PromptValue(ctx))
func (c *CommandController) Execute(commandInput string) tea.Cmd {
input := strings.TrimSpace(commandInput)
if input == "" {
return nil
}
@ -41,9 +38,8 @@ func (c *CommandController) Execute() uimodels.Operation {
tokens := shellwords.Split(input)
command, ok := c.commands[tokens[0]]
if !ok {
return errors.New("no such command: " + tokens[0])
return events.SetStatus("no such command: " + tokens[0])
}
return command.Execute(WithCommandArgs(ctx, tokens[1:]))
})
return command(tokens)
}

View file

@ -0,0 +1,11 @@
package commandctrl
import tea "github.com/charmbracelet/bubbletea"
type Command func(args []string) tea.Cmd
func NoArgCommand(cmd tea.Cmd) Command {
return func(args []string) tea.Cmd {
return cmd
}
}

View file

@ -2,8 +2,6 @@ package controllers
import (
"context"
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/models"
@ -52,20 +50,16 @@ func (c *TableReadController) scanTable(name string) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
log.Println("Fetching table info")
tableInfo, err := c.tableService.Describe(ctx, name)
if err != nil {
return events.Error(errors.Wrapf(err, "cannot describe %v", c.tableName))
}
log.Println("Scanning")
resultSet, err := c.tableService.Scan(ctx, tableInfo)
if err != nil {
log.Println("error: ", err)
return events.Error(err)
}
log.Println("Scan done")
return NewResultSet{resultSet}
}
}
@ -74,14 +68,11 @@ func (c *TableReadController) Rescan(resultSet *models.ResultSet) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
log.Println("Scanning")
resultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo)
if err != nil {
log.Println("error: ", err)
return events.Error(err)
}
log.Println("Scan done")
return NewResultSet{resultSet}
}
}

View file

@ -1,7 +0,0 @@
package ui
import tea "github.com/charmbracelet/bubbletea"
type MessagePublisher interface {
Send(msg tea.Msg)
}

View file

@ -2,6 +2,7 @@ package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/commandctrl"
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview"
@ -12,12 +13,13 @@ import (
type Model struct {
tableReadController *controllers.TableReadController
commandController *commandctrl.CommandController
root tea.Model
}
func NewModel(rc *controllers.TableReadController) Model {
dtv := dynamotableview.New(rc)
func NewModel(rc *controllers.TableReadController, cc *commandctrl.CommandController) Model {
dtv := dynamotableview.New(rc, cc)
div := dynamoitemview.New()
m := statusandprompt.New(
@ -28,6 +30,7 @@ func NewModel(rc *controllers.TableReadController) Model {
return Model{
tableReadController: rc,
commandController: cc,
root: root,
}
}

View file

@ -1,303 +0,0 @@
package ui
import (
"context"
"strings"
table "github.com/calyptia/go-bubble-table"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/common/ui/commandctrl"
"github.com/lmika/awstools/internal/common/ui/dispatcher"
"github.com/lmika/awstools/internal/common/ui/uimodels"
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
)
var (
activeHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#ffffff")).
Background(lipgloss.Color("#4479ff"))
inactiveHeaderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#d1d1d1"))
)
type uiModel struct {
table table.Model
viewport viewport.Model
// TEMP
tableSelect tea.Model
tableWidth, tableHeight int
ready bool
state controllers.State
message string
// pendingInput *events.PromptForInput
textInput textinput.Model
dispatcher *dispatcher.Dispatcher
commandController *commandctrl.CommandController
tableReadController *controllers.TableReadController
tableWriteController *controllers.TableWriteController
}
func NewModelOld(dispatcher *dispatcher.Dispatcher, commandController *commandctrl.CommandController, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model {
tbl := table.New([]string{"pk", "sk"}, 100, 20)
rows := make([]table.Row, 0)
tbl.SetRows(rows)
textInput := textinput.New()
model := uiModel{
table: tbl,
message: "Press s to scan",
textInput: textInput,
// TEMP
tableSelect: newSizeWaitModel(func(w, h int) tea.Model {
return newTableSelectModel(w, h)
}),
dispatcher: dispatcher,
commandController: commandController,
tableReadController: tableReadController,
tableWriteController: tableWriteController,
}
return model
}
func (m uiModel) Init() tea.Cmd {
//m.invokeOperation(context.Background(), m.tableReadController.Scan())
return nil
}
/*
func (m *uiModel) updateTable() {
if !m.ready {
return
}
resultSet := m.state.ResultSet
newTbl := table.New(resultSet.Columns, m.tableWidth, m.tableHeight)
newRows := make([]table.Row, len(resultSet.Items))
for i, r := range resultSet.Items {
newRows[i] = itemTableRow{resultSet, r}
}
newTbl.SetRows(newRows)
m.table = newTbl
}
func (m *uiModel) selectedItem() (itemTableRow, bool) {
resultSet := m.state.ResultSet
if m.ready && resultSet != nil && len(resultSet.Items) > 0 {
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
if ok {
return selectedItem, true
}
}
return itemTableRow{}, false
}
func (m *uiModel) updateViewportToSelectedMessage() {
selectedItem, ok := m.selectedItem()
if !ok {
m.viewport.SetContent("(no row selected)")
return
}
viewportContent := &strings.Builder{}
tabWriter := tabwriter.NewWriter(viewportContent, 0, 1, 1, ' ', 0)
for _, colName := range selectedItem.resultSet.Columns {
switch colVal := selectedItem.item[colName].(type) {
case nil:
break
case *types.AttributeValueMemberS:
fmt.Fprintf(tabWriter, "%v\tS\t%s\n", colName, colVal.Value)
case *types.AttributeValueMemberN:
fmt.Fprintf(tabWriter, "%v\tN\t%s\n", colName, colVal.Value)
default:
fmt.Fprintf(tabWriter, "%v\t?\t%s\n", colName, "(other)")
}
}
tabWriter.Flush()
m.viewport.SetContent(viewportContent.String())
}
*/
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var textInputCommands tea.Cmd
switch msg := msg.(type) {
// Local events
case controllers.NewResultSet:
m.state.ResultSet = msg.ResultSet
// m.updateTable()
// m.updateViewportToSelectedMessage()
case controllers.SetReadWrite:
m.state.InReadWriteMode = msg.NewValue
// Shared events
// case events.Error:
// m.message = "Error: " + msg.Error()
// case events.Message:
// m.message = string(msg)
// case events.PromptForInput:
// m.textInput.Prompt = msg.Prompt
// m.textInput.Focus()
// m.textInput.SetValue("")
// m.pendingInput = &msg
// Tea events
case tea.WindowSizeMsg:
fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView())
viewportHeight := msg.Height / 2 // TODO: make this dynamic
if viewportHeight > 15 {
viewportHeight = 15
}
tableHeight := msg.Height - fixedViewsHeight - viewportHeight
if !m.ready {
m.viewport = viewport.New(msg.Width, viewportHeight)
m.viewport.SetContent("(no message selected)")
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight
}
m.tableWidth, m.tableHeight = msg.Width, tableHeight
m.table.SetSize(m.tableWidth, m.tableHeight)
case tea.KeyMsg:
// If text input in focus, allow that to accept input messages
// if m.pendingInput != nil {
// switch msg.String() {
// case "ctrl+c", "esc":
// m.pendingInput = nil
// case "enter":
// m.invokeOperation(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
// m.pendingInput = nil
// default:
// m.textInput, textInputCommands = m.textInput.Update(msg)
// }
// break
// }
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "i":
m.table.GoUp()
// m.updateViewportToSelectedMessage()
case "down", "k":
m.table.GoDown()
// m.updateViewportToSelectedMessage()
// TODO: these should be moved somewhere else
case ":":
m.invokeOperation(context.Background(), m.commandController.Prompt())
// case "s":
// m.invokeOperation(context.Background(), m.tableReadController.Scan())
case "D":
m.invokeOperation(context.Background(), m.tableWriteController.Delete())
}
default:
m.textInput, textInputCommands = m.textInput.Update(msg)
}
updatedTable, tableMsgs := m.table.Update(msg)
updatedViewport, viewportMsgs := m.viewport.Update(msg)
updatedTableSelectModel, tableSelectMsgs := m.tableSelect.Update(msg)
m.table = updatedTable
m.viewport = updatedViewport
m.tableSelect = updatedTableSelectModel
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs, tableSelectMsgs)
}
func (m uiModel) invokeOperation(ctx context.Context, op uimodels.Operation) {
state := m.state
// if selectedItem, ok := m.selectedItem(); ok {
// state.SelectedItem = selectedItem.item
// }
ctx = controllers.ContextWithState(ctx, state)
m.dispatcher.Start(ctx, op)
}
func (m uiModel) View() string {
// TEMP
return m.tableSelect.View()
/*
if !m.ready {
return "Initializing"
}
if m.pendingInput != nil {
return lipgloss.JoinVertical(lipgloss.Top,
m.headerView(),
m.table.View(),
m.splitterView(),
m.viewport.View(),
m.textInput.View(),
)
}
return lipgloss.JoinVertical(lipgloss.Top,
m.headerView(),
m.table.View(),
m.splitterView(),
m.viewport.View(),
m.footerView(),
)
*/
}
func (m uiModel) headerView() string {
var titleText string
if m.state.ResultSet != nil {
titleText = "Table: " + m.state.ResultSet.TableInfo.Name
} else {
titleText = "No table"
}
title := activeHeaderStyle.Render(titleText)
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
}
func (m uiModel) splitterView() string {
title := inactiveHeaderStyle.Render("Item")
line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
}
func (m uiModel) footerView() string {
title := m.message
line := strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View file

@ -1,48 +0,0 @@
package ui
import (
tea "github.com/charmbracelet/bubbletea"
"log"
)
// sizeWaitModel is a model which waits until the first screen size message comes through. It then creates the
// submodel and delegates calls to that model
type sizeWaitModel struct {
constr func(width, height int) tea.Model
model tea.Model
}
func newSizeWaitModel(constr func(width, height int) tea.Model) tea.Model {
return sizeWaitModel{constr: constr}
}
func (s sizeWaitModel) Init() tea.Cmd {
return nil
}
func (s sizeWaitModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m := msg.(type) {
case tea.WindowSizeMsg:
log.Println("got window size message")
if s.model == nil {
log.Println("creating model")
s.model = s.constr(m.Width, m.Height)
s.model.Init()
}
}
var submodelCmds tea.Cmd
if s.model != nil {
log.Println("starting update")
s.model, submodelCmds = s.model.Update(msg)
log.Println("ending update")
}
return s, submodelCmds
}
func (s sizeWaitModel) View() string {
if s.model == nil {
return ""
}
return s.model.View()
}

View file

@ -1,95 +0,0 @@
package ui
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
)
type tableSelectModel struct {
list list.Model
}
func (t tableSelectModel) Init() tea.Cmd {
return nil
}
func (t tableSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
t.list.SetHeight(msg.Height)
t.list.SetWidth(msg.Width)
return t, nil
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case "ctrl+c":
return t, tea.Quit
case "enter":
//i, ok := m.list.SelectedItem().(item)
//if ok {
// m.choice = string(i)
//}
return t, tea.Quit
}
}
var cmd tea.Cmd
t.list, cmd = t.list.Update(msg)
return t, cmd
}
func (t tableSelectModel) View() string {
return t.list.View()
}
func newTableSelectModel(w, h int) tableSelectModel {
tableItems := []tableItem{
{name: "alpha"},
{name: "beta"},
{name: "gamma"},
}
items := toListItems(tableItems)
delegate := list.NewDefaultDelegate()
delegate.ShowDescription = false
return tableSelectModel{
list: list.New(items, delegate, w, h),
}
}
type tableItem struct {
name string
}
func (ti tableItem) FilterValue() string {
return ""
}
func (ti tableItem) Title() string {
return ti.name
}
func (ti tableItem) Description() string {
return "abc"
}
func toListItems[T list.Item](xs []T) []list.Item {
ls := make([]list.Item, len(xs))
for i, x := range xs {
ls[i] = x
}
return ls
}

View file

@ -4,6 +4,7 @@ import (
table "github.com/calyptia/go-bubble-table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/common/ui/commandctrl"
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/models"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
@ -13,6 +14,8 @@ import (
type Model struct {
tableReadControllers *controllers.TableReadController
commandCtrl *commandctrl.CommandController
frameTitle frame.FrameTitle
table table.Model
w, h int
@ -21,7 +24,7 @@ type Model struct {
resultSet *models.ResultSet
}
func New(tableReadControllers *controllers.TableReadController) Model {
func New(tableReadControllers *controllers.TableReadController, commandCtrl *commandctrl.CommandController) Model {
tbl := table.New([]string{"pk", "sk"}, 100, 100)
rows := make([]table.Row, 0)
tbl.SetRows(rows)
@ -30,6 +33,7 @@ func New(tableReadControllers *controllers.TableReadController) Model {
return Model{
tableReadControllers: tableReadControllers,
commandCtrl: commandCtrl,
frameTitle: frameTitle,
table: tbl,
}
@ -58,6 +62,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// TEMP
case "s":
return m, m.tableReadControllers.Rescan(m.resultSet)
case ":":
return m, m.commandCtrl.Prompt()
// END TEMP
case "ctrl+c", "esc":
return m, tea.Quit
}

View file

@ -1,9 +0,0 @@
package teamodels
import tea "github.com/charmbracelet/bubbletea"
// NewModePushed pushes a new mode on the modal stack
type NewModePushed tea.Model
// ModePopped pops a mode from the modal stack
type ModePopped struct{}

View file

@ -1,33 +0,0 @@
package teamodels
import (
tea "github.com/charmbracelet/bubbletea"
)
// TestModel is a model used for testing
type TestModel struct {
Message string
OnKeyPressed func(k string) tea.Cmd
}
func (t TestModel) Init() tea.Cmd {
return nil
}
func (t TestModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
return t, tea.Quit
default:
return t, t.OnKeyPressed(msg.String())
}
}
return t, nil
}
func (t TestModel) View() string {
return t.Message
}