1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
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 tea.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.KeyMsg:
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() string {
126 currentTheme := theme.CurrentTheme()
127 baseStyle := styles.BaseStyle()
128
129 if len(t.themes) == 0 {
130 return baseStyle.Padding(1, 2).
131 Border(lipgloss.RoundedBorder()).
132 BorderBackground(currentTheme.Background()).
133 BorderForeground(currentTheme.TextMuted()).
134 Width(40).
135 Render("No themes available")
136 }
137
138 // Calculate max width needed for theme names
139 maxWidth := 40 // Minimum width
140 for _, themeName := range t.themes {
141 if len(themeName) > maxWidth-4 { // Account for padding
142 maxWidth = len(themeName) + 4
143 }
144 }
145
146 maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
147
148 // Build the theme list
149 themeItems := make([]string, 0, len(t.themes))
150 for i, themeName := range t.themes {
151 itemStyle := baseStyle.Width(maxWidth)
152
153 if i == t.selectedIdx {
154 itemStyle = itemStyle.
155 Background(currentTheme.Primary()).
156 Foreground(currentTheme.Background()).
157 Bold(true)
158 }
159
160 themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
161 }
162
163 title := baseStyle.
164 Foreground(currentTheme.Primary()).
165 Bold(true).
166 Width(maxWidth).
167 Padding(0, 1).
168 Render("Select Theme")
169
170 content := lipgloss.JoinVertical(
171 lipgloss.Left,
172 title,
173 baseStyle.Width(maxWidth).Render(""),
174 baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
175 baseStyle.Width(maxWidth).Render(""),
176 )
177
178 return baseStyle.Padding(1, 2).
179 Border(lipgloss.RoundedBorder()).
180 BorderBackground(currentTheme.Background()).
181 BorderForeground(currentTheme.TextMuted()).
182 Width(lipgloss.Width(content) + 4).
183 Render(content)
184}
185
186func (t *themeDialogCmp) BindingKeys() []key.Binding {
187 return layout.KeyMapToSlice(themeKeys)
188}
189
190// NewThemeDialogCmp creates a new theme switching dialog
191func NewThemeDialogCmp() ThemeDialog {
192 return &themeDialogCmp{
193 themes: []string{},
194 selectedIdx: 0,
195 currentTheme: "",
196 }
197}
198