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(cursor)
86 b.WriteString(line)
87 }
88 b.WriteString("\n")
89 }
90
91 b.WriteString("\n")
92 b.WriteString(styles.RenderKeyHelp("↑/↓", "navigate") + " " +
93 styles.RenderKeyHelp("enter", "select") + " " +
94 styles.RenderKeyHelp("esc", "cancel"))
95
96 return b.String()
97}
98
99func (m SelectorModel) Selected() string { return m.selected }
100func (m SelectorModel) Cancelled() bool { return m.cancelled }
101
102// RunSelector runs a selector dialog and returns the selected item.
103func RunSelector(title string, items []string, current string) (string, error) {
104 m := NewSelector(title, items, current)
105 p := tea.NewProgram(m)
106 finalModel, err := p.Run()
107 if err != nil {
108 return "", fmt.Errorf("error running selector: %w", err)
109 }
110 result := finalModel.(SelectorModel)
111 if result.Cancelled() {
112 return "", nil
113 }
114 return result.Selected(), nil
115}