Building the main model

This commit is contained in:
Leon Mika 2022-03-27 22:23:28 +00:00 committed by GitHub
parent 6ab8a3ef44
commit 7a5584cf9a
3 changed files with 348 additions and 298 deletions

View file

@ -21,9 +21,6 @@ import (
"github.com/lmika/awstools/internal/dynamo-browse/services/tables" "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"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/modal" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/modal"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
@ -71,21 +68,27 @@ func main() {
"dup": tableWriteController.Duplicate(), "dup": tableWriteController.Duplicate(),
}) })
uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) _ = uiDispatcher
_ = commandController
// uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController)
// TEMP // TEMP
_ = uiModel // _ = uiModel
// END TEMP // END TEMP
var model tea.Model = statusandprompt.New( /*
layout.NewVBox( var model tea.Model = statusandprompt.New(
layout.LastChildFixedAt(11), layout.NewVBox(
dynamotableview.New(tableReadController), layout.LastChildFixedAt(11),
dynamoitemview.New(), dynamotableview.New(tableReadController),
), dynamoitemview.New(),
"Hello world", ),
) "Hello world",
model = layout.FullScreen(tableselect.New(model)) )
model = layout.FullScreen(tableselect.New(model))
*/
model := ui.NewModel(tableReadController)
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang. // Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
lipgloss.HasDarkBackground() lipgloss.HasDarkBackground()

View file

@ -1,304 +1,47 @@
package ui package ui
import ( 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" 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/events"
"github.com/lmika/awstools/internal/common/ui/uimodels"
"github.com/lmika/awstools/internal/dynamo-browse/controllers" "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"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
) )
var ( type Model struct {
activeHeaderStyle = lipgloss.NewStyle(). tableReadController *controllers.TableReadController
Bold(true).
Foreground(lipgloss.Color("#ffffff")).
Background(lipgloss.Color("#4479ff"))
inactiveHeaderStyle = lipgloss.NewStyle(). root tea.Model
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 NewModel(dispatcher *dispatcher.Dispatcher, commandController *commandctrl.CommandController, tableReadController *controllers.TableReadController, tableWriteController *controllers.TableWriteController) tea.Model { func NewModel(rc *controllers.TableReadController) Model {
tbl := table.New([]string{"pk", "sk"}, 100, 20) dtv := dynamotableview.New(rc)
rows := make([]table.Row, 0) div := dynamoitemview.New()
tbl.SetRows(rows)
textInput := textinput.New() m := statusandprompt.New(
layout.NewVBox(layout.LastChildFixedAt(11), dtv, div),
"Hello world",
)
root := layout.FullScreen(tableselect.New(m))
model := uiModel{ return Model{
table: tbl, tableReadController: rc,
message: "Press s to scan", root: root,
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 { func (m Model) Init() tea.Cmd {
//m.invokeOperation(context.Background(), m.tableReadController.Scan()) return m.tableReadController.Scan()
return nil
} }
/* func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *uiModel) updateTable() { var cmd tea.Cmd
if !m.ready { m.root, cmd = m.root.Update(msg)
return return m, cmd
}
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 Model) View() string {
return m.root.View()
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

@ -0,0 +1,304 @@
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/events"
"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
}