more theme cleanup

Kujtim Hoxha created

Change summary

cspell.json                                           |   2 
internal/tui/components/chat/chat.go                  |   4 
internal/tui/components/chat/editor/editor.go         |  12 
internal/tui/components/chat/messages/messages.go     |  19 
internal/tui/components/chat/messages/renderer.go     |  40 -
internal/tui/components/chat/messages/tool.go         |  15 
internal/tui/components/completions/completions.go    |   7 
internal/tui/components/dialog/filepicker.go          |  32 
internal/tui/components/dialog/help.go                | 203 ----------
internal/tui/components/dialog/init.go                |  32 
internal/tui/components/dialog/permission.go          |  90 +--
internal/tui/components/dialog/theme.go               | 201 ---------
internal/tui/components/dialogs/commands/arguments.go |  49 -
internal/tui/components/dialogs/quit/quit.go          |  24 
internal/tui/layout/overlay.go                        | 169 --------
internal/tui/page/logs.go                             |   2 
internal/tui/styles/markdown.go                       | 262 ------------
internal/tui/styles/styles.go                         | 155 -------
18 files changed, 134 insertions(+), 1,184 deletions(-)

Detailed changes

cspell.json šŸ”—

@@ -1 +1 @@
-{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps"],"version":"0.2","language":"en","flagWords":[]}
+{"words":["opencode","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph"],"version":"0.2","language":"en","flagWords":[]}

internal/tui/components/chat/chat.go šŸ”—

@@ -14,7 +14,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/session"
 	"github.com/opencode-ai/opencode/internal/tui/components/chat/messages"
 	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
-	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
@@ -87,9 +86,6 @@ func (m *messageListCmp) Init() tea.Cmd {
 // Update handles incoming messages and updates the component state.
 func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case dialog.ThemeChangedMsg:
-		m.listCmp.ResetView()
-		return m, nil
 	case SessionSelectedMsg:
 		if msg.ID != m.session.ID {
 			cmd := m.SetSession(msg)

internal/tui/components/chat/editor/editor.go šŸ”—

@@ -22,7 +22,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -138,9 +137,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case dialog.ThemeChangedMsg:
-		m.textarea = CreateTextArea(&m.textarea)
-		return m, cmd
 	case chat.SessionSelectedMsg:
 		if msg.ID != m.session.ID {
 			m.session = msg
@@ -300,11 +296,11 @@ func (m *editorCmp) GetSize() (int, int) {
 
 func (m *editorCmp) attachmentsContent() string {
 	var styledAttachments []string
-	t := theme.CurrentTheme()
-	attachmentStyles := styles.BaseStyle().
+	t := styles.CurrentTheme()
+	attachmentStyles := t.S().Base.
 		MarginLeft(1).
-		Background(t.TextMuted()).
-		Foreground(t.Text())
+		Background(t.FgMuted).
+		Foreground(t.FgBase)
 	for i, attachment := range m.attachments {
 		var filename string
 		if len(attachment.FileName) > 10 {

internal/tui/components/chat/messages/messages.go šŸ”—

@@ -16,7 +16,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/tui/components/anim"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -124,7 +123,7 @@ func (m *messageCmp) textWidth() int {
 // style returns the lipgloss style for the message component.
 // Applies different border colors and styles based on message role and focus state.
 func (msg *messageCmp) style() lipgloss.Style {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	var borderColor color.Color
 	borderStyle := lipgloss.NormalBorder()
 	if msg.focused {
@@ -133,17 +132,16 @@ func (msg *messageCmp) style() lipgloss.Style {
 
 	switch msg.message.Role {
 	case message.User:
-		borderColor = t.Secondary()
+		borderColor = t.Secondary
 	case message.Assistant:
-		borderColor = t.Primary()
+		borderColor = t.Primary
 	default:
 		// Tool call
-		borderColor = t.TextMuted()
+		borderColor = t.BgSubtle
 	}
 
-	return styles.BaseStyle().
+	return t.S().Muted.
 		BorderLeft(true).
-		Foreground(t.TextMuted()).
 		BorderForeground(borderColor).
 		BorderStyle(borderStyle)
 }
@@ -182,14 +180,13 @@ func (m *messageCmp) renderAssistantMessage() string {
 // renderUserMessage renders user messages with file attachments.
 // Displays message content and any attached files with appropriate icons.
 func (m *messageCmp) renderUserMessage() string {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	parts := []string{
 		m.markdownContent(),
 	}
-	attachmentStyles := styles.BaseStyle().
+	attachmentStyles := t.S().Text.
 		MarginLeft(1).
-		Background(t.BackgroundSecondary()).
-		Foreground(t.Text())
+		Background(t.BgSubtle)
 	attachments := []string{}
 	for _, attachment := range m.message.BinaryContent() {
 		file := filepath.Base(attachment.Path)

internal/tui/components/chat/messages/renderer.go šŸ”—

@@ -15,7 +15,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/llm/agent"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 )
 
 // responseContextHeight limits the number of lines displayed in tool output
@@ -107,7 +106,7 @@ func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []
 	return joinHeaderBody(header, body)
 }
 
-// unmarshalParams safely unmarshals JSON parameters
+// unmarshalParams safely unmarshal JSON parameters
 func (br baseRenderer) unmarshalParams(input string, target any) error {
 	return json.Unmarshal([]byte(input), target)
 }
@@ -593,7 +592,7 @@ func joinHeaderBody(header, body string) string {
 }
 
 func renderPlainContent(v *toolCallCmp, content string) string {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	content = strings.TrimSpace(content)
 	lines := strings.Split(content, "\n")
 
@@ -606,58 +605,55 @@ func renderPlainContent(v *toolCallCmp, content string) string {
 		if len(ln) > v.textWidth() {
 			ln = v.fit(ln, v.textWidth())
 		}
-		out = append(out, lipgloss.NewStyle().
+		out = append(out, t.S().Muted.
 			Width(v.textWidth()).
-			Background(t.BackgroundSecondary()).
-			Foreground(t.TextMuted()).
+			Background(t.BgSubtle).
 			Render(ln))
 	}
 
 	if len(lines) > responseContextHeight {
-		out = append(out, lipgloss.NewStyle().
-			Background(t.BackgroundSecondary()).
-			Foreground(t.TextMuted()).
+		out = append(out, t.S().Muted.
+			Background(t.BgSubtle).
 			Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
 	}
 	return strings.Join(out, "\n")
 }
 
 func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	truncated := truncateHeight(content, responseContextHeight)
 
-	highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BackgroundSecondary())
+	highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle)
 	lines := strings.Split(highlighted, "\n")
 
 	if len(strings.Split(content, "\n")) > responseContextHeight {
-		lines = append(lines, lipgloss.NewStyle().
-			Background(t.BackgroundSecondary()).
-			Foreground(t.TextMuted()).
+		lines = append(lines, t.S().Muted.
+			Background(t.BgSubtle).
 			Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
 	}
 
 	for i, ln := range lines {
-		num := lipgloss.NewStyle().
-			PaddingLeft(4).PaddingRight(2).
-			Background(t.BackgroundSecondary()).
-			Foreground(t.TextMuted()).
+		num := t.S().Muted.
+			Background(t.BgSubtle).
+			PaddingLeft(4).
+			PaddingRight(2).
 			Render(fmt.Sprintf("%d", i+1+offset))
 		w := v.textWidth() - lipgloss.Width(num)
 		lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
 			num,
-			lipgloss.NewStyle().
+			t.S().Base.
 				Width(w).
-				Background(t.BackgroundSecondary()).
+				Background(t.BgSubtle).
 				Render(v.fit(ln, w)))
 	}
 	return lipgloss.JoinVertical(lipgloss.Left, lines...)
 }
 
 func (v *toolCallCmp) renderToolError() string {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	err := strings.ReplaceAll(v.result.Content, "\n", " ")
 	err = fmt.Sprintf("Error: %s", err)
-	return styles.BaseStyle().Foreground(t.Error()).Render(v.fit(err, v.textWidth()))
+	return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()))
 }
 
 func removeWorkingDirPrefix(path string) string {

internal/tui/components/chat/messages/tool.go šŸ”—

@@ -11,7 +11,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/tui/components/anim"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -216,19 +215,17 @@ func (m *toolCallCmp) renderPending() string {
 // style returns the lipgloss style for the tool call component.
 // Applies muted colors and focus-dependent border styles.
 func (m *toolCallCmp) style() lipgloss.Style {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	if m.isNested {
-		return styles.BaseStyle().
-			Foreground(t.TextMuted())
+		return t.S().Muted
 	}
 	borderStyle := lipgloss.NormalBorder()
 	if m.focused {
 		borderStyle = lipgloss.DoubleBorder()
 	}
-	return styles.BaseStyle().
+	return t.S().Muted.
 		BorderLeft(true).
-		Foreground(t.TextMuted()).
-		BorderForeground(t.TextMuted()).
+		BorderForeground(t.Border).
 		BorderStyle(borderStyle)
 }
 
@@ -240,8 +237,8 @@ func (m *toolCallCmp) textWidth() int {
 
 // fit truncates content to fit within the specified width with ellipsis
 func (m *toolCallCmp) fit(content string, width int) string {
-	t := theme.CurrentTheme()
-	lineStyle := lipgloss.NewStyle().Background(t.BackgroundSecondary()).Foreground(t.TextMuted())
+	t := styles.CurrentTheme()
+	lineStyle := t.S().Muted.Background(t.BgSubtle)
 	dots := lineStyle.Render("...")
 	return ansi.Truncate(content, width, dots)
 }

internal/tui/components/completions/completions.go šŸ”—

@@ -6,7 +6,6 @@ import (
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -172,11 +171,11 @@ func (c *completionsCmp) View() tea.View {
 }
 
 func (c *completionsCmp) style() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return styles.BaseStyle().
+	t := styles.CurrentTheme()
+	return t.S().Base.
 		Width(c.width).
 		Height(c.height).
-		Background(t.BackgroundSecondary())
+		Background(t.BgSubtle)
 }
 
 func (c *completionsCmp) Open() bool {

internal/tui/components/dialog/filepicker.go šŸ”—

@@ -19,7 +19,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/tui/image"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -258,7 +257,8 @@ func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
 }
 
 func (f *filepickerCmp) View() tea.View {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
 	const maxVisibleDirs = 20
 	const maxWidth = 80
 
@@ -286,12 +286,11 @@ func (f *filepickerCmp) View() tea.View {
 
 	for i := startIdx; i < endIdx; i++ {
 		file := f.dirs[i]
-		itemStyle := styles.BaseStyle().Width(adjustedWidth)
+		itemStyle := t.S().Text.Width(adjustedWidth)
 
 		if i == f.cursor {
 			itemStyle = itemStyle.
-				Background(t.Primary()).
-				Foreground(t.Background()).
+				Background(t.Primary).
 				Bold(true)
 		}
 		filename := file.Name()
@@ -309,20 +308,18 @@ func (f *filepickerCmp) View() tea.View {
 
 	// Pad to always show exactly 21 lines
 	for len(files) < maxVisibleDirs {
-		files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
+		files = append(files, baseStyle.Width(adjustedWidth).Render(""))
 	}
 
-	currentPath := styles.BaseStyle().
+	currentPath := baseStyle.
 		Height(1).
 		Width(adjustedWidth).
 		Render(f.cwd.View())
 
-	viewportstyle := lipgloss.NewStyle().
+	viewportstyle := baseStyle.
 		Width(f.viewport.Width()).
-		Background(t.Background()).
 		Border(lipgloss.RoundedBorder()).
-		BorderForeground(t.TextMuted()).
-		BorderBackground(t.Background()).
+		BorderForeground(t.BorderFocus).
 		Padding(2).
 		Render(f.viewport.View())
 	var insertExitText string
@@ -335,17 +332,16 @@ func (f *filepickerCmp) View() tea.View {
 	content := lipgloss.JoinVertical(
 		lipgloss.Left,
 		currentPath,
-		styles.BaseStyle().Width(adjustedWidth).Render(""),
-		styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
-		styles.BaseStyle().Width(adjustedWidth).Render(""),
-		styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
+		baseStyle.Width(adjustedWidth).Render(""),
+		baseStyle.Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
+		baseStyle.Width(adjustedWidth).Render(""),
+		t.S().Muted.Width(adjustedWidth).Render(insertExitText),
 	)
 
 	f.cwd.SetValue(f.cwd.Value())
-	contentStyle := styles.BaseStyle().Padding(1, 2).
+	contentStyle := baseStyle.Padding(1, 2).
 		Border(lipgloss.RoundedBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(t.TextMuted()).
+		BorderForeground(t.BorderFocus).
 		Width(lipgloss.Width(content) + 4)
 
 	return tea.NewView(

internal/tui/components/dialog/help.go šŸ”—

@@ -1,203 +0,0 @@
-package dialog
-
-import (
-	"strings"
-
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-	"github.com/opencode-ai/opencode/internal/tui/util"
-)
-
-type helpCmp struct {
-	width  int
-	height int
-	keys   []key.Binding
-}
-
-func (h *helpCmp) Init() tea.Cmd {
-	return nil
-}
-
-func (h *helpCmp) SetBindings(k []key.Binding) {
-	h.keys = k
-}
-
-func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	switch msg := msg.(type) {
-	case tea.WindowSizeMsg:
-		h.width = 90
-		h.height = msg.Height
-	}
-	return h, nil
-}
-
-func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
-	seen := make(map[string]struct{})
-	result := make([]key.Binding, 0, len(bindings))
-
-	// Process bindings in reverse order
-	for i := len(bindings) - 1; i >= 0; i-- {
-		b := bindings[i]
-		k := strings.Join(b.Keys(), " ")
-		if _, ok := seen[k]; ok {
-			// duplicate, skip
-			continue
-		}
-		seen[k] = struct{}{}
-		// Add to the beginning of result to maintain original order
-		result = append([]key.Binding{b}, result...)
-	}
-
-	return result
-}
-
-func (h *helpCmp) render() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	helpKeyStyle := styles.Bold().
-		Background(t.Background()).
-		Foreground(t.Text()).
-		Padding(0, 1, 0, 0)
-
-	helpDescStyle := styles.Regular().
-		Background(t.Background()).
-		Foreground(t.TextMuted())
-
-	// Compile list of bindings to render
-	bindings := removeDuplicateBindings(h.keys)
-
-	// Enumerate through each group of bindings, populating a series of
-	// pairs of columns, one for keys, one for descriptions
-	var (
-		pairs []string
-		width int
-		rows  = 12 - 2
-	)
-
-	for i := 0; i < len(bindings); i += rows {
-		var (
-			keys  []string
-			descs []string
-		)
-		for j := i; j < min(i+rows, len(bindings)); j++ {
-			keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
-			descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
-		}
-
-		// Render pair of columns; beyond the first pair, render a three space
-		// left margin, in order to visually separate the pairs.
-		var cols []string
-		if len(pairs) > 0 {
-			cols = []string{baseStyle.Render("   ")}
-		}
-
-		maxDescWidth := 0
-		for _, desc := range descs {
-			if maxDescWidth < lipgloss.Width(desc) {
-				maxDescWidth = lipgloss.Width(desc)
-			}
-		}
-		for i := range descs {
-			remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
-			if remainingWidth > 0 {
-				descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
-			}
-		}
-		maxKeyWidth := 0
-		for _, key := range keys {
-			if maxKeyWidth < lipgloss.Width(key) {
-				maxKeyWidth = lipgloss.Width(key)
-			}
-		}
-		for i := range keys {
-			remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
-			if remainingWidth > 0 {
-				keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
-			}
-		}
-
-		cols = append(cols,
-			strings.Join(keys, "\n"),
-			strings.Join(descs, "\n"),
-		)
-
-		pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
-		// check whether it exceeds the maximum width avail (the width of the
-		// terminal, subtracting 2 for the borders).
-		width += lipgloss.Width(pair)
-		if width > h.width-2 {
-			break
-		}
-		pairs = append(pairs, pair)
-	}
-
-	// https://github.com/charmbracelet/lipgloss/v2/issues/209
-	if len(pairs) > 1 {
-		prefix := pairs[:len(pairs)-1]
-		lastPair := pairs[len(pairs)-1]
-		prefix = append(prefix, lipgloss.Place(
-			lipgloss.Width(lastPair),   // width
-			lipgloss.Height(prefix[0]), // height
-			lipgloss.Left,              // x
-			lipgloss.Top,               // y
-			lastPair,                   // content
-			lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
-		))
-		content := baseStyle.Width(h.width).Render(
-			lipgloss.JoinHorizontal(
-				lipgloss.Top,
-				prefix...,
-			),
-		)
-		return content
-	}
-
-	// Join pairs of columns and enclose in a border
-	content := baseStyle.Width(h.width).Render(
-		lipgloss.JoinHorizontal(
-			lipgloss.Top,
-			pairs...,
-		),
-	)
-	return content
-}
-
-func (h *helpCmp) View() tea.View {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	content := h.render()
-	header := baseStyle.
-		Bold(true).
-		Width(lipgloss.Width(content)).
-		Foreground(t.Primary()).
-		Render("Keyboard Shortcuts")
-
-	return tea.NewView(
-		baseStyle.Padding(1).
-			Border(lipgloss.RoundedBorder()).
-			BorderForeground(t.TextMuted()).
-			Width(h.width).
-			BorderBackground(t.Background()).
-			Render(
-				lipgloss.JoinVertical(lipgloss.Center,
-					header,
-					baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
-					content,
-				),
-			),
-	)
-}
-
-type HelpCmp interface {
-	util.Model
-	SetBindings([]key.Binding)
-}
-
-func NewHelpCmp() HelpCmp {
-	return &helpCmp{}
-}

internal/tui/components/dialog/init.go šŸ”—

@@ -6,7 +6,6 @@ import (
 	"github.com/charmbracelet/lipgloss/v2"
 
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -93,51 +92,45 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 // View implements tea.Model.
 func (m InitDialogCmp) View() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
 
 	// Calculate width needed for content
 	maxWidth := 60 // Width for explanation text
 
 	title := baseStyle.
-		Foreground(t.Primary()).
+		Foreground(t.Primary).
 		Bold(true).
 		Width(maxWidth).
 		Padding(0, 1).
 		Render("Initialize Project")
 
-	explanation := baseStyle.
-		Foreground(t.Text()).
+	explanation := t.S().Text.
 		Width(maxWidth).
 		Padding(0, 1).
 		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.")
 
-	question := baseStyle.
-		Foreground(t.Text()).
+	question := t.S().Text.
 		Width(maxWidth).
 		Padding(1, 1).
 		Render("Would you like to initialize this project?")
 
 	maxWidth = min(maxWidth, m.width-10)
-	yesStyle := baseStyle
-	noStyle := baseStyle
+	yesStyle := t.S().Text
+	noStyle := yesStyle
 
 	if m.selected == 0 {
 		yesStyle = yesStyle.
-			Background(t.Primary()).
-			Foreground(t.Background()).
+			Background(t.Primary).
 			Bold(true)
 		noStyle = noStyle.
-			Background(t.Background()).
-			Foreground(t.Primary())
+			Background(t.BgSubtle)
 	} else {
 		noStyle = noStyle.
-			Background(t.Primary()).
-			Foreground(t.Background()).
+			Background(t.Primary).
 			Bold(true)
 		yesStyle = yesStyle.
-			Background(t.Background()).
-			Foreground(t.Primary())
+			Background(t.BgSubtle)
 	}
 
 	yes := yesStyle.Padding(0, 3).Render("Yes")
@@ -161,8 +154,7 @@ func (m InitDialogCmp) View() string {
 
 	return baseStyle.Padding(1, 2).
 		Border(lipgloss.RoundedBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(t.TextMuted()).
+		BorderForeground(t.BorderFocus).
 		Width(lipgloss.Width(content) + 4).
 		Render(content)
 }

internal/tui/components/dialog/permission.go šŸ”—

@@ -13,7 +13,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -149,28 +148,26 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
 }
 
 func (p *permissionDialogCmp) renderButtons() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	t := styles.CurrentTheme()
 
-	allowStyle := baseStyle
-	allowSessionStyle := baseStyle
-	denyStyle := baseStyle
-	spacerStyle := baseStyle.Background(t.Background())
+	allowStyle := t.S().Text
+	allowSessionStyle := allowStyle
+	denyStyle := allowStyle
 
 	// Style the selected button
 	switch p.selectedOption {
 	case 0:
-		allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
-		allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
-		denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
+		allowStyle = allowStyle.Background(t.Primary)
+		allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
+		denyStyle = denyStyle.Background(t.BgSubtle)
 	case 1:
-		allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
-		allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
-		denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
+		allowStyle = allowStyle.Background(t.BgSubtle)
+		allowSessionStyle = allowSessionStyle.Background(t.Primary)
+		denyStyle = denyStyle.Background(t.BgSubtle)
 	case 2:
-		allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
-		allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
-		denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
+		allowStyle = allowStyle.Background(t.BgSubtle)
+		allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
+		denyStyle = denyStyle.Background(t.Primary)
 	}
 
 	allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
@@ -180,33 +177,31 @@ func (p *permissionDialogCmp) renderButtons() string {
 	content := lipgloss.JoinHorizontal(
 		lipgloss.Left,
 		allowButton,
-		spacerStyle.Render("  "),
+		"  ",
 		allowSessionButton,
-		spacerStyle.Render("  "),
+		"  ",
 		denyButton,
-		spacerStyle.Render("  "),
+		"  ",
 	)
 
 	remainingWidth := p.width - lipgloss.Width(content)
 	if remainingWidth > 0 {
-		content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
+		content = strings.Repeat(" ", remainingWidth) + content
 	}
 	return content
 }
 
 func (p *permissionDialogCmp) renderHeader() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
 
-	toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
-	toolValue := baseStyle.
-		Foreground(t.Text()).
+	toolKey := t.S().Muted.Bold(true).Render("Tool")
+	toolValue := t.S().Text.
 		Width(p.width - lipgloss.Width(toolKey)).
 		Render(fmt.Sprintf(": %s", p.permission.ToolName))
 
-	pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
-	pathValue := baseStyle.
-		Foreground(t.Text()).
+	pathKey := t.S().Muted.Bold(true).Render("Path")
+	pathValue := t.S().Text.
 		Width(p.width - lipgloss.Width(pathKey)).
 		Render(fmt.Sprintf(": %s", p.permission.Path))
 
@@ -228,12 +223,11 @@ func (p *permissionDialogCmp) renderHeader() string {
 	// Add tool-specific header information
 	switch p.permission.ToolName {
 	case tools.BashToolName:
-		headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
+		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("Command"))
 	case tools.EditToolName:
 		params := p.permission.Params.(tools.EditPermissionsParams)
-		fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
-		filePath := baseStyle.
-			Foreground(t.Text()).
+		fileKey := t.S().Muted.Bold(true).Render("File")
+		filePath := t.S().Text.
 			Width(p.width - lipgloss.Width(fileKey)).
 			Render(fmt.Sprintf(": %s", params.FilePath))
 		headerParts = append(headerParts,
@@ -247,9 +241,8 @@ func (p *permissionDialogCmp) renderHeader() string {
 
 	case tools.WriteToolName:
 		params := p.permission.Params.(tools.WritePermissionsParams)
-		fileKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("File")
-		filePath := baseStyle.
-			Foreground(t.Text()).
+		fileKey := t.S().Muted.Bold(true).Render("File")
+		filePath := t.S().Text.
 			Width(p.width - lipgloss.Width(fileKey)).
 			Render(fmt.Sprintf(": %s", params.FilePath))
 		headerParts = append(headerParts,
@@ -261,15 +254,14 @@ func (p *permissionDialogCmp) renderHeader() string {
 			baseStyle.Render(strings.Repeat(" ", p.width)),
 		)
 	case tools.FetchToolName:
-		headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
+		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
 	}
 
-	return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
+	return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
 }
 
 func (p *permissionDialogCmp) renderBashContent() string {
-	baseStyle := styles.BaseStyle()
-
+	baseStyle := styles.CurrentTheme().S().Base
 	if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
 		content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
 
@@ -327,8 +319,7 @@ func (p *permissionDialogCmp) renderWriteContent() string {
 }
 
 func (p *permissionDialogCmp) renderFetchContent() string {
-	baseStyle := styles.BaseStyle()
-
+	baseStyle := styles.CurrentTheme().S().Base
 	if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
 		content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
 
@@ -349,7 +340,7 @@ func (p *permissionDialogCmp) renderFetchContent() string {
 }
 
 func (p *permissionDialogCmp) renderDefaultContent() string {
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.CurrentTheme().S().Base
 
 	content := p.permission.Description
 
@@ -373,21 +364,19 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
 }
 
 func (p *permissionDialogCmp) styleViewport() string {
-	t := theme.CurrentTheme()
-	contentStyle := lipgloss.NewStyle().
-		Background(t.Background())
+	t := styles.CurrentTheme()
 
-	return contentStyle.Render(p.contentViewPort.View())
+	return t.S().Base.Render(p.contentViewPort.View())
 }
 
 func (p *permissionDialogCmp) render() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
 
 	title := baseStyle.
 		Bold(true).
 		Width(p.width - 4).
-		Foreground(t.Primary()).
+		Foreground(t.Primary).
 		Render("Permission Required")
 	// Render header
 	headerContent := p.renderHeader()
@@ -428,8 +417,7 @@ func (p *permissionDialogCmp) render() string {
 	return baseStyle.
 		Padding(1, 0, 0, 1).
 		Border(lipgloss.RoundedBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(t.TextMuted()).
+		BorderForeground(t.BorderFocus).
 		Width(p.width).
 		Height(p.height).
 		Render(

internal/tui/components/dialog/theme.go šŸ”—

@@ -1,201 +0,0 @@
-package dialog
-
-import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/opencode-ai/opencode/internal/tui/layout"
-	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-	"github.com/opencode-ai/opencode/internal/tui/util"
-)
-
-// ThemeChangedMsg is sent when the theme is changed
-type ThemeChangedMsg struct {
-	ThemeName string
-}
-
-// CloseThemeDialogMsg is sent when the theme dialog is closed
-type CloseThemeDialogMsg struct{}
-
-// ThemeDialog interface for the theme switching dialog
-type ThemeDialog interface {
-	util.Model
-	layout.Bindings
-}
-
-type themeDialogCmp struct {
-	themes       []string
-	selectedIdx  int
-	width        int
-	height       int
-	currentTheme string
-}
-
-type themeKeyMap struct {
-	Up     key.Binding
-	Down   key.Binding
-	Enter  key.Binding
-	Escape key.Binding
-	J      key.Binding
-	K      key.Binding
-}
-
-var themeKeys = themeKeyMap{
-	Up: key.NewBinding(
-		key.WithKeys("up"),
-		key.WithHelp("↑", "previous theme"),
-	),
-	Down: key.NewBinding(
-		key.WithKeys("down"),
-		key.WithHelp("↓", "next theme"),
-	),
-	Enter: key.NewBinding(
-		key.WithKeys("enter"),
-		key.WithHelp("enter", "select theme"),
-	),
-	Escape: key.NewBinding(
-		key.WithKeys("esc"),
-		key.WithHelp("esc", "close"),
-	),
-	J: key.NewBinding(
-		key.WithKeys("j"),
-		key.WithHelp("j", "next theme"),
-	),
-	K: key.NewBinding(
-		key.WithKeys("k"),
-		key.WithHelp("k", "previous theme"),
-	),
-}
-
-func (t *themeDialogCmp) Init() tea.Cmd {
-	// Load available themes and update selectedIdx based on current theme
-	t.themes = theme.AvailableThemes()
-	t.currentTheme = theme.CurrentThemeName()
-
-	// Find the current theme in the list
-	for i, name := range t.themes {
-		if name == t.currentTheme {
-			t.selectedIdx = i
-			break
-		}
-	}
-
-	return nil
-}
-
-func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	switch msg := msg.(type) {
-	case tea.KeyPressMsg:
-		switch {
-		case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
-			if t.selectedIdx > 0 {
-				t.selectedIdx--
-			}
-			return t, nil
-		case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
-			if t.selectedIdx < len(t.themes)-1 {
-				t.selectedIdx++
-			}
-			return t, nil
-		case key.Matches(msg, themeKeys.Enter):
-			if len(t.themes) > 0 {
-				previousTheme := theme.CurrentThemeName()
-				selectedTheme := t.themes[t.selectedIdx]
-				if previousTheme == selectedTheme {
-					return t, util.CmdHandler(CloseThemeDialogMsg{})
-				}
-				if err := theme.SetTheme(selectedTheme); err != nil {
-					return t, util.ReportError(err)
-				}
-				return t, util.CmdHandler(ThemeChangedMsg{
-					ThemeName: selectedTheme,
-				})
-			}
-		case key.Matches(msg, themeKeys.Escape):
-			return t, util.CmdHandler(CloseThemeDialogMsg{})
-		}
-	case tea.WindowSizeMsg:
-		t.width = msg.Width
-		t.height = msg.Height
-	}
-	return t, nil
-}
-
-func (t *themeDialogCmp) View() tea.View {
-	currentTheme := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	if len(t.themes) == 0 {
-		return tea.NewView(
-			baseStyle.Padding(1, 2).
-				Border(lipgloss.RoundedBorder()).
-				BorderBackground(currentTheme.Background()).
-				BorderForeground(currentTheme.TextMuted()).
-				Width(40).
-				Render("No themes available"),
-		)
-	}
-
-	// Calculate max width needed for theme names
-	maxWidth := 40 // Minimum width
-	for _, themeName := range t.themes {
-		if len(themeName) > maxWidth-4 { // Account for padding
-			maxWidth = len(themeName) + 4
-		}
-	}
-
-	maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
-
-	// Build the theme list
-	themeItems := make([]string, 0, len(t.themes))
-	for i, themeName := range t.themes {
-		itemStyle := baseStyle.Width(maxWidth)
-
-		if i == t.selectedIdx {
-			itemStyle = itemStyle.
-				Background(currentTheme.Primary()).
-				Foreground(currentTheme.Background()).
-				Bold(true)
-		}
-
-		themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
-	}
-
-	title := baseStyle.
-		Foreground(currentTheme.Primary()).
-		Bold(true).
-		Width(maxWidth).
-		Padding(0, 1).
-		Render("Select Theme")
-
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		title,
-		baseStyle.Width(maxWidth).Render(""),
-		baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
-		baseStyle.Width(maxWidth).Render(""),
-	)
-
-	return tea.NewView(
-		baseStyle.Padding(1, 2).
-			Border(lipgloss.RoundedBorder()).
-			BorderBackground(currentTheme.Background()).
-			BorderForeground(currentTheme.TextMuted()).
-			Width(lipgloss.Width(content) + 4).
-			Render(content),
-	)
-}
-
-func (t *themeDialogCmp) BindingKeys() []key.Binding {
-	return layout.KeyMapToSlice(themeKeys)
-}
-
-// NewThemeDialogCmp creates a new theme switching dialog
-func NewThemeDialogCmp() ThemeDialog {
-	return &themeDialogCmp{
-		themes:       []string{},
-		selectedIdx:  0,
-		currentTheme: "",
-	}
-}

internal/tui/components/dialogs/commands/arguments.go šŸ”—

@@ -11,7 +11,6 @@ import (
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -54,7 +53,7 @@ type commandArgumentsDialogCmp struct {
 }
 
 func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
-	t := theme.CurrentTheme()
+	t := styles.CurrentTheme()
 	inputs := make([]textinput.Model, len(argNames))
 
 	for i, name := range argNames {
@@ -63,15 +62,8 @@ func NewCommandArgumentsDialog(commandID, content string, argNames []string) Com
 		ti.SetWidth(40)
 		ti.SetVirtualCursor(false)
 		ti.Prompt = ""
-		ds := ti.Styles()
-
-		ds.Blurred.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
-		ds.Blurred.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.TextMuted())
-		ds.Blurred.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.TextMuted())
-		ds.Focused.Placeholder = ds.Blurred.Placeholder.Background(t.Background()).Foreground(t.TextMuted())
-		ds.Focused.Prompt = ds.Blurred.Prompt.Background(t.Background()).Foreground(t.Text())
-		ds.Focused.Text = ds.Blurred.Text.Background(t.Background()).Foreground(t.Text())
-		ti.SetStyles(ds)
+
+		ti.SetStyles(t.S().TextInput)
 		// Only focus the first input initially
 		if i == 0 {
 			ti.Focus()
@@ -148,42 +140,36 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 // View implements CommandArgumentsDialog.
 func (c *commandArgumentsDialogCmp) View() tea.View {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
 
 	title := lipgloss.NewStyle().
-		Foreground(t.Primary()).
+		Foreground(t.Primary).
 		Bold(true).
 		Padding(0, 1).
-		Background(t.Background()).
 		Render("Command Arguments")
 
-	explanation := lipgloss.NewStyle().
-		Foreground(t.Text()).
+	explanation := t.S().Text.
 		Padding(0, 1).
-		Background(t.Background()).
 		Render("This command requires arguments.")
 
 	// Create input fields for each argument
 	inputFields := make([]string, len(c.inputs))
 	for i, input := range c.inputs {
 		// Highlight the label of the focused input
-		labelStyle := lipgloss.NewStyle().
-			Padding(1, 1, 0, 1).
-			Background(t.Background())
+		labelStyle := baseStyle.
+			Padding(1, 1, 0, 1)
 
 		if i == c.focusIndex {
-			labelStyle = labelStyle.Foreground(t.Text()).Bold(true)
+			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
 		} else {
-			labelStyle = labelStyle.Foreground(t.TextMuted())
+			labelStyle = labelStyle.Foreground(t.FgMuted)
 		}
 
 		label := labelStyle.Render(c.argNames[i] + ":")
 
-		field := lipgloss.NewStyle().
-			Foreground(t.Text()).
+		field := t.S().Text.
 			Padding(0, 1).
-			Background(t.Background()).
 			Render(input.View())
 
 		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
@@ -205,9 +191,7 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
 	view := tea.NewView(
 		baseStyle.Padding(1, 1, 0, 1).
 			Border(lipgloss.RoundedBorder()).
-			BorderBackground(t.Background()).
-			BorderForeground(t.TextMuted()).
-			Background(t.Background()).
+			BorderForeground(t.BorderFocus).
 			Width(c.width).
 			Render(content),
 	)
@@ -228,13 +212,12 @@ func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
 }
 
 func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return styles.BaseStyle().
+	t := styles.CurrentTheme()
+	return t.S().Base.
 		Width(c.width).
 		Padding(1).
 		Border(lipgloss.RoundedBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(t.TextMuted())
+		BorderForeground(t.BorderFocus)
 }
 
 func (c *commandArgumentsDialogCmp) Position() (int, int) {

internal/tui/components/dialogs/quit/quit.go šŸ”—

@@ -7,7 +7,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 
@@ -69,26 +68,24 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 // View renders the quit dialog with Yes/No buttons.
 func (q *quitDialogCmp) View() tea.View {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	yesStyle := baseStyle
-	noStyle := baseStyle
-	spacerStyle := baseStyle.Background(t.Background())
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base
+	yesStyle := t.S().Text
+	noStyle := yesStyle
 
 	if q.selectedNo {
-		noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
-		yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
+		noStyle = noStyle.Background(t.Primary)
+		yesStyle = yesStyle.Background(t.BgSubtle)
 	} else {
-		yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
-		noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
+		yesStyle = yesStyle.Background(t.Primary)
+		noStyle = noStyle.Background(t.BgSubtle)
 	}
 
 	yesButton := yesStyle.Padding(0, 1).Render("Yes")
 	noButton := noStyle.Padding(0, 1).Render("No")
 
 	buttons := baseStyle.Width(lipgloss.Width(question)).Align(lipgloss.Right).Render(
-		lipgloss.JoinHorizontal(lipgloss.Center, yesButton, spacerStyle.Render("  "), noButton),
+		lipgloss.JoinHorizontal(lipgloss.Center, yesButton, "  ", noButton),
 	)
 
 	content := baseStyle.Render(
@@ -103,8 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
 	quitDialogStyle := baseStyle.
 		Padding(1, 2).
 		Border(lipgloss.RoundedBorder()).
-		BorderBackground(t.Background()).
-		BorderForeground(t.TextMuted())
+		BorderForeground(t.BorderFocus)
 
 	return tea.NewView(
 		quitDialogStyle.Render(content),

internal/tui/layout/overlay.go šŸ”—

@@ -1,169 +0,0 @@
-package layout
-
-import (
-	"strings"
-
-	"github.com/charmbracelet/lipgloss/v2"
-	chAnsi "github.com/charmbracelet/x/ansi"
-	"github.com/muesli/ansi"
-	"github.com/muesli/reflow/truncate"
-	"github.com/muesli/termenv"
-	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-	"github.com/opencode-ai/opencode/internal/tui/util"
-)
-
-// Most of this code is borrowed from
-// https://github.com/charmbracelet/lipgloss/v2/pull/102
-// as well as the lipgloss library, with some modification for what I needed.
-
-// Split a string into lines, additionally returning the size of the widest
-// line.
-func getLines(s string) (lines []string, widest int) {
-	lines = strings.Split(s, "\n")
-
-	for _, l := range lines {
-		w := ansi.PrintableRuneWidth(l)
-		if widest < w {
-			widest = w
-		}
-	}
-
-	return lines, widest
-}
-
-// PlaceOverlay places fg on top of bg.
-func PlaceOverlay(
-	x, y int,
-	fg, bg string,
-	shadow bool, opts ...WhitespaceOption,
-) string {
-	fgLines, fgWidth := getLines(fg)
-	bgLines, bgWidth := getLines(bg)
-	bgHeight := len(bgLines)
-	fgHeight := len(fgLines)
-
-	if shadow {
-		t := theme.CurrentTheme()
-		baseStyle := styles.BaseStyle()
-
-		var shadowbg string = ""
-		shadowchar := lipgloss.NewStyle().
-			Background(t.BackgroundDarker()).
-			Foreground(t.Background()).
-			Render("ā–‘")
-		bgchar := baseStyle.Render(" ")
-		for i := 0; i <= fgHeight; i++ {
-			if i == 0 {
-				shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
-			} else {
-				shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
-			}
-		}
-
-		fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
-		fgLines, fgWidth = getLines(fg)
-		fgHeight = len(fgLines)
-	}
-
-	if fgWidth >= bgWidth && fgHeight >= bgHeight {
-		// FIXME: return fg or bg?
-		return fg
-	}
-	// TODO: allow placement outside of the bg box?
-	x = util.Clamp(x, 0, bgWidth-fgWidth)
-	y = util.Clamp(y, 0, bgHeight-fgHeight)
-
-	ws := &whitespace{}
-	for _, opt := range opts {
-		opt(ws)
-	}
-
-	var b strings.Builder
-	for i, bgLine := range bgLines {
-		if i > 0 {
-			b.WriteByte('\n')
-		}
-		if i < y || i >= y+fgHeight {
-			b.WriteString(bgLine)
-			continue
-		}
-
-		pos := 0
-		if x > 0 {
-			left := truncate.String(bgLine, uint(x))
-			pos = ansi.PrintableRuneWidth(left)
-			b.WriteString(left)
-			if pos < x {
-				b.WriteString(ws.render(x - pos))
-				pos = x
-			}
-		}
-
-		fgLine := fgLines[i-y]
-		b.WriteString(fgLine)
-		pos += ansi.PrintableRuneWidth(fgLine)
-
-		right := cutLeft(bgLine, pos)
-		bgWidth := ansi.PrintableRuneWidth(bgLine)
-		rightWidth := ansi.PrintableRuneWidth(right)
-		if rightWidth <= bgWidth-pos {
-			b.WriteString(ws.render(bgWidth - rightWidth - pos))
-		}
-
-		b.WriteString(right)
-	}
-
-	return b.String()
-}
-
-// cutLeft cuts printable characters from the left.
-// This function is heavily based on muesli's ansi and truncate packages.
-func cutLeft(s string, cutWidth int) string {
-	return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
-}
-
-func max(a, b int) int {
-	if a > b {
-		return a
-	}
-	return b
-}
-
-type whitespace struct {
-	style termenv.Style
-	chars string
-}
-
-// Render whitespaces.
-func (w whitespace) render(width int) string {
-	if w.chars == "" {
-		w.chars = " "
-	}
-
-	r := []rune(w.chars)
-	j := 0
-	b := strings.Builder{}
-
-	// Cycle through runes and print them into the whitespace.
-	for i := 0; i < width; {
-		b.WriteRune(r[j])
-		j++
-		if j >= len(r) {
-			j = 0
-		}
-		i += ansi.PrintableRuneWidth(string(r[j]))
-	}
-
-	// Fill any extra gaps white spaces. This might be necessary if any runes
-	// are more than one cell wide, which could leave a one-rune gap.
-	short := width - ansi.PrintableRuneWidth(b.String())
-	if short > 0 {
-		b.WriteString(strings.Repeat(" ", short))
-	}
-
-	return w.style.Styled(b.String())
-}
-
-// WhitespaceOption sets a styling rule for rendering whitespace.
-type WhitespaceOption func(*whitespace)

internal/tui/page/logs.go šŸ”—

@@ -43,7 +43,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 func (p *logsPage) View() tea.View {
-	style := styles.BaseStyle().Width(p.width).Height(p.height)
+	style := styles.CurrentTheme().S().Base.Width(p.width).Height(p.height)
 	return tea.NewView(
 		style.Render(
 			lipgloss.JoinVertical(lipgloss.Top,

internal/tui/styles/markdown.go šŸ”—

@@ -1,12 +1,7 @@
 package styles
 
 import (
-	"fmt"
-	"image/color"
-
 	"github.com/charmbracelet/glamour/v2"
-	"github.com/charmbracelet/glamour/v2/ansi"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
 )
 
 // Helper functions for style pointers
@@ -16,263 +11,10 @@ func uintPtr(u uint) *uint       { return &u }
 
 // returns a glamour TermRenderer configured with the current theme
 func GetMarkdownRenderer(width int) *glamour.TermRenderer {
+	t := CurrentTheme()
 	r, _ := glamour.NewTermRenderer(
-		glamour.WithStyles(generateMarkdownStyleConfig()),
+		glamour.WithStyles(t.S().Markdown),
 		glamour.WithWordWrap(width),
 	)
 	return r
 }
-
-// creates an ansi.StyleConfig for markdown rendering
-// using adaptive colors from the provided theme.
-func generateMarkdownStyleConfig() ansi.StyleConfig {
-	t := theme.CurrentTheme()
-
-	return ansi.StyleConfig{
-		Document: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Color: stringPtr(colorToString(t.MarkdownText())),
-			},
-			Margin: uintPtr(defaultMargin),
-		},
-		BlockQuote: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Color:  stringPtr(colorToString(t.MarkdownBlockQuote())),
-				Italic: boolPtr(true),
-				Prefix: "ā”ƒ ",
-			},
-			Indent:      uintPtr(1),
-			IndentToken: stringPtr(BaseStyle().Render(" ")),
-		},
-		List: ansi.StyleList{
-			LevelIndent: defaultMargin,
-			StyleBlock: ansi.StyleBlock{
-				IndentToken: stringPtr(BaseStyle().Render(" ")),
-				StylePrimitive: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.MarkdownText())),
-				},
-			},
-		},
-		Heading: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				BlockSuffix: "\n",
-				Color:       stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:        boolPtr(true),
-			},
-		},
-		H1: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "# ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		H2: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "## ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		H3: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "### ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		H4: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "#### ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		H5: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "##### ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		H6: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Prefix: "###### ",
-				Color:  stringPtr(colorToString(t.MarkdownHeading())),
-				Bold:   boolPtr(true),
-			},
-		},
-		Strikethrough: ansi.StylePrimitive{
-			CrossedOut: boolPtr(true),
-			Color:      stringPtr(colorToString(t.TextMuted())),
-		},
-		Emph: ansi.StylePrimitive{
-			Color:  stringPtr(colorToString(t.MarkdownEmph())),
-			Italic: boolPtr(true),
-		},
-		Strong: ansi.StylePrimitive{
-			Bold:  boolPtr(true),
-			Color: stringPtr(colorToString(t.MarkdownStrong())),
-		},
-		HorizontalRule: ansi.StylePrimitive{
-			Color:  stringPtr(colorToString(t.MarkdownHorizontalRule())),
-			Format: "\n─────────────────────────────────────────\n",
-		},
-		Item: ansi.StylePrimitive{
-			BlockPrefix: "• ",
-			Color:       stringPtr(colorToString(t.MarkdownListItem())),
-		},
-		Enumeration: ansi.StylePrimitive{
-			BlockPrefix: ". ",
-			Color:       stringPtr(colorToString(t.MarkdownListEnumeration())),
-		},
-		Task: ansi.StyleTask{
-			StylePrimitive: ansi.StylePrimitive{},
-			Ticked:         "[āœ“] ",
-			Unticked:       "[ ] ",
-		},
-		Link: ansi.StylePrimitive{
-			Color:     stringPtr(colorToString(t.MarkdownLink())),
-			Underline: boolPtr(true),
-		},
-		LinkText: ansi.StylePrimitive{
-			Color: stringPtr(colorToString(t.MarkdownLinkText())),
-			Bold:  boolPtr(true),
-		},
-		Image: ansi.StylePrimitive{
-			Color:     stringPtr(colorToString(t.MarkdownImage())),
-			Underline: boolPtr(true),
-			Format:    "šŸ–¼ {{.text}}",
-		},
-		ImageText: ansi.StylePrimitive{
-			Color:  stringPtr(colorToString(t.MarkdownImageText())),
-			Format: "{{.text}}",
-		},
-		Code: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Color:  stringPtr(colorToString(t.MarkdownCode())),
-				Prefix: "",
-				Suffix: "",
-			},
-		},
-		CodeBlock: ansi.StyleCodeBlock{
-			StyleBlock: ansi.StyleBlock{
-				StylePrimitive: ansi.StylePrimitive{
-					Prefix: " ",
-					Color:  stringPtr(colorToString(t.MarkdownCodeBlock())),
-				},
-				Margin: uintPtr(defaultMargin),
-			},
-			Chroma: &ansi.Chroma{
-				Text: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.MarkdownText())),
-				},
-				Error: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.Error())),
-				},
-				Comment: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxComment())),
-				},
-				CommentPreproc: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				Keyword: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				KeywordReserved: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				KeywordNamespace: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				KeywordType: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxType())),
-				},
-				Operator: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxOperator())),
-				},
-				Punctuation: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxPunctuation())),
-				},
-				Name: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxVariable())),
-				},
-				NameBuiltin: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxVariable())),
-				},
-				NameTag: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				NameAttribute: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxFunction())),
-				},
-				NameClass: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxType())),
-				},
-				NameConstant: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxVariable())),
-				},
-				NameDecorator: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxFunction())),
-				},
-				NameFunction: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxFunction())),
-				},
-				LiteralNumber: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxNumber())),
-				},
-				LiteralString: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxString())),
-				},
-				LiteralStringEscape: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.SyntaxKeyword())),
-				},
-				GenericDeleted: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.DiffRemoved())),
-				},
-				GenericEmph: ansi.StylePrimitive{
-					Color:  stringPtr(colorToString(t.MarkdownEmph())),
-					Italic: boolPtr(true),
-				},
-				GenericInserted: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.DiffAdded())),
-				},
-				GenericStrong: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.MarkdownStrong())),
-					Bold:  boolPtr(true),
-				},
-				GenericSubheading: ansi.StylePrimitive{
-					Color: stringPtr(colorToString(t.MarkdownHeading())),
-				},
-			},
-		},
-		Table: ansi.StyleTable{
-			StyleBlock: ansi.StyleBlock{
-				StylePrimitive: ansi.StylePrimitive{
-					BlockPrefix: "\n",
-					BlockSuffix: "\n",
-				},
-			},
-			CenterSeparator: stringPtr("┼"),
-			ColumnSeparator: stringPtr("│"),
-			RowSeparator:    stringPtr("─"),
-		},
-		DefinitionDescription: ansi.StylePrimitive{
-			BlockPrefix: "\n āÆ ",
-			Color:       stringPtr(colorToString(t.MarkdownLinkText())),
-		},
-		Text: ansi.StylePrimitive{
-			Color: stringPtr(colorToString(t.MarkdownText())),
-		},
-		Paragraph: ansi.StyleBlock{
-			StylePrimitive: ansi.StylePrimitive{
-				Color: stringPtr(colorToString(t.MarkdownText())),
-			},
-		},
-	}
-}
-
-func colorToString(c color.Color) string {
-	rgba := color.RGBAModel.Convert(c).(color.RGBA)
-	return fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B)
-}

