theme.go 4.7 KB

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