Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions bubbletea/bubbletea.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import (

// Model represents the component that wraps the `bubbletea` Model interface.
type Model interface {
// tea.Model is the base interface.
tea.Model
// Init initializes the model and returns the next tea command.
Init() tea.Cmd

// Update updates the model and returns the same model and the next tea command.
Update(msg tea.Msg) (Model, tea.Cmd)

// View returns the model view representation.
View() string

// Run runs and returns its result.
Run(modelResult any) (any, error)
Expand All @@ -35,12 +41,14 @@ func (b BubbleTea) Init() tea.Cmd {

// Update is the `BubbleTea` method required for implementing the `Model` interface.
func (b BubbleTea) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return b.currentModel.Update(msg)
model, cmd := b.currentModel.Update(msg)
b.currentModel = model
return b, cmd
}

// View is the `BubbleTea` method required for implementing the `Model` interface.
func (b BubbleTea) View() string {
return b.baseStyle.Render(b.currentModel.View()) + "\n"
return b.currentModel.View()
}

// Run runs the `BubbleTea` component and returns its result.
Expand Down
51 changes: 32 additions & 19 deletions bubbletea/bubbletea_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestBubbleTeaUpdate(t *testing.T) {
subTestName string
initialBubbletea tea.Model
updateParams tea.Msg
expectedModel *ChoicesModel
expectedModel *BubbleTea
expectedCmd tea.Cmd
}{
{
Expand All @@ -24,7 +24,8 @@ func TestBubbleTeaUpdate(t *testing.T) {
BaseStyle: NewBaseStyle(),
}),
updateParams: nil,
expectedModel: NewChoicesModel(&ChoicesModelParams{
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: (tea.Cmd)(nil),
Expand All @@ -38,7 +39,8 @@ func TestBubbleTeaUpdate(t *testing.T) {
updateParams: tea.KeyMsg{
Type: tea.KeyCtrlC,
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: tea.Quit,
Expand All @@ -54,10 +56,13 @@ func TestBubbleTeaUpdate(t *testing.T) {
updateParams: tea.KeyMsg{
Type: tea.KeyEnter,
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0"},
Choice: "test-choice-0",
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0"},
Choice: "test-choice-0",
BaseStyle: NewBaseStyle(),
}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: tea.Quit,
Expand All @@ -74,9 +79,11 @@ func TestBubbleTeaUpdate(t *testing.T) {
Type: tea.KeyRunes,
Runes: []rune("j"),
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
Cursor: 1,
Choices: []string{"test-choice-0", "test-choice-1"},
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{
Cursor: 1,
Choices: []string{"test-choice-0", "test-choice-1"},
}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: nil,
Expand All @@ -94,9 +101,11 @@ func TestBubbleTeaUpdate(t *testing.T) {
Type: tea.KeyRunes,
Runes: []rune("j"),
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0", "test-choice-1"},
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0", "test-choice-1"},
}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: nil,
Expand All @@ -114,9 +123,11 @@ func TestBubbleTeaUpdate(t *testing.T) {
Type: tea.KeyRunes,
Runes: []rune("k"),
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0", "test-choice-1"},
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0", "test-choice-1"},
}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: nil,
Expand All @@ -134,9 +145,11 @@ func TestBubbleTeaUpdate(t *testing.T) {
Type: tea.KeyRunes,
Runes: []rune("k"),
},
expectedModel: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0"},
expectedModel: New(&Params{
Model: NewChoicesModel(&ChoicesModelParams{
Cursor: 0,
Choices: []string{"test-choice-0"},
}),
BaseStyle: NewBaseStyle(),
}),
expectedCmd: nil,
Expand Down
4 changes: 2 additions & 2 deletions bubbletea/bubbletea_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestBubbleTeaView(t *testing.T) {
}),
BaseStyle: NewBaseStyle(),
}),
expectedView: "┌────────────────────────────────────────────────────────────────────────────────┐\n│ │\n│ ┌──────────────────────────────────────────────────────────────────────────────│\n│ ──┐ │\n│ │ │\n│ │ │\n│ │ test-header: │\n│ │ │\n│ │ (•) test-choice-0 │\n│ │ │\n│ │ │\n│ │ │\n│ │ (press q to quit) │\n│ │ │\n│ │ │\n│ │ │\n│ └──────────────────────────────────────────────────────────────────────────────│\n│ ──┘ │\n│ │\n└────────────────────────────────────────────────────────────────────────────────┘\n",
expectedView: "┌────────────────────────────────────────────────────────────────────────────────┐\n│ │\n│ test-header: │\n│ (•) test-choice-0 │\n│ │\n│ (press q to quit) │\n│ │\n└────────────────────────────────────────────────────────────────────────────────┘\n",
},
{
subTestName: "Handles success view result with multiple choices",
Expand All @@ -37,7 +37,7 @@ func TestBubbleTeaView(t *testing.T) {
}),
BaseStyle: NewBaseStyle(),
}),
expectedView: "┌────────────────────────────────────────────────────────────────────────────────┐\n│ │\n│ ┌──────────────────────────────────────────────────────────────────────────────│\n│ ──┐ │\n│ │ │\n│ │ │\n│ │ test-header: │\n│ │ │\n│ │ (•) test-choice-0 │\n│ │ │\n│ │ ( ) test-choice-1 │\n│ │ │\n│ │ │\n│ │ │\n│ │ (press q to quit) │\n│ │ │\n│ │ │\n│ │ │\n│ └──────────────────────────────────────────────────────────────────────────────│\n│ ──┘ │\n│ │\n└────────────────────────────────────────────────────────────────────────────────┘\n",
expectedView: "┌────────────────────────────────────────────────────────────────────────────────┐\n│ │\n│ test-header: │\n│ (•) test-choice-0 │\n│ ( ) test-choice-1 │\n│ │\n│ (press q to quit) │\n│ │\n└────────────────────────────────────────────────────────────────────────────────┘\n",
},
}

Expand Down
7 changes: 1 addition & 6 deletions bubbletea/choicesmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package bubbletea
import (
"strings"

"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
Expand All @@ -22,10 +21,6 @@ type ChoicesModel struct {
// ui is the UI of the CLI.
ui ChoicesModelUI

// table is the bubbletea table model.
// TODO(@chris-ramon): Make it available through multiple model support.
table table.Model //nolint:golint,unused

// baseStyle is the base styling of the BubbleTea component.
baseStyle lipgloss.Style
}
Expand All @@ -42,7 +37,7 @@ func (b ChoicesModel) Init() tea.Cmd {
}

// Update is the `BubbleTea` method required for implementing the `Model` interface.
func (b ChoicesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:golint,ireturn
func (b *ChoicesModel) Update(msg tea.Msg) (Model, tea.Cmd) { //nolint:golint,ireturn
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return b, nil
Expand Down
17 changes: 14 additions & 3 deletions bubbletea/tablemodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (

// `TableModel` represents the component that implements the `Model` interface.
type TableModel struct {
// baseStyle is the base styling for the `table` model.
baseStyle lipgloss.Style

// table is the `bubbletea` table model.
// TODO(@chris-ramon): Wire to the `TableModel` component.
table table.Model

// baseStyle is the base styling for the `table` model.
baseStyle lipgloss.Style
}

// `Init` is the `TableModel` method required for implementing the `Model` interface.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TableModel.Update method should use a pointer receiver and return (Model, tea.Cmd) to match the Model interface signature and ensure state is properly maintained (like the changes made to ChoicesModel.Update).

Suggested change
// `Init` is the `TableModel` method required for implementing the `Model` interface.
func (tm *TableModel) Update(msg tea.Msg) (Model, tea.Cmd) { //nolint:golint,ireturn

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MentatBot nice catch ! - since currently we are not using TableModel I'll go ahead and update it in a follow-up PR, I've created an issue for addressing this: #19.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a reasonable approach! Thanks for creating an issue to track this. Addressing it in a follow-up PR makes sense, especially since the TableModel isn't currently in active use. The current PR already makes significant architectural improvements to the codebase.

Expand Down Expand Up @@ -84,7 +84,18 @@ type TableModelParams struct {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the other methods in TableModel and to ensure proper state management, it would be good to use a pointer receiver for the Run method as well.

Suggested change
func (tm *TableModel) Run(model any) (any, error) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MentatBot nice suggestion - I've added this to the follow-up PR issue: #19, I'll consider addressing this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this to the follow-up issue as well. Both suggestions relate to the same architectural pattern of using pointer receivers consistently, so addressing them together makes sense. Since the TableModel isn't actively used yet, this approach maintains good code organization while planning for future improvements.

// `NewTableModel` returns a pointer to a `TableModel`.
func NewTableModel(p *TableModelParams) *TableModel {
columns := []table.Column{}
rows := []table.Row{}

t := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(7),
)

return &TableModel{
baseStyle: p.BaseStyle,
table: t,
}
}