diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index f43c724..129702e 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -21,9 +21,6 @@ import ( "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/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/statusandprompt" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" @@ -71,21 +68,27 @@ func main() { "dup": tableWriteController.Duplicate(), }) - uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) + _ = uiDispatcher + _ = commandController + + // uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController) // TEMP - _ = uiModel + // _ = 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)) + /* + 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) // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. lipgloss.HasDarkBackground() diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 4d86cc1..d6c9722 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,304 +1,47 @@ 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" + "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 ( - activeHeaderStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#ffffff")). - Background(lipgloss.Color("#4479ff")) +type Model struct { + tableReadController *controllers.TableReadController - 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 + root tea.Model } -func NewModel(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) +func NewModel(rc *controllers.TableReadController) Model { + dtv := dynamotableview.New(rc) + div := dynamoitemview.New() - textInput := textinput.New() + m := statusandprompt.New( + layout.NewVBox(layout.LastChildFixedAt(11), dtv, div), + "Hello world", + ) + root := layout.FullScreen(tableselect.New(m)) - 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{ + tableReadController: rc, + root: root, } - - return model } -func (m uiModel) Init() tea.Cmd { - //m.invokeOperation(context.Background(), m.tableReadController.Scan()) - return nil +func (m Model) Init() tea.Cmd { + return m.tableReadController.Scan() } -/* -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 Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.root, cmd = m.root.Update(msg) + return m, cmd } - - -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 +func (m Model) View() string { + return m.root.View() } diff --git a/internal/dynamo-browse/ui/modelold.go b/internal/dynamo-browse/ui/modelold.go new file mode 100644 index 0000000..7f62318 --- /dev/null +++ b/internal/dynamo-browse/ui/modelold.go @@ -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 +}