1package components
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
8
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/charmbracelet/lipgloss"
11)
12
13// SelectorModel is a list selection dialog.
14type SelectorModel struct {
15 title string
16 items []string
17 current string // highlighted item (e.g., current tag)
18 cursor int
19 selected string
20 cancelled bool
21}
22
23// NewSelector creates a new selector dialog.
24func NewSelector(title string, items []string, current string) SelectorModel {
25 return SelectorModel{
26 title: title,
27 items: items,
28 current: current,
29 }
30}
31
32func (m SelectorModel) Init() tea.Cmd { return nil }
33
34func (m SelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
35 switch msg := msg.(type) {
36 case tea.KeyMsg:
37 if len(m.items) == 0 {
38 switch msg.String() {
39 case "esc", "ctrl+c", "q":
40 m.cancelled = true
41 return m, tea.Quit
42 }
43 return m, nil
44 }
45 switch msg.String() {
46 case "up", "k":
47 if m.cursor > 0 {
48 m.cursor--
49 }
50 case "down", "j":
51 if m.cursor < len(m.items)-1 {
52 m.cursor++
53 }
54 case "enter":
55 m.selected = m.items[m.cursor]
56 return m, tea.Quit
57 case "esc", "ctrl+c", "q":
58 m.cancelled = true
59 return m, tea.Quit
60 }
61 }
62 return m, nil
63}
64
65func (m SelectorModel) View() string {
66 var b strings.Builder
67
68 b.WriteString(styles.Theme.Bold.Render(m.title))
69 b.WriteString("\n\n")
70
71 for i, item := range m.items {
72 cursor := " "
73 if i == m.cursor {
74 cursor = "> "
75 }
76
77 line := item
78 if item == m.current {
79 line = item + " " + lipgloss.NewStyle().Foreground(styles.ColorPrimary).Render("(current)")
80 }
81
82 if i == m.cursor {
83 b.WriteString(styles.Theme.Highlight.Render(cursor + line))
84 } else {
85 b.WriteString(fmt.Sprintf("%s%s", cursor, line))
86 }
87 b.WriteString("\n")
88 }
89
90 b.WriteString("\n")
91 b.WriteString(styles.RenderKeyHelp("↑/↓", "navigate") + " " +
92 styles.RenderKeyHelp("enter", "select") + " " +
93 styles.RenderKeyHelp("esc", "cancel"))
94
95 return b.String()
96}
97
98func (m SelectorModel) Selected() string { return m.selected }
99func (m SelectorModel) Cancelled() bool { return m.cancelled }
100
101// RunSelector runs a selector dialog and returns the selected item.
102func RunSelector(title string, items []string, current string) (string, error) {
103 m := NewSelector(title, items, current)
104 p := tea.NewProgram(m)
105 finalModel, err := p.Run()
106 if err != nil {
107 return "", fmt.Errorf("error running selector: %w", err)
108 }
109 result := finalModel.(SelectorModel)
110 if result.Cancelled() {
111 return "", nil
112 }
113 return result.Selected(), nil
114}