1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/theme"
10 "github.com/opencode-ai/opencode/internal/tui/util"
11)
12
13// ThemeChangedMsg is sent when the theme is changed
14type ThemeChangedMsg struct {
15 ThemeName string
16}
17
18// CloseThemeDialogMsg is sent when the theme dialog is closed
19type CloseThemeDialogMsg struct{}
20
21// ThemeDialog interface for the theme switching dialog
22type ThemeDialog interface {
23 util.Model
24 layout.Bindings
25}
26
27type themeDialogCmp struct {
28 themes []string
29 selectedIdx int
30 width int
31 height int
32 currentTheme string
33}
34
35type themeKeyMap struct {
36 Up key.Binding
37 Down key.Binding
38 Enter key.Binding
39 Escape key.Binding
40 J key.Binding
41 K key.Binding
42}
43
44var themeKeys = themeKeyMap{
45 Up: key.NewBinding(
46 key.WithKeys("up"),
47 key.WithHelp("↑", "previous theme"),
48 ),
49 Down: key.NewBinding(
50 key.WithKeys("down"),
51 key.WithHelp("↓", "next theme"),
52 ),
53 Enter: key.NewBinding(
54 key.WithKeys("enter"),
55 key.WithHelp("enter", "select theme"),
56 ),
57 Escape: key.NewBinding(
58 key.WithKeys("esc"),
59 key.WithHelp("esc", "close"),
60 ),
61 J: key.NewBinding(
62 key.WithKeys("j"),
63 key.WithHelp("j", "next theme"),
64 ),
65 K: key.NewBinding(
66 key.WithKeys("k"),
67 key.WithHelp("k", "previous theme"),
68 ),
69}
70
71func (t *themeDialogCmp) Init() tea.Cmd {
72 // Load available themes and update selectedIdx based on current theme
73 t.themes = theme.AvailableThemes()
74 t.currentTheme = theme.CurrentThemeName()
75
76 // Find the current theme in the list
77 for i, name := range t.themes {
78 if name == t.currentTheme {
79 t.selectedIdx = i
80 break
81 }
82 }
83
84 return nil
85}
86
87func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88 switch msg := msg.(type) {
89 case tea.KeyPressMsg:
90 switch {
91 case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
92 if t.selectedIdx > 0 {
93 t.selectedIdx--
94 }
95 return t, nil
96 case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
97 if t.selectedIdx < len(t.themes)-1 {
98 t.selectedIdx++
99 }
100 return t, nil
101 case key.Matches(msg, themeKeys.Enter):
102 if len(t.themes) > 0 {
103 previousTheme := theme.CurrentThemeName()
104 selectedTheme := t.themes[t.selectedIdx]
105 if previousTheme == selectedTheme {
106 return t, util.CmdHandler(CloseThemeDialogMsg{})
107 }
108 if err := theme.SetTheme(selectedTheme); err != nil {
109 return t, util.ReportError(err)
110 }
111 return t, util.CmdHandler(ThemeChangedMsg{
112 ThemeName: selectedTheme,
113 })
114 }
115 case key.Matches(msg, themeKeys.Escape):
116 return t, util.CmdHandler(CloseThemeDialogMsg{})
117 }
118 case tea.WindowSizeMsg:
119 t.width = msg.Width
120 t.height = msg.Height
121 }
122 return t, nil
123}
124
125func (t *themeDialogCmp) View() tea.View {
126 currentTheme := theme.CurrentTheme()
127 baseStyle := styles.BaseStyle()
128
129 if len(t.themes) == 0 {
130 return tea.NewView(
131 baseStyle.Padding(1, 2).
132 Border(lipgloss.RoundedBorder()).
133 BorderBackground(currentTheme.Background()).
134 BorderForeground(currentTheme.TextMuted()).
135 Width(40).
136 Render("No themes available"),
137 )
138 }
139
140 // Calculate max width needed for theme names
141 maxWidth := 40 // Minimum width
142 for _, themeName := range t.themes {
143 if len(themeName) > maxWidth-4 { // Account for padding
144 maxWidth = len(themeName) + 4
145 }
146 }
147
148 maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
149
150 // Build the theme list
151 themeItems := make([]string, 0, len(t.themes))
152 for i, themeName := range t.themes {
153 itemStyle := baseStyle.Width(maxWidth)
154
155 if i == t.selectedIdx {
156 itemStyle = itemStyle.
157 Background(currentTheme.Primary()).
158 Foreground(currentTheme.Background()).
159 Bold(true)
160 }
161
162 themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
163 }
164
165 title := baseStyle.
166 Foreground(currentTheme.Primary()).
167 Bold(true).
168 Width(maxWidth).
169 Padding(0, 1).
170 Render("Select Theme")
171
172 content := lipgloss.JoinVertical(
173 lipgloss.Left,
174 title,
175 baseStyle.Width(maxWidth).Render(""),
176 baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
177 baseStyle.Width(maxWidth).Render(""),
178 )
179
180 return tea.NewView(
181 baseStyle.Padding(1, 2).
182 Border(lipgloss.RoundedBorder()).
183 BorderBackground(currentTheme.Background()).
184 BorderForeground(currentTheme.TextMuted()).
185 Width(lipgloss.Width(content) + 4).
186 Render(content),
187 )
188}
189
190func (t *themeDialogCmp) BindingKeys() []key.Binding {
191 return layout.KeyMapToSlice(themeKeys)
192}
193
194// NewThemeDialogCmp creates a new theme switching dialog
195func NewThemeDialogCmp() ThemeDialog {
196 return &themeDialogCmp{
197 themes: []string{},
198 selectedIdx: 0,
199 currentTheme: "",
200 }
201}