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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ go.work.sum
.works/
AGENTS.md
CLAUDE.md
GEMINI.md
.serena/
.claude/
# VitePress
Expand All @@ -50,3 +51,4 @@ docs/.vitepress/cache

# Node
node_modules/
.gocache/
63 changes: 63 additions & 0 deletions internal/ui/components/binding_details.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Package components provides reusable UI components for the BobaMixer TUI.
package components

import (
"fmt"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// BindingDetails represents the selected binding metadata.
type BindingDetails struct {
ToolID string
ProviderID string
UseProxy bool
ModelOverride string
}

// BindingDetailsPanel renders the binding metadata.
type BindingDetailsPanel struct {
details *BindingDetails
styles theme.Styles
}

// NewBindingDetailsPanel constructs the panel.
func NewBindingDetailsPanel(details *BindingDetails, styles theme.Styles) BindingDetailsPanel {
return BindingDetailsPanel{
details: details,
styles: styles,
}
}

// Update satisfies the Bubble Tea component interface.
func (c BindingDetailsPanel) Update(_ tea.Msg) (BindingDetailsPanel, tea.Cmd) {
return c, nil
}

// View renders the binding details if available.
func (c BindingDetailsPanel) View() string {
if c.details == nil {
return ""
}

lines := []string{
fmt.Sprintf("Tool ID: %s", c.details.ToolID),
fmt.Sprintf("Provider ID: %s", c.details.ProviderID),
fmt.Sprintf("Use Proxy: %t", c.details.UseProxy),
}

if model := strings.TrimSpace(c.details.ModelOverride); model != "" {
lines = append(lines, fmt.Sprintf("Model Override: %s", model))
}

var b strings.Builder
for _, line := range lines {
normalStyle := c.styles.Normal
b.WriteString(normalStyle.PaddingLeft(2).Render(line))
b.WriteString("\n")
}

return strings.TrimRight(b.String(), "\n")
}
80 changes: 80 additions & 0 deletions internal/ui/components/binding_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package components

import (
"fmt"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// BindingRow represents a binding entry in the list.
type BindingRow struct {
ToolName string
ProviderName string
UseProxy bool
}

// BindingList renders tool-provider bindings with proxy status.
type BindingList struct {
rows []BindingRow
selected int
proxyEnabled string
proxyDisabled string
emptyState string
styles theme.Styles
}

// NewBindingList constructs the bindings list component.
func NewBindingList(rows []BindingRow, selected int, emptyState string, proxyEnabled string, proxyDisabled string, styles theme.Styles) BindingList {
return BindingList{
rows: rows,
selected: selected,
proxyEnabled: proxyEnabled,
proxyDisabled: proxyDisabled,
emptyState: strings.TrimSpace(emptyState),
styles: styles,
}
}

// Update satisfies the Bubble Tea component interface.
func (c BindingList) Update(_ tea.Msg) (BindingList, tea.Cmd) {
return c, nil
}

// View renders the bindings list or the empty state message.
func (c BindingList) View() string {
if len(c.rows) == 0 {
if c.emptyState == "" {
return ""
}
normalStyle := c.styles.Normal
return normalStyle.PaddingLeft(2).Render(c.emptyState)
}

var b strings.Builder
selected := c.selected
if selected >= len(c.rows) {
selected = len(c.rows) - 1
}
if selected < 0 {
selected = 0
}

for idx, row := range c.rows {
icon := c.proxyDisabled
if row.UseProxy {
icon = c.proxyEnabled
}

line := fmt.Sprintf(" %-15s → %-25s Proxy: %s", row.ToolName, row.ProviderName, icon)
if idx == selected {
b.WriteString(c.styles.Selected.Render("▶ " + line))
} else {
b.WriteString(c.styles.Normal.Render(" " + line))
}
b.WriteString("\n")
}

return strings.TrimRight(b.String(), "\n")
}
47 changes: 47 additions & 0 deletions internal/ui/components/bullet_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package components

import (
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// BulletList renders bullet points using the muted text style.
type BulletList struct {
items []string
styles theme.Styles
}

// NewBulletList constructs a bullet list component.
func NewBulletList(items []string, styles theme.Styles) BulletList {
return BulletList{
items: items,
styles: styles,
}
}

// Update satisfies the Bubble Tea component interface.
func (c BulletList) Update(_ tea.Msg) (BulletList, tea.Cmd) {
return c, nil
}

// View renders each bullet with muted styling.
func (c BulletList) View() string {
if len(c.items) == 0 {
return ""
}

var b strings.Builder
style := c.styles.Normal
style = style.PaddingLeft(2)
for _, item := range c.items {
text := strings.TrimSpace(item)
if text == "" {
continue
}
b.WriteString(style.Render("• " + text))
b.WriteString("\n")
}
return strings.TrimRight(b.String(), "\n")
}
78 changes: 78 additions & 0 deletions internal/ui/components/config_file_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package components

import (
"fmt"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// ConfigFile represents an editable configuration file entry.
type ConfigFile struct {
Name string
File string
Desc string
}

// ConfigFileList renders selectable config files.
type ConfigFileList struct {
files []ConfigFile
selected int
home string
styles theme.Styles
}

// NewConfigFileList constructs the list component.
func NewConfigFileList(files []ConfigFile, selected int, home string, styles theme.Styles) ConfigFileList {
return ConfigFileList{
files: files,
selected: selected,
home: home,
styles: styles,
}
}

// Update satisfies the Bubble Tea component interface.
func (c ConfigFileList) Update(_ tea.Msg) (ConfigFileList, tea.Cmd) {
return c, nil
}

// View renders the config file list with descriptions for the selected item.
func (c ConfigFileList) View() string {
if len(c.files) == 0 {
normalStyle := c.styles.Normal
return normalStyle.PaddingLeft(2).Render("No configuration files detected.")
}

var b strings.Builder
selected := c.selected
if selected >= len(c.files) {
selected = 0
}

for idx, cfg := range c.files {
fileLabel := fmt.Sprintf(" (%s)", cfg.File)
if idx == selected {
b.WriteString(c.styles.Selected.Render("▶ " + cfg.Name))
b.WriteString(c.styles.Help.Render(fileLabel))
} else {
b.WriteString(c.styles.Normal.Render(" " + cfg.Name))
b.WriteString(c.styles.Help.Render(fileLabel))
}
b.WriteString("\n")

if idx == selected {
normalStyle := c.styles.Normal
descLine := normalStyle.PaddingLeft(4).Render(cfg.Desc)
helpStyle := c.styles.Help
pathLine := helpStyle.PaddingLeft(4).Render(fmt.Sprintf("Full path: %s/%s", strings.TrimSpace(c.home), cfg.File))
b.WriteString(descLine)
b.WriteString("\n")
b.WriteString(pathLine)
b.WriteString("\n")
}
}

return strings.TrimRight(b.String(), "\n")
}
30 changes: 30 additions & 0 deletions internal/ui/components/help_bar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package components

import (
tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// HelpBar renders the navigation help line.
type HelpBar struct {
text string
styles theme.Styles
}

// NewHelpBar constructs a HelpBar component.
func NewHelpBar(text string, styles theme.Styles) HelpBar {
return HelpBar{
text: text,
styles: styles,
}
}

// Update satisfies the Bubble Tea component contract (no mutations needed).
func (c HelpBar) Update(_ tea.Msg) (HelpBar, tea.Cmd) {
return c, nil
}

// View renders the help text with the shared style.
func (c HelpBar) View() string {
return c.styles.Help.Render(c.text)
}
30 changes: 30 additions & 0 deletions internal/ui/components/help_footer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package components

import (
tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// HelpFooter renders the closing hint for the help overlay.
type HelpFooter struct {
message string
styles theme.Styles
}

// NewHelpFooter constructs the footer component.
func NewHelpFooter(message string, styles theme.Styles) HelpFooter {
return HelpFooter{
message: message,
styles: styles,
}
}

// Update keeps the component immutable because the footer has no state.
func (c HelpFooter) Update(_ tea.Msg) (HelpFooter, tea.Cmd) {
return c, nil
}

// View renders the footer hint message.
func (c HelpFooter) View() string {
return c.styles.Help.Render(c.message)
}
36 changes: 36 additions & 0 deletions internal/ui/components/help_header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package components

import (
tea "github.com/charmbracelet/bubbletea"
"github.com/royisme/bobamixer/internal/ui/theme"
)

// HelpHeader renders the main title for the help overlay.
type HelpHeader struct {
title string
subtext string
styles theme.Styles
}

// NewHelpHeader builds a header component with the provided title and optional subtext.
func NewHelpHeader(title string, subtext string, styles theme.Styles) HelpHeader {
return HelpHeader{
title: title,
subtext: subtext,
styles: styles,
}
}

// Update satisfies the component contract but the header has no runtime state to change.
func (c HelpHeader) Update(_ tea.Msg) (HelpHeader, tea.Cmd) {
return c, nil
}

// View renders the header and subtext using the shared theme styles.
func (c HelpHeader) View() string {
content := c.styles.Title.Render(c.title)
if c.subtext != "" {
content += "\n" + c.styles.Help.Render(c.subtext)
}
return content
}
Loading
Loading