on main help sign in
more theme cleanup
commitdfd23784fc26989944085cc2398e661d6a954011 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 4 Jun 2025, 13:12 (1 year ago) parent4a74863
18 files changed+134−1184
cspell.json+1−1 internal/tui/components/chat/chat.go−4 internal/tui/components/chat/editor/editor.go+4−8 internal/tui/components/chat/messages/messages.go+8−11 internal/tui/components/chat/messages/renderer.go+18−22 internal/tui/components/chat/messages/tool.go+6−9 internal/tui/components/completions/completions.go+3−4 internal/tui/components/dialog/filepicker.go+14−18 internal/tui/components/dialog/help.go−203 internal/tui/components/dialog/init.go+12−20 internal/tui/components/dialog/permission.go+39−51 internal/tui/components/dialog/theme.go−201 internal/tui/components/dialogs/commands/arguments.go+16−33 internal/tui/components/dialogs/quit/quit.go+10−14 internal/tui/layout/overlay.go−169 internal/tui/page/logs.go+1−1 internal/tui/styles/markdown.go+2−260 internal/tui/styles/styles.go−155
cspell.json+1−1
@@ -1 +1 @@
1−{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps"],"version":"0.2","language":"en","flagWords":[]}
1+{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph"],"version":"0.2","language":"en","flagWords":[]}
internal/tui/components/chat/chat.go−4
@@ -14,7 +14,6 @@ import (
1414 "github.com/opencode-ai/opencode/internal/session"
1515 "github.com/opencode-ai/opencode/internal/tui/components/chat/messages"
1616 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
17− "github.com/opencode-ai/opencode/internal/tui/components/dialog"
1817 "github.com/opencode-ai/opencode/internal/tui/layout"
1918 "github.com/opencode-ai/opencode/internal/tui/util"
2019)
@@ -87,9 +86,6 @@ func (m *messageListCmp) Init() tea.Cmd {
8786// Update handles incoming messages and updates the component state.
8887func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8988 switch msg := msg.(type) {
90− case dialog.ThemeChangedMsg:
91− m.listCmp.ResetView()
92− return m, nil
9389 case SessionSelectedMsg:
9490 if msg.ID != m.session.ID {
9591 cmd := m.SetSession(msg)
internal/tui/components/chat/editor/editor.go+4−8
@@ -22,7 +22,6 @@ import (
2222 "github.com/opencode-ai/opencode/internal/tui/components/dialog"
2323 "github.com/opencode-ai/opencode/internal/tui/layout"
2424 "github.com/opencode-ai/opencode/internal/tui/styles"
25− "github.com/opencode-ai/opencode/internal/tui/theme"
2625 "github.com/opencode-ai/opencode/internal/tui/util"
2726)
2827
@@ -138,9 +137,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138137 var cmd tea.Cmd
139138 var cmds []tea.Cmd
140139 switch msg := msg.(type) {
141− case dialog.ThemeChangedMsg:
142− m.textarea = CreateTextArea(&m.textarea)
143− return m, cmd
144140 case chat.SessionSelectedMsg:
145141 if msg.ID != m.session.ID {
146142 m.session = msg
@@ -300,11 +296,11 @@ func (m *editorCmp) GetSize() (int, int) {
300296
301297func (m *editorCmp) attachmentsContent() string {
302298 var styledAttachments []string
303− t := theme.CurrentTheme()
304− attachmentStyles := styles.BaseStyle().
299+ t := styles.CurrentTheme()
300+ attachmentStyles := t.S().Base.
305301 MarginLeft(1).
306− Background(t.TextMuted()).
307− Foreground(t.Text())
302+ Background(t.FgMuted).
303+ Foreground(t.FgBase)
308304 for i, attachment := range m.attachments {
309305 var filename string
310306 if len(attachment.FileName) > 10 {
internal/tui/components/chat/messages/messages.go+8−11
@@ -16,7 +16,6 @@ import (
1616 "github.com/opencode-ai/opencode/internal/tui/components/anim"
1717 "github.com/opencode-ai/opencode/internal/tui/layout"
1818 "github.com/opencode-ai/opencode/internal/tui/styles"
19− "github.com/opencode-ai/opencode/internal/tui/theme"
2019 "github.com/opencode-ai/opencode/internal/tui/util"
2120)
2221
@@ -124,7 +123,7 @@ func (m *messageCmp) textWidth() int {
124123// style returns the lipgloss style for the message component.
125124// Applies different border colors and styles based on message role and focus state.
126125func (msg *messageCmp) style() lipgloss.Style {
127− t := theme.CurrentTheme()
126+ t := styles.CurrentTheme()
128127 var borderColor color.Color
129128 borderStyle := lipgloss.NormalBorder()
130129 if msg.focused {
@@ -133,17 +132,16 @@ func (msg *messageCmp) style() lipgloss.Style {
133132
134133 switch msg.message.Role {
135134 case message.User:
136− borderColor = t.Secondary()
135+ borderColor = t.Secondary
137136 case message.Assistant:
138− borderColor = t.Primary()
137+ borderColor = t.Primary
139138 default:
140139 // Tool call
141− borderColor = t.TextMuted()
140+ borderColor = t.BgSubtle
142141 }
143142
144− return styles.BaseStyle().
143+ return t.S().Muted.
145144 BorderLeft(true).
146− Foreground(t.TextMuted()).
147145 BorderForeground(borderColor).
148146 BorderStyle(borderStyle)
149147}
@@ -182,14 +180,13 @@ func (m *messageCmp) renderAssistantMessage() string {
182180// renderUserMessage renders user messages with file attachments.
183181// Displays message content and any attached files with appropriate icons.
184182func (m *messageCmp) renderUserMessage() string {
185− t := theme.CurrentTheme()
183+ t := styles.CurrentTheme()
186184 parts := []string{
187185 m.markdownContent(),
188186 }
189− attachmentStyles := styles.BaseStyle().
187+ attachmentStyles := t.S().Text.
190188 MarginLeft(1).
191− Background(t.BackgroundSecondary()).
192− Foreground(t.Text())
189+ Background(t.BgSubtle)
193190 attachments := []string{}
194191 for _, attachment := range m.message.BinaryContent() {
195192 file := filepath.Base(attachment.Path)
internal/tui/components/chat/messages/renderer.go+18−22
@@ -15,7 +15,6 @@ import (
1515 "github.com/opencode-ai/opencode/internal/llm/agent"
1616 "github.com/opencode-ai/opencode/internal/llm/tools"
1717 "github.com/opencode-ai/opencode/internal/tui/styles"
18− "github.com/opencode-ai/opencode/internal/tui/theme"
1918)
2019
2120// responseContextHeight limits the number of lines displayed in tool output
@@ -107,7 +106,7 @@ func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []
107106 return joinHeaderBody(header, body)
108107}
109108
110−// unmarshalParams safely unmarshals JSON parameters
109+// unmarshalParams safely unmarshal JSON parameters
111110func (br baseRenderer) unmarshalParams(input string, target any) error {
112111 return json.Unmarshal([]byte(input), target)
113112}
@@ -593,7 +592,7 @@ func joinHeaderBody(header, body string) string {
593592}
594593
595594func renderPlainContent(v *toolCallCmp, content string) string {
596− t := theme.CurrentTheme()
595+ t := styles.CurrentTheme()
597596 content = strings.TrimSpace(content)
598597 lines := strings.Split(content, "\n")
599598
@@ -606,58 +605,55 @@ func renderPlainContent(v *toolCallCmp, content string) string {
606605 if len(ln) > v.textWidth() {
607606 ln = v.fit(ln, v.textWidth())
608607 }
609− out = append(out, lipgloss.NewStyle().
608+ out = append(out, t.S().Muted.
610609 Width(v.textWidth()).
611− Background(t.BackgroundSecondary()).
612− Foreground(t.TextMuted()).
610+ Background(t.BgSubtle).
613611 Render(ln))
614612 }
615613
616614 if len(lines) > responseContextHeight {
617− out = append(out, lipgloss.NewStyle().
618− Background(t.BackgroundSecondary()).
619− Foreground(t.TextMuted()).
615+ out = append(out, t.S().Muted.
616+ Background(t.BgSubtle).
620617 Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
621618 }
622619 return strings.Join(out, "\n")
623620}
624621
625622func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
626− t := theme.CurrentTheme()
623+ t := styles.CurrentTheme()
627624 truncated := truncateHeight(content, responseContextHeight)
628625
629− highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BackgroundSecondary())
626+ highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle)
630627 lines := strings.Split(highlighted, "\n")
631628
632629 if len(strings.Split(content, "\n")) > responseContextHeight {
633− lines = append(lines, lipgloss.NewStyle().
634− Background(t.BackgroundSecondary()).
635− Foreground(t.TextMuted()).
630+ lines = append(lines, t.S().Muted.
631+ Background(t.BgSubtle).
636632 Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
637633 }
638634
639635 for i, ln := range lines {
640− num := lipgloss.NewStyle().
641− PaddingLeft(4).PaddingRight(2).
642− Background(t.BackgroundSecondary()).
643− Foreground(t.TextMuted()).
636+ num := t.S().Muted.
637+ Background(t.BgSubtle).
638+ PaddingLeft(4).
639+ PaddingRight(2).
644640 Render(fmt.Sprintf("%d", i+1+offset))
645641 w := v.textWidth() - lipgloss.Width(num)
646642 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
647643 num,
648− lipgloss.NewStyle().
644+ t.S().Base.
649645 Width(w).
650− Background(t.BackgroundSecondary()).
646+ Background(t.BgSubtle).
651647 Render(v.fit(ln, w)))
652648 }
653649 return lipgloss.JoinVertical(lipgloss.Left, lines...)
654650}
655651
656652func (v *toolCallCmp) renderToolError() string {
657− t := theme.CurrentTheme()
653+ t := styles.CurrentTheme()
658654 err := strings.ReplaceAll(v.result.Content, "\n", " ")
659655 err = fmt.Sprintf("Error: %s", err)
660− return styles.BaseStyle().Foreground(t.Error()).Render(v.fit(err, v.textWidth()))
656+ return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()))
661657}
662658
663659func removeWorkingDirPrefix(path string) string {
internal/tui/components/chat/messages/tool.go+6−9
@@ -11,7 +11,6 @@ import (
1111 "github.com/opencode-ai/opencode/internal/tui/components/anim"
1212 "github.com/opencode-ai/opencode/internal/tui/layout"
1313 "github.com/opencode-ai/opencode/internal/tui/styles"
14− "github.com/opencode-ai/opencode/internal/tui/theme"
1514 "github.com/opencode-ai/opencode/internal/tui/util"
1615)
1716
@@ -216,19 +215,17 @@ func (m *toolCallCmp) renderPending() string {
216215// style returns the lipgloss style for the tool call component.
217216// Applies muted colors and focus-dependent border styles.
218217func (m *toolCallCmp) style() lipgloss.Style {
219− t := theme.CurrentTheme()
218+ t := styles.CurrentTheme()
220219 if m.isNested {
221− return styles.BaseStyle().
222− Foreground(t.TextMuted())
220+ return t.S().Muted
223221 }
224222 borderStyle := lipgloss.NormalBorder()
225223 if m.focused {
226224 borderStyle = lipgloss.DoubleBorder()
227225 }
228− return styles.BaseStyle().
226+ return t.S().Muted.
229227 BorderLeft(true).
230− Foreground(t.TextMuted()).
231− BorderForeground(t.TextMuted()).
228+ BorderForeground(t.Border).
232229 BorderStyle(borderStyle)
233230}
234231
@@ -240,8 +237,8 @@ func (m *toolCallCmp) textWidth() int {
240237
241238// fit truncates content to fit within the specified width with ellipsis
242239func (m *toolCallCmp) fit(content string, width int) string {
243− t := theme.CurrentTheme()
244− lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted())
240+ t := styles.CurrentTheme()
241+ lineStyle := t.S().Muted.Background(t.BgSubtle)
245242 dots := lineStyle.Render("...")
246243 return ansi.Truncate(content, width, dots)
247244}
internal/tui/components/completions/completions.go+3−4
@@ -6,7 +6,6 @@ import (
66 "github.com/charmbracelet/lipgloss/v2"
77 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
88 "github.com/opencode-ai/opencode/internal/tui/styles"
9− "github.com/opencode-ai/opencode/internal/tui/theme"
109 "github.com/opencode-ai/opencode/internal/tui/util"
1110)
1211
@@ -172,11 +171,11 @@ func (c *completionsCmp) View() tea.View {
172171}
173172
174173func (c *completionsCmp) style() lipgloss.Style {
175− t := theme.CurrentTheme()
176− return styles.BaseStyle().
174+ t := styles.CurrentTheme()
175+ return t.S().Base.
177176 Width(c.width).
178177 Height(c.height).
179− Background(t.BackgroundSecondary())
178+ Background(t.BgSubtle)
180179}
181180
182181func (c *completionsCmp) Open() bool {
internal/tui/components/dialog/filepicker.go+14−18
@@ -19,7 +19,6 @@ import (
1919 "github.com/opencode-ai/opencode/internal/message"
2020 "github.com/opencode-ai/opencode/internal/tui/image"
2121 "github.com/opencode-ai/opencode/internal/tui/styles"
22− "github.com/opencode-ai/opencode/internal/tui/theme"
2322 "github.com/opencode-ai/opencode/internal/tui/util"
2423)
2524
@@ -258,7 +257,8 @@ func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
258257}
259258
260259func (f *filepickerCmp) View() tea.View {
261− t := theme.CurrentTheme()
260+ t := styles.CurrentTheme()
261+ baseStyle := t.S().Base
262262 const maxVisibleDirs = 20
263263 const maxWidth = 80
264264
@@ -286,12 +286,11 @@ func (f *filepickerCmp) View() tea.View {
286286
287287 for i := startIdx; i < endIdx; i++ {
288288 file := f.dirs[i]
289− itemStyle := styles.BaseStyle().Width(adjustedWidth)
289+ itemStyle := t.S().Text.Width(adjustedWidth)
290290
291291 if i == f.cursor {
292292 itemStyle = itemStyle.
293− Background(t.Primary()).
294− Foreground(t.Background()).
293+ Background(t.Primary).
295294 Bold(true)
296295 }
297296 filename := file.Name()
@@ -309,20 +308,18 @@ func (f *filepickerCmp) View() tea.View {
309308
310309 // Pad to always show exactly 21 lines
311310 for len(files) < maxVisibleDirs {
312− files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
311+ files = append(files, baseStyle.Width(adjustedWidth).Render(""))
313312 }
314313
315− currentPath := styles.BaseStyle().
314+ currentPath := baseStyle.
316315 Height(1).
317316 Width(adjustedWidth).
318317 Render(f.cwd.View())
319318
320− viewportstyle := lipgloss.NewStyle().
319+ viewportstyle := baseStyle.
321320 Width(f.viewport.Width()).
322− Background(t.Background()).
323321 Border(lipgloss.RoundedBorder()).
324− BorderForeground(t.TextMuted()).
325− BorderBackground(t.Background()).
322+ BorderForeground(t.BorderFocus).
326323 Padding(2).
327324 Render(f.viewport.View())
328325 var insertExitText string
@@ -335,17 +332,16 @@ func (f *filepickerCmp) View() tea.View {
335332 content := lipgloss.JoinVertical(
336333 lipgloss.Left,
337334 currentPath,
338− styles.BaseStyle().Width(adjustedWidth).Render(""),
339− styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
340− styles.BaseStyle().Width(adjustedWidth).Render(""),
341− styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
335+ baseStyle.Width(adjustedWidth).Render(""),
336+ baseStyle.Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
337+ baseStyle.Width(adjustedWidth).Render(""),
338+ t.S().Muted.Width(adjustedWidth).Render(insertExitText),
342339 )
343340
344341 f.cwd.SetValue(f.cwd.Value())
345− contentStyle := styles.BaseStyle().Padding(1, 2).
342+ contentStyle := baseStyle.Padding(1, 2).
346343 Border(lipgloss.RoundedBorder()).
347− BorderBackground(t.Background()).
348− BorderForeground(t.TextMuted()).
344+ BorderForeground(t.BorderFocus).
349345 Width(lipgloss.Width(content) + 4)
350346
351347 return tea.NewView(
internal/tui/components/dialog/help.godeleted−203
@@ -1,203 +0,0 @@
1−package dialog
2−
3−import (
4− "strings"
5−
6− "github.com/charmbracelet/bubbles/v2/key"
7− tea "github.com/charmbracelet/bubbletea/v2"
8− "github.com/charmbracelet/lipgloss/v2"
9− "github.com/opencode-ai/opencode/internal/tui/styles"
10− "github.com/opencode-ai/opencode/internal/tui/theme"
11− "github.com/opencode-ai/opencode/internal/tui/util"
12−)
13−
14−type helpCmp struct {
15− width int
16− height int
17− keys []key.Binding
18−}
19−
20−func (h *helpCmp) Init() tea.Cmd {
21− return nil
22−}
23−
24−func (h *helpCmp) SetBindings(k []key.Binding) {
25− h.keys = k
26−}
27−
28−func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
29− switch msg := msg.(type) {
30− case tea.WindowSizeMsg:
31− h.width = 90
32− h.height = msg.Height
33− }
34− return h, nil
35−}
36−
37−func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
38− seen := make(map[string]struct{})
39− result := make([]key.Binding, 0, len(bindings))
40−
41− // Process bindings in reverse order
42− for i := len(bindings) - 1; i >= 0; i-- {
43− b := bindings[i]
44− k := strings.Join(b.Keys(), " ")
45− if _, ok := seen[k]; ok {
46− // duplicate, skip
47− continue
48− }
49− seen[k] = struct{}{}
50− // Add to the beginning of result to maintain original order
51− result = append([]key.Binding{b}, result...)
52− }
53−
54− return result
55−}
56−
57−func (h *helpCmp) render() string {
58− t := theme.CurrentTheme()
59− baseStyle := styles.BaseStyle()
60−
61− helpKeyStyle := styles.Bold().
62− Background(t.Background()).
63− Foreground(t.Text()).
64− Padding(0, 1, 0, 0)
65−
66− helpDescStyle := styles.Regular().
67− Background(t.Background()).
68− Foreground(t.TextMuted())
69−
70− // Compile list of bindings to render
71− bindings := removeDuplicateBindings(h.keys)
72−
73− // Enumerate through each group of bindings, populating a series of
74− // pairs of columns, one for keys, one for descriptions
75− var (
76− pairs []string
77− width int
78− rows = 12 - 2
79− )
80−
81− for i := 0; i < len(bindings); i += rows {
82− var (
83− keys []string
84− descs []string
85− )
86− for j := i; j < min(i+rows, len(bindings)); j++ {
87− keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
88− descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
89− }
90−
91− // Render pair of columns; beyond the first pair, render a three space
92− // left margin, in order to visually separate the pairs.
93− var cols []string
94− if len(pairs) > 0 {
95− cols = []string{baseStyle.Render(" ")}
96− }
97−
98− maxDescWidth := 0
99− for _, desc := range descs {
100− if maxDescWidth < lipgloss.Width(desc) {
101− maxDescWidth = lipgloss.Width(desc)
102− }
103− }
104− for i := range descs {
105− remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
106− if remainingWidth > 0 {
107− descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
108− }
109− }
110− maxKeyWidth := 0
111− for _, key := range keys {
112− if maxKeyWidth < lipgloss.Width(key) {
113− maxKeyWidth = lipgloss.Width(key)
114− }
115− }
116− for i := range keys {
117− remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
118− if remainingWidth > 0 {
119− keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
120− }
121− }
122−
123− cols = append(cols,
124− strings.Join(keys, "\n"),
125− strings.Join(descs, "\n"),
126− )
127−
128− pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
129− // check whether it exceeds the maximum width avail (the width of the
130− // terminal, subtracting 2 for the borders).
131− width += lipgloss.Width(pair)
132− if width > h.width-2 {
133− break
134− }
135− pairs = append(pairs, pair)
136− }
137−
138− // https://github.com/charmbracelet/lipgloss/v2/issues/209
139− if len(pairs) > 1 {
140− prefix := pairs[:len(pairs)-1]
141− lastPair := pairs[len(pairs)-1]
142− prefix = append(prefix, lipgloss.Place(
143− lipgloss.Width(lastPair), // width
144− lipgloss.Height(prefix[0]), // height
145− lipgloss.Left, // x
146− lipgloss.Top, // y
147− lastPair, // content
148− lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
149− ))
150− content := baseStyle.Width(h.width).Render(
151− lipgloss.JoinHorizontal(
152− lipgloss.Top,
153− prefix...,
154− ),
155− )
156− return content
157− }
158−
159− // Join pairs of columns and enclose in a border
160− content := baseStyle.Width(h.width).Render(
161− lipgloss.JoinHorizontal(
162− lipgloss.Top,
163− pairs...,
164− ),
165− )
166− return content
167−}
168−
169−func (h *helpCmp) View() tea.View {
170− t := theme.CurrentTheme()
171− baseStyle := styles.BaseStyle()
172−
173− content := h.render()
174− header := baseStyle.
175− Bold(true).
176− Width(lipgloss.Width(content)).
177− Foreground(t.Primary()).
178− Render("Keyboard Shortcuts")
179−
180− return tea.NewView(
181− baseStyle.Padding(1).
182− Border(lipgloss.RoundedBorder()).
183− BorderForeground(t.TextMuted()).
184− Width(h.width).
185− BorderBackground(t.Background()).
186− Render(
187− lipgloss.JoinVertical(lipgloss.Center,
188− header,
189− baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
190− content,
191− ),
192− ),
193− )
194−}
195−
196−type HelpCmp interface {
197− util.Model
198− SetBindings([]key.Binding)
199−}
200−
201−func NewHelpCmp() HelpCmp {
202− return &helpCmp{}
203−}
internal/tui/components/dialog/init.go+12−20
@@ -6,7 +6,6 @@ import (
66 "github.com/charmbracelet/lipgloss/v2"
77
88 "github.com/opencode-ai/opencode/internal/tui/styles"
9− "github.com/opencode-ai/opencode/internal/tui/theme"
109 "github.com/opencode-ai/opencode/internal/tui/util"
1110)
1211
@@ -93,51 +92,45 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9392
9493// View implements tea.Model.
9594func (m InitDialogCmp) View() string {
96− t := theme.CurrentTheme()
97− baseStyle := styles.BaseStyle()
95+ t := styles.CurrentTheme()
96+ baseStyle := t.S().Base
9897
9998 // Calculate width needed for content
10099 maxWidth := 60 // Width for explanation text
101100
102101 title := baseStyle.
103− Foreground(t.Primary()).
102+ Foreground(t.Primary).
104103 Bold(true).
105104 Width(maxWidth).
106105 Padding(0, 1).
107106 Render("Initialize Project")
108107
109− explanation := baseStyle.
110− Foreground(t.Text()).
108+ explanation := t.S().Text.
111109 Width(maxWidth).
112110 Padding(0, 1).
113111 Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
114112
115− question := baseStyle.
116− Foreground(t.Text()).
113+ question := t.S().Text.
117114 Width(maxWidth).
118115 Padding(1, 1).
119116 Render("Would you like to initialize this project?")
120117
121118 maxWidth = min(maxWidth, m.width-10)
122− yesStyle := baseStyle
123− noStyle := baseStyle
119+ yesStyle := t.S().Text
120+ noStyle := yesStyle
124121
125122 if m.selected == 0 {
126123 yesStyle = yesStyle.
127− Background(t.Primary()).
128− Foreground(t.Background()).
124+ Background(t.Primary).
129125 Bold(true)
130126 noStyle = noStyle.
131− Background(t.Background()).
132− Foreground(t.Primary())
127+ Background(t.BgSubtle)
133128 } else {
134129 noStyle = noStyle.
135− Background(t.Primary()).
136− Foreground(t.Background()).
130+ Background(t.Primary).
137131 Bold(true)
138132 yesStyle = yesStyle.
139− Background(t.Background()).
140− Foreground(t.Primary())
133+ Background(t.BgSubtle)
141134 }
142135
143136 yes := yesStyle.Padding(0, 3).Render("Yes")
@@ -161,8 +154,7 @@ func (m InitDialogCmp) View() string {
161154
162155 return baseStyle.Padding(1, 2).
163156 Border(lipgloss.RoundedBorder()).
164− BorderBackground(t.Background()).
165− BorderForeground(t.TextMuted()).
157+ BorderForeground(t.BorderFocus).
166158 Width(lipgloss.Width(content) + 4).
167159 Render(content)
168160}
internal/tui/components/dialog/permission.go+39−51
@@ -13,7 +13,6 @@ import (
1313 "github.com/opencode-ai/opencode/internal/permission"
1414 "github.com/opencode-ai/opencode/internal/tui/layout"
1515 "github.com/opencode-ai/opencode/internal/tui/styles"
16− "github.com/opencode-ai/opencode/internal/tui/theme"
1716 "github.com/opencode-ai/opencode/internal/tui/util"
1817)
1918
@@ -149,28 +148,26 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
149148}
150149
151150func (p *permissionDialogCmp) renderButtons() string {
152− t := theme.CurrentTheme()
153− baseStyle := styles.BaseStyle()
151+ t := styles.CurrentTheme()
154152
155− allowStyle := baseStyle
156− allowSessionStyle := baseStyle
157− denyStyle := baseStyle
158− spacerStyle := baseStyle.Background(t.Background())
153+ allowStyle := t.S().Text
154+ allowSessionStyle := allowStyle
155+ denyStyle := allowStyle
159156
160157 // Style the selected button
161158 switch p.selectedOption {
162159 case 0:
163− allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
164− allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
165− denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
160+ allowStyle = allowStyle.Background(t.Primary)
161+ allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
162+ denyStyle = denyStyle.Background(t.BgSubtle)
166163 case 1:
167− allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
168− allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
169− denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
164+ allowStyle = allowStyle.Background(t.BgSubtle)
165+ allowSessionStyle = allowSessionStyle.Background(t.Primary)
166+ denyStyle = denyStyle.Background(t.BgSubtle)
170167 case 2:
171− allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
172− allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
173− denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
168+ allowStyle = allowStyle.Background(t.BgSubtle)
169+ allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
170+ denyStyle = denyStyle.Background(t.Primary)
174171 }
175172
176173 allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
@@ -180,33 +177,31 @@ func (p *permissionDialogCmp) renderButtons() string {
180177 content := lipgloss.JoinHorizontal(
181178 lipgloss.Left,
182179 allowButton,
183− spacerStyle.Render(" "),
180+ " ",
184181 allowSessionButton,
185− spacerStyle.Render(" "),
182+ " ",
186183 denyButton,
187− spacerStyle.Render(" "),
184+ " ",
188185 )
189186
190187 remainingWidth := p.width - lipgloss.Width(content)
191188 if remainingWidth > 0 {
192− content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
189+ content = strings.Repeat(" ", remainingWidth) + content
193190 }
194191 return content
195192}
196193
197194func (p *permissionDialogCmp) renderHeader() string {
198− t := theme.CurrentTheme()
199− baseStyle := styles.BaseStyle()
195+ t := styles.CurrentTheme()
196+ baseStyle := t.S().Base
200197
201− toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
202− toolValue := baseStyle.
203− Foreground(t.Text()).
198+ toolKey := t.S().Muted.Bold(true).Render("Tool")
199+ toolValue := t.S().Text.
204200 Width(p.width - lipgloss.Width(toolKey)).
205201 Render(fmt.Sprintf(": %s", p.permission.ToolName))
206202
207− pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
208− pathValue := baseStyle.
209− Foreground(t.Text()).
203+ pathKey := t.S().Muted.Bold(true).Render("Path")
204+ pathValue := t.S().Text.
210205 Width(p.width - lipgloss.Width(pathKey)).
211206 Render(fmt.Sprintf(": %s", p.permission.Path))
212207
@@ -228,12 +223,11 @@ func (p *permissionDialogCmp) renderHeader() string {
228223 // Add tool-specific header information
229224 switch p.permission.ToolName {
230225 case tools.BashToolName:
231− headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
226+ headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("Command"))
232227 case tools.EditToolName:
233228 params := p.permission.Params.(tools.EditPermissionsParams)
234− fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
235− filePath := baseStyle.
236− Foreground(t.Text()).
229+ fileKey := t.S().Muted.Bold(true).Render("File")
230+ filePath := t.S().Text.
237231 Width(p.width - lipgloss.Width(fileKey)).
238232 Render(fmt.Sprintf(": %s", params.FilePath))
239233 headerParts = append(headerParts,
@@ -247,9 +241,8 @@ func (p *permissionDialogCmp) renderHeader() string {
247241
248242 case tools.WriteToolName:
249243 params := p.permission.Params.(tools.WritePermissionsParams)
250− fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
251− filePath := baseStyle.
252− Foreground(t.Text()).
244+ fileKey := t.S().Muted.Bold(true).Render("File")
245+ filePath := t.S().Text.
253246 Width(p.width - lipgloss.Width(fileKey)).
254247 Render(fmt.Sprintf(": %s", params.FilePath))
255248 headerParts = append(headerParts,
@@ -261,15 +254,14 @@ func (p *permissionDialogCmp) renderHeader() string {
261254 baseStyle.Render(strings.Repeat(" ", p.width)),
262255 )
263256 case tools.FetchToolName:
264− headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
257+ headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
265258 }
266259
267− return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
260+ return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
268261}
269262
270263func (p *permissionDialogCmp) renderBashContent() string {
271− baseStyle := styles.BaseStyle()
272−
264+ baseStyle := styles.CurrentTheme().S().Base
273265 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
274266 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
275267
@@ -327,8 +319,7 @@ func (p *permissionDialogCmp) renderWriteContent() string {
327319}
328320
329321func (p *permissionDialogCmp) renderFetchContent() string {
330− baseStyle := styles.BaseStyle()
331−
322+ baseStyle := styles.CurrentTheme().S().Base
332323 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
333324 content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
334325
@@ -349,7 +340,7 @@ func (p *permissionDialogCmp) renderFetchContent() string {
349340}
350341
351342func (p *permissionDialogCmp) renderDefaultContent() string {
352− baseStyle := styles.BaseStyle()
343+ baseStyle := styles.CurrentTheme().S().Base
353344
354345 content := p.permission.Description
355346
@@ -373,21 +364,19 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
373364}
374365
375366func (p *permissionDialogCmp) styleViewport() string {
376− t := theme.CurrentTheme()
377− contentStyle := lipgloss.NewStyle().
378− Background(t.Background())
367+ t := styles.CurrentTheme()
379368
380− return contentStyle.Render(p.contentViewPort.View())
369+ return t.S().Base.Render(p.contentViewPort.View())
381370}
382371
383372func (p *permissionDialogCmp) render() string {
384− t := theme.CurrentTheme()
385− baseStyle := styles.BaseStyle()
373+ t := styles.CurrentTheme()
374+ baseStyle := t.S().Base
386375
387376 title := baseStyle.
388377 Bold(true).
389378 Width(p.width - 4).
390− Foreground(t.Primary()).
379+ Foreground(t.Primary).
391380 Render("Permission Required")
392381 // Render header
393382 headerContent := p.renderHeader()
@@ -428,8 +417,7 @@ func (p *permissionDialogCmp) render() string {
428417 return baseStyle.
429418 Padding(1, 0, 0, 1).
430419 Border(lipgloss.RoundedBorder()).
431− BorderBackground(t.Background()).
432− BorderForeground(t.TextMuted()).
420+ BorderForeground(t.BorderFocus).
433421 Width(p.width).
434422 Height(p.height).
435423 Render(
internal/tui/components/dialog/theme.godeleted−201
@@ -1,201 +0,0 @@
1−package dialog
2−
3−import (
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
14−type ThemeChangedMsg struct {
15− ThemeName string
16−}
17−
18−// CloseThemeDialogMsg is sent when the theme dialog is closed
19−type CloseThemeDialogMsg struct{}
20−
21−// ThemeDialog interface for the theme switching dialog
22−type ThemeDialog interface {
23− util.Model
24− layout.Bindings
25−}
26−
27−type themeDialogCmp struct {
28− themes []string
29− selectedIdx int
30− width int
31− height int
32− currentTheme string
33−}
34−
35−type 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−
44−var 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−
71−func (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−
87−func (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−
125−func (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−
190−func (t *themeDialogCmp) BindingKeys() []key.Binding {
191− return layout.KeyMapToSlice(themeKeys)
192−}
193−
194−// NewThemeDialogCmp creates a new theme switching dialog
195−func NewThemeDialogCmp() ThemeDialog {
196− return &themeDialogCmp{
197− themes: []string{},
198− selectedIdx: 0,
199− currentTheme: "",
200− }
201−}
internal/tui/components/dialogs/commands/arguments.go+16−33
@@ -11,7 +11,6 @@ import (
1111 "github.com/charmbracelet/lipgloss/v2"
1212 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
1313 "github.com/opencode-ai/opencode/internal/tui/styles"
14− "github.com/opencode-ai/opencode/internal/tui/theme"
1514 "github.com/opencode-ai/opencode/internal/tui/util"
1615)
1716
@@ -54,7 +53,7 @@ type commandArgumentsDialogCmp struct {
5453}
5554
5655func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
57− t := theme.CurrentTheme()
56+ t := styles.CurrentTheme()
5857 inputs := make([]textinput.Model, len(argNames))
5958
6059 for i, name := range argNames {
@@ -63,15 +62,8 @@ func NewCommandArgumentsDialog(commandID, content string, argNames []string) Com
6362 ti.SetWidth(40)
6463 ti.SetVirtualCursor(false)
6564 ti.Prompt = ""
66− ds := ti.Styles()
67−
68− ds.Blurred.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
69− ds.Blurred.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.TextMuted())
70− ds.Blurred.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.TextMuted())
71− ds.Focused.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
72− ds.Focused.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.Text())
73− ds.Focused.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.Text())
74− ti.SetStyles(ds)
65+
66+ ti.SetStyles(t.S().TextInput)
7567 // Only focus the first input initially
7668 if i == 0 {
7769 ti.Focus()
@@ -148,42 +140,36 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
148140
149141// View implements CommandArgumentsDialog.
150142func (c *commandArgumentsDialogCmp) View() tea.View {
151− t := theme.CurrentTheme()
152− baseStyle := styles.BaseStyle()
143+ t := styles.CurrentTheme()
144+ baseStyle := t.S().Base
153145
154146 title := lipgloss.NewStyle().
155− Foreground(t.Primary()).
147+ Foreground(t.Primary).
156148 Bold(true).
157149 Padding(0, 1).
158− Background(t.Background()).
159150 Render("Command Arguments")
160151
161− explanation := lipgloss.NewStyle().
162− Foreground(t.Text()).
152+ explanation := t.S().Text.
163153 Padding(0, 1).
164− Background(t.Background()).
165154 Render("This command requires arguments.")
166155
167156 // Create input fields for each argument
168157 inputFields := make([]string, len(c.inputs))
169158 for i, input := range c.inputs {
170159 // Highlight the label of the focused input
171− labelStyle := lipgloss.NewStyle().
172− Padding(1, 1, 0, 1).
173− Background(t.Background())
160+ labelStyle := baseStyle.
161+ Padding(1, 1, 0, 1)
174162
175163 if i == c.focusIndex {
176− labelStyle = labelStyle.Foreground(t.Text()).Bold(true)
164+ labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
177165 } else {
178− labelStyle = labelStyle.Foreground(t.TextMuted())
166+ labelStyle = labelStyle.Foreground(t.FgMuted)
179167 }
180168
181169 label := labelStyle.Render(c.argNames[i] + ":")
182170
183− field := lipgloss.NewStyle().
184− Foreground(t.Text()).
171+ field := t.S().Text.
185172 Padding(0, 1).
186− Background(t.Background()).
187173 Render(input.View())
188174
189175 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
@@ -205,9 +191,7 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
205191 view := tea.NewView(
206192 baseStyle.Padding(1, 1, 0, 1).
207193 Border(lipgloss.RoundedBorder()).
208− BorderBackground(t.Background()).
209− BorderForeground(t.TextMuted()).
210− Background(t.Background()).
194+ BorderForeground(t.BorderFocus).
211195 Width(c.width).
212196 Render(content),
213197 )
@@ -228,13 +212,12 @@ func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
228212}
229213
230214func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
231− t := theme.CurrentTheme()
232− return styles.BaseStyle().
215+ t := styles.CurrentTheme()
216+ return t.S().Base.
233217 Width(c.width).
234218 Padding(1).
235219 Border(lipgloss.RoundedBorder()).
236− BorderBackground(t.Background()).
237− BorderForeground(t.TextMuted())
220+ BorderForeground(t.BorderFocus)
238221}
239222
240223func (c *commandArgumentsDialogCmp) Position() (int, int) {
internal/tui/components/dialogs/quit/quit.go+10−14
@@ -7,7 +7,6 @@ import (
77 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
88 "github.com/opencode-ai/opencode/internal/tui/layout"
99 "github.com/opencode-ai/opencode/internal/tui/styles"
10− "github.com/opencode-ai/opencode/internal/tui/theme"
1110 "github.com/opencode-ai/opencode/internal/tui/util"
1211)
1312
@@ -69,26 +68,24 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6968
7069// View renders the quit dialog with Yes/No buttons.
7170func (q *quitDialogCmp) View() tea.View {
72− t := theme.CurrentTheme()
73− baseStyle := styles.BaseStyle()
74−
75− yesStyle := baseStyle
76− noStyle := baseStyle
77− spacerStyle := baseStyle.Background(t.Background())
71+ t := styles.CurrentTheme()
72+ baseStyle := t.S().Base
73+ yesStyle := t.S().Text
74+ noStyle := yesStyle
7875
7976 if q.selectedNo {
80− noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
81− yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
77+ noStyle = noStyle.Background(t.Primary)
78+ yesStyle = yesStyle.Background(t.BgSubtle)
8279 } else {
83− yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
84− noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
80+ yesStyle = yesStyle.Background(t.Primary)
81+ noStyle = noStyle.Background(t.BgSubtle)
8582 }
8683
8784 yesButton := yesStyle.Padding(0, 1).Render("Yes")
8885 noButton := noStyle.Padding(0, 1).Render("No")
8986
9087 buttons := baseStyle.Width(lipgloss.Width(question)).Align(lipgloss.Right).Render(
91− lipgloss.JoinHorizontal(lipgloss.Center, yesButton, spacerStyle.Render(" "), noButton),
88+ lipgloss.JoinHorizontal(lipgloss.Center, yesButton, " ", noButton),
9289 )
9390
9491 content := baseStyle.Render(
@@ -103,8 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
103100 quitDialogStyle := baseStyle.
104101 Padding(1, 2).
105102 Border(lipgloss.RoundedBorder()).
106− BorderBackground(t.Background()).
107− BorderForeground(t.TextMuted())
103+ BorderForeground(t.BorderFocus)
108104
109105 return tea.NewView(
110106 quitDialogStyle.Render(content),
internal/tui/layout/overlay.godeleted−169
@@ -1,169 +0,0 @@
1−package layout
2−
3−import (
4− "strings"
5−
6− "github.com/charmbracelet/lipgloss/v2"
7− chAnsi "github.com/charmbracelet/x/ansi"
8− "github.com/muesli/ansi"
9− "github.com/muesli/reflow/truncate"
10− "github.com/muesli/termenv"
11− "github.com/opencode-ai/opencode/internal/tui/styles"
12− "github.com/opencode-ai/opencode/internal/tui/theme"
13− "github.com/opencode-ai/opencode/internal/tui/util"
14−)
15−
16−// Most of this code is borrowed from
17−// https://github.com/charmbracelet/lipgloss/v2/pull/102
18−// as well as the lipgloss library, with some modification for what I needed.
19−
20−// Split a string into lines, additionally returning the size of the widest
21−// line.
22−func getLines(s string) (lines []string, widest int) {
23− lines = strings.Split(s, "\n")
24−
25− for _, l := range lines {
26− w := ansi.PrintableRuneWidth(l)
27− if widest < w {
28− widest = w
29− }
30− }
31−
32− return lines, widest
33−}
34−
35−// PlaceOverlay places fg on top of bg.
36−func PlaceOverlay(
37− x, y int,
38− fg, bg string,
39− shadow bool, opts ...WhitespaceOption,
40−) string {
41− fgLines, fgWidth := getLines(fg)
42− bgLines, bgWidth := getLines(bg)
43− bgHeight := len(bgLines)
44− fgHeight := len(fgLines)
45−
46− if shadow {
47− t := theme.CurrentTheme()
48− baseStyle := styles.BaseStyle()
49−
50− var shadowbg string = ""
51− shadowchar := lipgloss.NewStyle().
52− Background(t.BackgroundDarker()).
53− Foreground(t.Background()).
54− Render("░")
55− bgchar := baseStyle.Render(" ")
56− for i := 0; i <= fgHeight; i++ {
57− if i == 0 {
58− shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
59− } else {
60− shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
61− }
62− }
63−
64− fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
65− fgLines, fgWidth = getLines(fg)
66− fgHeight = len(fgLines)
67− }
68−
69− if fgWidth >= bgWidth && fgHeight >= bgHeight {
70− // FIXME: return fg or bg?
71− return fg
72− }
73− // TODO: allow placement outside of the bg box?
74− x = util.Clamp(x, 0, bgWidth-fgWidth)
75− y = util.Clamp(y, 0, bgHeight-fgHeight)
76−
77− ws := &whitespace{}
78− for _, opt := range opts {
79− opt(ws)
80− }
81−
82− var b strings.Builder
83− for i, bgLine := range bgLines {
84− if i > 0 {
85− b.WriteByte('\n')
86− }
87− if i < y || i >= y+fgHeight {
88− b.WriteString(bgLine)
89− continue
90− }
91−
92− pos := 0
93− if x > 0 {
94− left := truncate.String(bgLine, uint(x))
95− pos = ansi.PrintableRuneWidth(left)
96− b.WriteString(left)
97− if pos < x {
98− b.WriteString(ws.render(x - pos))
99− pos = x
100− }
101− }
102−
103− fgLine := fgLines[i-y]
104− b.WriteString(fgLine)
105− pos += ansi.PrintableRuneWidth(fgLine)
106−
107− right := cutLeft(bgLine, pos)
108− bgWidth := ansi.PrintableRuneWidth(bgLine)
109− rightWidth := ansi.PrintableRuneWidth(right)
110− if rightWidth <= bgWidth-pos {
111− b.WriteString(ws.render(bgWidth - rightWidth - pos))
112− }
113−
114− b.WriteString(right)
115− }
116−
117− return b.String()
118−}
119−
120−// cutLeft cuts printable characters from the left.
121−// This function is heavily based on muesli's ansi and truncate packages.
122−func cutLeft(s string, cutWidth int) string {
123− return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
124−}
125−
126−func max(a, b int) int {
127− if a > b {
128− return a
129− }
130− return b
131−}
132−
133−type whitespace struct {
134− style termenv.Style
135− chars string
136−}
137−
138−// Render whitespaces.
139−func (w whitespace) render(width int) string {
140− if w.chars == "" {
141− w.chars = " "
142− }
143−
144− r := []rune(w.chars)
145− j := 0
146− b := strings.Builder{}
147−
148− // Cycle through runes and print them into the whitespace.
149− for i := 0; i < width; {
150− b.WriteRune(r[j])
151− j++
152− if j >= len(r) {
153− j = 0
154− }
155− i += ansi.PrintableRuneWidth(string(r[j]))
156− }
157−
158− // Fill any extra gaps white spaces. This might be necessary if any runes
159− // are more than one cell wide, which could leave a one-rune gap.
160− short := width - ansi.PrintableRuneWidth(b.String())
161− if short > 0 {
162− b.WriteString(strings.Repeat(" ", short))
163− }
164−
165− return w.style.Styled(b.String())
166−}
167−
168−// WhitespaceOption sets a styling rule for rendering whitespace.
169−type WhitespaceOption func(*whitespace)
internal/tui/page/logs.go+1−1
@@ -43,7 +43,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4343}
4444
4545func (p *logsPage) View() tea.View {
46− style := styles.BaseStyle().Width(p.width).Height(p.height)
46+ style := styles.CurrentTheme().S().Base.Width(p.width).Height(p.height)
4747 return tea.NewView(
4848 style.Render(
4949 lipgloss.JoinVertical(lipgloss.Top,
internal/tui/styles/markdown.go+2−260
@@ -1,12 +1,7 @@
11package styles
22
33import (
4− "fmt"
5− "image/color"
6−
74 "github.com/charmbracelet/glamour/v2"
8− "github.com/charmbracelet/glamour/v2/ansi"
9− "github.com/opencode-ai/opencode/internal/tui/theme"
105)
116
127// Helper functions for style pointers
@@ -16,263 +11,10 @@ func uintPtr(u uint) *uint { return &u }
1611
1712// returns a glamour TermRenderer configured with the current theme
1813func GetMarkdownRenderer(width int) *glamour.TermRenderer {
14+ t := CurrentTheme()
1915 r, _ := glamour.NewTermRenderer(
20− glamour.WithStyles(generateMarkdownStyleConfig()),
16+ glamour.WithStyles(t.S().Markdown),
2117 glamour.WithWordWrap(width),
2218 )
2319 return r
2420}
25−
26−// creates an ansi.StyleConfig for markdown rendering
27−// using adaptive colors from the provided theme.
28−func generateMarkdownStyleConfig() ansi.StyleConfig {
29− t := theme.CurrentTheme()
30−
31− return ansi.StyleConfig{
32− Document: ansi.StyleBlock{
33− StylePrimitive: ansi.StylePrimitive{
34− Color: stringPtr(colorToString(t.MarkdownText())),
35− },
36− Margin: uintPtr(defaultMargin),
37− },
38− BlockQuote: ansi.StyleBlock{
39− StylePrimitive: ansi.StylePrimitive{
40− Color: stringPtr(colorToString(t.MarkdownBlockQuote())),
41− Italic: boolPtr(true),
42− Prefix: "┃ ",
43− },
44− Indent: uintPtr(1),
45− IndentToken: stringPtr(BaseStyle().Render(" ")),
46− },
47− List: ansi.StyleList{
48− LevelIndent: defaultMargin,
49− StyleBlock: ansi.StyleBlock{
50− IndentToken: stringPtr(BaseStyle().Render(" ")),
51− StylePrimitive: ansi.StylePrimitive{
52− Color: stringPtr(colorToString(t.MarkdownText())),
53− },
54− },
55− },
56− Heading: ansi.StyleBlock{
57− StylePrimitive: ansi.StylePrimitive{
58− BlockSuffix: "\n",
59− Color: stringPtr(colorToString(t.MarkdownHeading())),
60− Bold: boolPtr(true),
61− },
62− },
63− H1: ansi.StyleBlock{
64− StylePrimitive: ansi.StylePrimitive{
65− Prefix: "# ",
66− Color: stringPtr(colorToString(t.MarkdownHeading())),
67− Bold: boolPtr(true),
68− },
69− },
70− H2: ansi.StyleBlock{
71− StylePrimitive: ansi.StylePrimitive{
72− Prefix: "## ",
73− Color: stringPtr(colorToString(t.MarkdownHeading())),
74− Bold: boolPtr(true),
75− },
76− },
77− H3: ansi.StyleBlock{
78− StylePrimitive: ansi.StylePrimitive{
79− Prefix: "### ",
80− Color: stringPtr(colorToString(t.MarkdownHeading())),
81− Bold: boolPtr(true),
82− },
83− },
84− H4: ansi.StyleBlock{
85− StylePrimitive: ansi.StylePrimitive{
86− Prefix: "#### ",
87− Color: stringPtr(colorToString(t.MarkdownHeading())),
88− Bold: boolPtr(true),
89− },
90− },
91− H5: ansi.StyleBlock{
92− StylePrimitive: ansi.StylePrimitive{
93− Prefix: "##### ",
94− Color: stringPtr(colorToString(t.MarkdownHeading())),
95− Bold: boolPtr(true),
96− },
97− },
98− H6: ansi.StyleBlock{
99− StylePrimitive: ansi.StylePrimitive{
100− Prefix: "###### ",
101− Color: stringPtr(colorToString(t.MarkdownHeading())),
102− Bold: boolPtr(true),
103− },
104− },
105− Strikethrough: ansi.StylePrimitive{
106− CrossedOut: boolPtr(true),
107− Color: stringPtr(colorToString(t.TextMuted())),
108− },
109− Emph: ansi.StylePrimitive{
110− Color: stringPtr(colorToString(t.MarkdownEmph())),
111− Italic: boolPtr(true),
112− },
113− Strong: ansi.StylePrimitive{
114− Bold: boolPtr(true),
115− Color: stringPtr(colorToString(t.MarkdownStrong())),
116− },
117− HorizontalRule: ansi.StylePrimitive{
118− Color: stringPtr(colorToString(t.MarkdownHorizontalRule())),
119− Format: "\n─────────────────────────────────────────\n",
120− },
121− Item: ansi.StylePrimitive{
122− BlockPrefix: "• ",
123− Color: stringPtr(colorToString(t.MarkdownListItem())),
124− },
125− Enumeration: ansi.StylePrimitive{
126− BlockPrefix: ". ",
127− Color: stringPtr(colorToString(t.MarkdownListEnumeration())),
128− },
129− Task: ansi.StyleTask{
130− StylePrimitive: ansi.StylePrimitive{},
131− Ticked: "[✓] ",
132− Unticked: "[ ] ",
133− },
134− Link: ansi.StylePrimitive{
135− Color: stringPtr(colorToString(t.MarkdownLink())),
136− Underline: boolPtr(true),
137− },
138− LinkText: ansi.StylePrimitive{
139− Color: stringPtr(colorToString(t.MarkdownLinkText())),
140− Bold: boolPtr(true),
141− },
142− Image: ansi.StylePrimitive{
143− Color: stringPtr(colorToString(t.MarkdownImage())),
144− Underline: boolPtr(true),
145− Format: "🖼 {{.text}}",
146− },
147− ImageText: ansi.StylePrimitive{
148− Color: stringPtr(colorToString(t.MarkdownImageText())),
149− Format: "{{.text}}",
150− },
151− Code: ansi.StyleBlock{
152− StylePrimitive: ansi.StylePrimitive{
153− Color: stringPtr(colorToString(t.MarkdownCode())),
154− Prefix: "",
155− Suffix: "",
156− },
157− },
158− CodeBlock: ansi.StyleCodeBlock{
159− StyleBlock: ansi.StyleBlock{
160− StylePrimitive: ansi.StylePrimitive{
161− Prefix: " ",
162− Color: stringPtr(colorToString(t.MarkdownCodeBlock())),
163− },
164− Margin: uintPtr(defaultMargin),
165− },
166− Chroma: &ansi.Chroma{
167− Text: ansi.StylePrimitive{
168− Color: stringPtr(colorToString(t.MarkdownText())),
169− },
170− Error: ansi.StylePrimitive{
171− Color: stringPtr(colorToString(t.Error())),
172− },
173− Comment: ansi.StylePrimitive{
174− Color: stringPtr(colorToString(t.SyntaxComment())),
175− },
176− CommentPreproc: ansi.StylePrimitive{
177− Color: stringPtr(colorToString(t.SyntaxKeyword())),
178− },
179− Keyword: ansi.StylePrimitive{
180− Color: stringPtr(colorToString(t.SyntaxKeyword())),
181− },
182− KeywordReserved: ansi.StylePrimitive{
183− Color: stringPtr(colorToString(t.SyntaxKeyword())),
184− },
185− KeywordNamespace: ansi.StylePrimitive{
186− Color: stringPtr(colorToString(t.SyntaxKeyword())),
187− },
188− KeywordType: ansi.StylePrimitive{
189− Color: stringPtr(colorToString(t.SyntaxType())),
190− },
191− Operator: ansi.StylePrimitive{
192− Color: stringPtr(colorToString(t.SyntaxOperator())),
193− },
194− Punctuation: ansi.StylePrimitive{
195− Color: stringPtr(colorToString(t.SyntaxPunctuation())),
196− },
197− Name: ansi.StylePrimitive{
198− Color: stringPtr(colorToString(t.SyntaxVariable())),
199− },
200− NameBuiltin: ansi.StylePrimitive{
201− Color: stringPtr(colorToString(t.SyntaxVariable())),
202− },
203− NameTag: ansi.StylePrimitive{
204− Color: stringPtr(colorToString(t.SyntaxKeyword())),
205− },
206− NameAttribute: ansi.StylePrimitive{
207− Color: stringPtr(colorToString(t.SyntaxFunction())),
208− },
209− NameClass: ansi.StylePrimitive{
210− Color: stringPtr(colorToString(t.SyntaxType())),
211− },
212− NameConstant: ansi.StylePrimitive{
213− Color: stringPtr(colorToString(t.SyntaxVariable())),
214− },
215− NameDecorator: ansi.StylePrimitive{
216− Color: stringPtr(colorToString(t.SyntaxFunction())),
217− },
218− NameFunction: ansi.StylePrimitive{
219− Color: stringPtr(colorToString(t.SyntaxFunction())),
220− },
221− LiteralNumber: ansi.StylePrimitive{
222− Color: stringPtr(colorToString(t.SyntaxNumber())),
223− },
224− LiteralString: ansi.StylePrimitive{
225− Color: stringPtr(colorToString(t.SyntaxString())),
226− },
227− LiteralStringEscape: ansi.StylePrimitive{
228− Color: stringPtr(colorToString(t.SyntaxKeyword())),
229− },
230− GenericDeleted: ansi.StylePrimitive{
231− Color: stringPtr(colorToString(t.DiffRemoved())),
232− },
233− GenericEmph: ansi.StylePrimitive{
234− Color: stringPtr(colorToString(t.MarkdownEmph())),
235− Italic: boolPtr(true),
236− },
237− GenericInserted: ansi.StylePrimitive{
238− Color: stringPtr(colorToString(t.DiffAdded())),
239− },
240− GenericStrong: ansi.StylePrimitive{
241− Color: stringPtr(colorToString(t.MarkdownStrong())),
242− Bold: boolPtr(true),
243− },
244− GenericSubheading: ansi.StylePrimitive{
245− Color: stringPtr(colorToString(t.MarkdownHeading())),
246− },
247− },
248− },
249− Table: ansi.StyleTable{
250− StyleBlock: ansi.StyleBlock{
251− StylePrimitive: ansi.StylePrimitive{
252− BlockPrefix: "\n",
253− BlockSuffix: "\n",
254− },
255− },
256− CenterSeparator: stringPtr("┼"),
257− ColumnSeparator: stringPtr("│"),
258− RowSeparator: stringPtr("─"),
259− },
260− DefinitionDescription: ansi.StylePrimitive{
261− BlockPrefix: "\n ❯ ",
262− Color: stringPtr(colorToString(t.MarkdownLinkText())),
263− },
264− Text: ansi.StylePrimitive{
265− Color: stringPtr(colorToString(t.MarkdownText())),
266− },
267− Paragraph: ansi.StyleBlock{
268− StylePrimitive: ansi.StylePrimitive{
269− Color: stringPtr(colorToString(t.MarkdownText())),
270− },
271− },
272− }
273−}
274−
275−func colorToString(c color.Color) string {
276− rgba := color.RGBAModel.Convert(c).(color.RGBA)
277− return fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B)
278−}
internal/tui/styles/styles.godeleted−155
@@ -1,155 +0,0 @@
1−package styles
2−
3−import (
4− "image/color"
5−
6− "github.com/charmbracelet/lipgloss/v2"
7− "github.com/opencode-ai/opencode/internal/tui/theme"
8−)
9−
10−var ImageBakcground = "#212121"
11−
12−// Style generation functions that use the current theme
13−
14−// BaseStyle returns the base style with background and foreground colors
15−func BaseStyle() lipgloss.Style {
16− t := theme.CurrentTheme()
17− return lipgloss.NewStyle().
18− Background(t.Background()).
19− Foreground(t.Text())
20−}
21−
22−// Regular returns a basic unstyled lipgloss.Style
23−func Regular() lipgloss.Style {
24− return lipgloss.NewStyle()
25−}
26−
27−// Bold returns a bold style
28−func Bold() lipgloss.Style {
29− return Regular().Bold(true)
30−}
31−
32−// Padded returns a style with horizontal padding
33−func Padded() lipgloss.Style {
34− return Regular().Padding(0, 1)
35−}
36−
37−// Border returns a style with a normal border
38−func Border() lipgloss.Style {
39− t := theme.CurrentTheme()
40− return Regular().
41− Border(lipgloss.NormalBorder()).
42− BorderForeground(t.BorderNormal())
43−}
44−
45−// ThickBorder returns a style with a thick border
46−func ThickBorder() lipgloss.Style {
47− t := theme.CurrentTheme()
48− return Regular().
49− Border(lipgloss.ThickBorder()).
50− BorderForeground(t.BorderNormal())
51−}
52−
53−// DoubleBorder returns a style with a double border
54−func DoubleBorder() lipgloss.Style {
55− t := theme.CurrentTheme()
56− return Regular().
57− Border(lipgloss.DoubleBorder()).
58− BorderForeground(t.BorderNormal())
59−}
60−
61−// FocusedBorder returns a style with a border using the focused border color
62−func FocusedBorder() lipgloss.Style {
63− t := theme.CurrentTheme()
64− return Regular().
65− Border(lipgloss.NormalBorder()).
66− BorderForeground(t.BorderFocused())
67−}
68−
69−// DimBorder returns a style with a border using the dim border color
70−func DimBorder() lipgloss.Style {
71− t := theme.CurrentTheme()
72− return Regular().
73− Border(lipgloss.NormalBorder()).
74− BorderForeground(t.BorderDim())
75−}
76−
77−// PrimaryColor returns the primary color from the current theme
78−func PrimaryColor() color.Color {
79− return theme.CurrentTheme().Primary()
80−}
81−
82−// SecondaryColor returns the secondary color from the current theme
83−func SecondaryColor() color.Color {
84− return theme.CurrentTheme().Secondary()
85−}
86−
87−// AccentColor returns the accent color from the current theme
88−func AccentColor() color.Color {
89− return theme.CurrentTheme().Accent()
90−}
91−
92−// ErrorColor returns the error color from the current theme
93−func ErrorColor() color.Color {
94− return theme.CurrentTheme().Error()
95−}
96−
97−// WarningColor returns the warning color from the current theme
98−func WarningColor() color.Color {
99− return theme.CurrentTheme().Warning()
100−}
101−
102−// SuccessColor returns the success color from the current theme
103−func SuccessColor() color.Color {
104− return theme.CurrentTheme().Success()
105−}
106−
107−// InfoColor returns the info color from the current theme
108−func InfoColor() color.Color {
109− return theme.CurrentTheme().Info()
110−}
111−
112−// TextColor returns the text color from the current theme
113−func TextColor() color.Color {
114− return theme.CurrentTheme().Text()
115−}
116−
117−// TextMutedColor returns the muted text color from the current theme
118−func TextMutedColor() color.Color {
119− return theme.CurrentTheme().TextMuted()
120−}
121−
122−// TextEmphasizedColor returns the emphasized text color from the current theme
123−func TextEmphasizedColor() color.Color {
124− return theme.CurrentTheme().TextEmphasized()
125−}
126−
127−// BackgroundColor returns the background color from the current theme
128−func BackgroundColor() color.Color {
129− return theme.CurrentTheme().Background()
130−}
131−
132−// BackgroundSecondaryColor returns the secondary background color from the current theme
133−func BackgroundSecondaryColor() color.Color {
134− return theme.CurrentTheme().BackgroundSecondary()
135−}
136−
137−// BackgroundDarkerColor returns the darker background color from the current theme
138−func BackgroundDarkerColor() color.Color {
139− return theme.CurrentTheme().BackgroundDarker()
140−}
141−
142−// BorderNormalColor returns the normal border color from the current theme
143−func BorderNormalColor() color.Color {
144− return theme.CurrentTheme().BorderNormal()
145−}
146−
147−// BorderFocusedColor returns the focused border color from the current theme
148−func BorderFocusedColor() color.Color {
149− return theme.CurrentTheme().BorderFocused()
150−}
151−
152−// BorderDimColor returns the dim border color from the current theme
153−func BorderDimColor() color.Color {
154− return theme.CurrentTheme().BorderDim()
155−}