From c8ad5b9ef9fdc094c2cd974d6b9a65112089922b Mon Sep 17 00:00:00 2001 From: s Date: Sun, 30 Nov 2025 00:53:37 -0500 Subject: refactor: replace table component with custom card-based person selector ui --- internal/tui/person_selector.go | 59 +++++++++++------ internal/tui/wizard.go | 143 ++++++++++++++++++++++++---------------- 2 files changed, 126 insertions(+), 76 deletions(-) diff --git a/internal/tui/person_selector.go b/internal/tui/person_selector.go index 2ae8caf..359c7b3 100644 --- a/internal/tui/person_selector.go +++ b/internal/tui/person_selector.go @@ -15,12 +15,24 @@ var ( Bold(true). Foreground(lipgloss.Color("86")) - selectedStyle = lipgloss.NewStyle(). + selectedCardStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("86")). + Padding(0, 1) + + normalCardStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("240")). + Padding(0, 1) + + nameStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")) + Foreground(lipgloss.Color("229")) - normalStyle = lipgloss.NewStyle(). + labelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("86")) + + valueStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("252")) dimStyle = lipgloss.NewStyle(). @@ -29,6 +41,10 @@ var ( helpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")) + pointerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("86")). + Bold(true) + errorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("196")) ) @@ -311,9 +327,9 @@ func (m PersonSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m PersonSelectorModel) visibleHeight() int { if m.windowHeight > 0 { - return m.windowHeight + return m.windowHeight / 6 } - return 20 + return 5 } func (m PersonSelectorModel) View() string { @@ -338,32 +354,33 @@ func (m PersonSelectorModel) View() string { for i := m.offset; i < endIdx; i++ { p := m.people[i] - cursor := " " - style := normalStyle + isSelected := i == m.cursor - if i == m.cursor { - cursor = "> " - style = selectedStyle + cardStyle := normalCardStyle + if isSelected { + cardStyle = selectedCardStyle } - line := fmt.Sprintf("%s%d. %s", cursor, p.Index, p.Name) + var cardContent strings.Builder + + pointer := " " + if isSelected { + pointer = pointerStyle.Render("▶ ") + } + cardContent.WriteString(fmt.Sprintf("%s%s\n", pointer, nameStyle.Render(fmt.Sprintf("%d. %s", p.Index, p.Name)))) - details := []string{} if p.Age != "" { - details = append(details, fmt.Sprintf("Age: %s", p.Age)) + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Age:"), valueStyle.Render(p.Age))) } if p.Location != "" { - details = append(details, p.Location) + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Location:"), valueStyle.Render(p.Location))) } if p.PhoneHint != "" { - details = append(details, fmt.Sprintf("Phone: %s", p.PhoneHint)) - } - - if len(details) > 0 { - line += dimStyle.Render(fmt.Sprintf(" (%s)", strings.Join(details, ", "))) + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Phone:"), valueStyle.Render(p.PhoneHint))) } - b.WriteString(style.Render(line)) + content := strings.TrimSuffix(cardContent.String(), "\n") + b.WriteString(cardStyle.Render(content)) b.WriteString("\n") } diff --git a/internal/tui/wizard.go b/internal/tui/wizard.go index 34ba70e..3bbad2e 100644 --- a/internal/tui/wizard.go +++ b/internal/tui/wizard.go @@ -6,7 +6,6 @@ import ( "strings" "git.db.org.ai/dborg/internal/formatter" - "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -30,8 +29,9 @@ type WizardModel struct { state wizardState inputs []textinput.Model focusIndex int - table table.Model people []Person + cursor int + offset int selectedIdx int sxKey string reportData map[string]interface{} @@ -63,10 +63,6 @@ var ( Foreground(lipgloss.Color("86")). MarginBottom(1) - tableStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). Italic(true) @@ -209,7 +205,8 @@ func (m WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.people = people m.sxKey = msg.sxKey - m.table = m.buildTable() + m.cursor = 0 + m.offset = 0 m.state = stateResults return m, nil @@ -279,14 +276,47 @@ func (m WizardModel) updateResults(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.updateFocus(), nil case "enter": - m.selectedIdx = m.table.Cursor() + m.selectedIdx = m.cursor m.state = stateGenerating return m, m.doReport() + + case "up", "k": + if m.cursor > 0 { + m.cursor-- + if m.cursor < m.offset { + m.offset = m.cursor + } + } + + case "down", "j": + if m.cursor < len(m.people)-1 { + m.cursor++ + visibleHeight := m.resultsVisibleHeight() + if m.cursor >= m.offset+visibleHeight { + m.offset = m.cursor - visibleHeight + 1 + } + } + + case "home": + m.cursor = 0 + m.offset = 0 + + case "end": + m.cursor = len(m.people) - 1 + visibleHeight := m.resultsVisibleHeight() + if len(m.people) > visibleHeight { + m.offset = len(m.people) - visibleHeight + } } - var cmd tea.Cmd - m.table, cmd = m.table.Update(msg) - return m, cmd + return m, nil +} + +func (m WizardModel) resultsVisibleHeight() int { + if m.windowHeight > 0 { + return (m.windowHeight - 10) / 6 + } + return 4 } func (m *WizardModel) updateFocus() WizardModel { @@ -312,48 +342,6 @@ func (m *WizardModel) updateInputs(msg tea.KeyMsg) tea.Cmd { return tea.Batch(cmds...) } -func (m WizardModel) buildTable() table.Model { - columns := []table.Column{ - {Title: "#", Width: 4}, - {Title: "Name", Width: 25}, - {Title: "Age", Width: 6}, - {Title: "Location", Width: 25}, - {Title: "Phone Hint", Width: 15}, - } - - rows := make([]table.Row, len(m.people)) - for i, p := range m.people { - rows[i] = table.Row{ - fmt.Sprintf("%d", p.Index), - p.Name, - p.Age, - p.Location, - p.PhoneHint, - } - } - - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(min(len(rows)+1, 15)), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) - - return t -} - func (m WizardModel) View() string { switch m.state { case stateForm: @@ -415,9 +403,54 @@ func (m WizardModel) viewResults() string { b.WriteString(statusStyle.Render(fmt.Sprintf("Found %d results", len(m.people)))) b.WriteString("\n\n") - b.WriteString(tableStyle.Render(m.table.View())) + visibleHeight := m.resultsVisibleHeight() + endIdx := m.offset + visibleHeight + if endIdx > len(m.people) { + endIdx = len(m.people) + } - b.WriteString("\n\n") + if m.offset > 0 { + b.WriteString(dimStyle.Render(fmt.Sprintf(" ↑ %d more above\n", m.offset))) + } + + for i := m.offset; i < endIdx; i++ { + p := m.people[i] + isSelected := i == m.cursor + + cardStyle := normalCardStyle + if isSelected { + cardStyle = selectedCardStyle + } + + var cardContent strings.Builder + + pointer := " " + if isSelected { + pointer = pointerStyle.Render("▶ ") + } + cardContent.WriteString(fmt.Sprintf("%s%s\n", pointer, nameStyle.Render(fmt.Sprintf("%d. %s", p.Index, p.Name)))) + + if p.Age != "" { + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Age:"), valueStyle.Render(p.Age))) + } + if p.Location != "" { + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Location:"), valueStyle.Render(p.Location))) + } + if p.PhoneHint != "" { + cardContent.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Phone:"), valueStyle.Render(p.PhoneHint))) + } + + content := strings.TrimSuffix(cardContent.String(), "\n") + b.WriteString(cardStyle.Render(content)) + b.WriteString("\n") + } + + remaining := len(m.people) - endIdx + if remaining > 0 { + b.WriteString(dimStyle.Render(fmt.Sprintf(" ↓ %d more below\n", remaining))) + } + + b.WriteString("\n") b.WriteString(helpStyle.Render("j/k or arrows: navigate | enter: select | esc: back | q: quit")) return b.String() -- cgit v1.2.3