internal/tui/styles/styles.go šŸ”—

@@ -1,155 +0,0 @@
-package styles
-
-import (
-	"image/color"
-
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-)
-
-var ImageBakcground = "#212121"
-
-// Style generation functions that use the current theme
-
-// BaseStyle returns the base style with background and foreground colors
-func BaseStyle() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return lipgloss.NewStyle().
-		Background(t.Background()).
-		Foreground(t.Text())
-}
-
-// Regular returns a basic unstyled lipgloss.Style
-func Regular() lipgloss.Style {
-	return lipgloss.NewStyle()
-}
-
-// Bold returns a bold style
-func Bold() lipgloss.Style {
-	return Regular().Bold(true)
-}
-
-// Padded returns a style with horizontal padding
-func Padded() lipgloss.Style {
-	return Regular().Padding(0, 1)
-}
-
-// Border returns a style with a normal border
-func Border() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.BorderNormal())
-}
-
-// ThickBorder returns a style with a thick border
-func ThickBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.ThickBorder()).
-		BorderForeground(t.BorderNormal())
-}
-
-// DoubleBorder returns a style with a double border
-func DoubleBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.DoubleBorder()).
-		BorderForeground(t.BorderNormal())
-}
-
-// FocusedBorder returns a style with a border using the focused border color
-func FocusedBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.BorderFocused())
-}
-
-// DimBorder returns a style with a border using the dim border color
-func DimBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.BorderDim())
-}
-
-// PrimaryColor returns the primary color from the current theme
-func PrimaryColor() color.Color {
-	return theme.CurrentTheme().Primary()
-}
-
-// SecondaryColor returns the secondary color from the current theme
-func SecondaryColor() color.Color {
-	return theme.CurrentTheme().Secondary()
-}
-
-// AccentColor returns the accent color from the current theme
-func AccentColor() color.Color {
-	return theme.CurrentTheme().Accent()
-}
-
-// ErrorColor returns the error color from the current theme
-func ErrorColor() color.Color {
-	return theme.CurrentTheme().Error()
-}
-
-// WarningColor returns the warning color from the current theme
-func WarningColor() color.Color {
-	return theme.CurrentTheme().Warning()
-}
-
-// SuccessColor returns the success color from the current theme
-func SuccessColor() color.Color {
-	return theme.CurrentTheme().Success()
-}
-
-// InfoColor returns the info color from the current theme
-func InfoColor() color.Color {
-	return theme.CurrentTheme().Info()
-}
-
-// TextColor returns the text color from the current theme
-func TextColor() color.Color {
-	return theme.CurrentTheme().Text()
-}
-
-// TextMutedColor returns the muted text color from the current theme
-func TextMutedColor() color.Color {
-	return theme.CurrentTheme().TextMuted()
-}
-
-// TextEmphasizedColor returns the emphasized text color from the current theme
-func TextEmphasizedColor() color.Color {
-	return theme.CurrentTheme().TextEmphasized()
-}
-
-// BackgroundColor returns the background color from the current theme
-func BackgroundColor() color.Color {
-	return theme.CurrentTheme().Background()
-}
-
-// BackgroundSecondaryColor returns the secondary background color from the current theme
-func BackgroundSecondaryColor() color.Color {
-	return theme.CurrentTheme().BackgroundSecondary()
-}
-
-// BackgroundDarkerColor returns the darker background color from the current theme
-func BackgroundDarkerColor() color.Color {
-	return theme.CurrentTheme().BackgroundDarker()
-}
-
-// BorderNormalColor returns the normal border color from the current theme
-func BorderNormalColor() color.Color {
-	return theme.CurrentTheme().BorderNormal()
-}
-
-// BorderFocusedColor returns the focused border color from the current theme
-func BorderFocusedColor() color.Color {
-	return theme.CurrentTheme().BorderFocused()
-}
-
-// BorderDimColor returns the dim border color from the current theme
-func BorderDimColor() color.Color {
-	return theme.CurrentTheme().BorderDim()
-}