theme.go 4.8 KB

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