feat: default system theme (#419)

Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
This commit is contained in:
Adam 2025-06-26 10:16:07 -05:00 committed by Jay V
parent 31b56e5a05
commit 2e26b58d16
33 changed files with 1214 additions and 429 deletions

View file

@ -66,6 +66,7 @@ func main() {
program := tea.NewProgram( program := tea.NewProgram(
tui.NewModel(app_), tui.NewModel(app_),
// tea.WithColorProfile(colorprofile.ANSI),
tea.WithAltScreen(), tea.WithAltScreen(),
tea.WithKeyboardEnhancements(), tea.WithKeyboardEnhancements(),
tea.WithMouseCellMotion(), tea.WithMouseCellMotion(),

View file

@ -14,6 +14,7 @@ import (
"github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client" "github.com/sst/opencode/pkg/client"
@ -103,6 +104,12 @@ func New(
} }
if appState.Theme != "" { if appState.Theme != "" {
if appState.Theme == "system" && styles.Terminal != nil {
theme.UpdateSystemTheme(
styles.Terminal.Background,
styles.Terminal.BackgroundIsDark,
)
}
theme.SetTheme(appState.Theme) theme.SetTheme(appState.Theme)
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
@ -37,7 +38,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI { func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
spacer := strings.Repeat(" ", space) spacer := strings.Repeat(" ", space)
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description) title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
value := string(cmd.Name) value := string(cmd.Name)
return dialog.NewCompletionItem(dialog.CompletionItem{ return dialog.NewCompletionItem(dialog.CompletionItem{
Title: title, Title: title,

View file

@ -26,6 +26,9 @@ type EditorComponent interface {
Content() string Content() string
Lines() int Lines() int
Value() string Value() string
Focused() bool
Focus() (tea.Model, tea.Cmd)
Blur()
Submit() (tea.Model, tea.Cmd) Submit() (tea.Model, tea.Cmd)
Clear() (tea.Model, tea.Cmd) Clear() (tea.Model, tea.Cmd)
Paste() (tea.Model, tea.Cmd) Paste() (tea.Model, tea.Cmd)
@ -48,7 +51,7 @@ type editorComponent struct {
} }
func (m *editorComponent) Init() tea.Cmd { func (m *editorComponent) Init() tea.Cmd {
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus) return tea.Batch(m.textarea.Focus(), m.spinner.Tick, tea.EnableReportFocus)
} }
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@ -69,7 +72,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ThemeSelectedMsg: case dialog.ThemeSelectedMsg:
m.textarea = createTextArea(&m.textarea) m.textarea = createTextArea(&m.textarea)
m.spinner = createSpinner() m.spinner = createSpinner()
return m, tea.Batch(m.spinner.Tick, textarea.Blink) return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
case dialog.CompletionSelectedMsg: case dialog.CompletionSelectedMsg:
if msg.IsCommand { if msg.IsCommand {
commandName := strings.TrimPrefix(msg.CompletionValue, "/") commandName := strings.TrimPrefix(msg.CompletionValue, "/")
@ -104,12 +107,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *editorComponent) Content() string { func (m *editorComponent) Content() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
base := styles.BaseStyle().Background(t.Background()).Render base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.Muted().Background(t.Background()).Render muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
promptStyle := lipgloss.NewStyle(). promptStyle := styles.NewStyle().Foreground(t.Primary()).
Padding(0, 0, 0, 1). Padding(0, 0, 0, 1).
Bold(true). Bold(true)
Foreground(t.Primary())
prompt := promptStyle.Render(">") prompt := promptStyle.Render(">")
textarea := lipgloss.JoinHorizontal( textarea := lipgloss.JoinHorizontal(
@ -117,11 +119,11 @@ func (m *editorComponent) Content() string {
prompt, prompt,
m.textarea.View(), m.textarea.View(),
) )
textarea = styles.BaseStyle(). textarea = styles.NewStyle().
Background(t.BackgroundElement()).
Width(m.width). Width(m.width).
PaddingTop(1). PaddingTop(1).
PaddingBottom(1). PaddingBottom(1).
Background(t.BackgroundElement()).
Render(textarea) Render(textarea)
hint := base(m.getSubmitKeyText()) + muted(" send ") hint := base(m.getSubmitKeyText()) + muted(" send ")
@ -140,10 +142,10 @@ func (m *editorComponent) Content() string {
} }
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint) space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("") spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
info := hint + spacer + model info := hint + spacer + model
info = styles.Padded().Background(t.Background()).Render(info) info = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(info)
content := strings.Join([]string{"", textarea, info}, "\n") content := strings.Join([]string{"", textarea, info}, "\n")
return content return content
@ -156,6 +158,18 @@ func (m *editorComponent) View() string {
return m.Content() return m.Content()
} }
func (m *editorComponent) Focused() bool {
return m.textarea.Focused()
}
func (m *editorComponent) Focus() (tea.Model, tea.Cmd) {
return m, m.textarea.Focus()
}
func (m *editorComponent) Blur() {
m.textarea.Blur()
}
func (m *editorComponent) GetSize() (width, height int) { func (m *editorComponent) GetSize() (width, height int) {
return m.width, m.height return m.width, m.height
} }
@ -297,14 +311,14 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta := textarea.New() ta := textarea.New()
ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor) ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor) ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor) ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor) ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor) ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor) ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor) ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor) ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Cursor.Color = t.Primary() ta.Styles.Cursor.Color = t.Primary()
ta.Prompt = " " ta.Prompt = " "
@ -317,18 +331,21 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta.SetHeight(existing.Height()) ta.SetHeight(existing.Height())
} }
ta.Focus() // ta.Focus()
return ta return ta
} }
func createSpinner() spinner.Model { func createSpinner() spinner.Model {
t := theme.CurrentTheme()
return spinner.New( return spinner.New(
spinner.WithSpinner(spinner.Ellipsis), spinner.WithSpinner(spinner.Ellipsis),
spinner.WithStyle( spinner.WithStyle(
styles. styles.NewStyle().
Muted(). Foreground(t.Background()).
Background(theme.CurrentTheme().Background()). Foreground(t.TextMuted()).
Width(3)), Width(3).
Lipgloss(),
),
) )
} }

View file

@ -129,15 +129,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
option(renderer) option(renderer)
} }
style := styles.BaseStyle(). style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
// MarginTop(renderer.marginTop). // MarginTop(renderer.marginTop).
// MarginBottom(renderer.marginBottom). // MarginBottom(renderer.marginBottom).
PaddingTop(renderer.paddingTop). PaddingTop(renderer.paddingTop).
PaddingBottom(renderer.paddingBottom). PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft). PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight). PaddingRight(renderer.paddingRight).
Background(t.BackgroundPanel()).
Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder()) BorderStyle(lipgloss.ThickBorder())
align := lipgloss.Left align := lipgloss.Left
@ -179,13 +177,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
layout.Current.Container.Width, layout.Current.Container.Width,
align, align,
content, content,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
content = lipgloss.PlaceHorizontal( content = lipgloss.PlaceHorizontal(
layout.Current.Viewport.Width, layout.Current.Viewport.Width,
lipgloss.Center, lipgloss.Center,
content, content,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
if renderer.marginTop > 0 { if renderer.marginTop > 0 {
for range renderer.marginTop { for range renderer.marginTop {
@ -226,7 +224,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
textWidth := max(lipgloss.Width(text), lipgloss.Width(info)) textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
if message.Role == client.Assistant { if message.Role == client.Assistant {
markdownWidth = width - padding - 4 - 2 markdownWidth = width - padding - 4 - 3
} }
if message.Role == client.User { if message.Role == client.User {
text = strings.ReplaceAll(text, "<", "\\<") text = strings.ReplaceAll(text, "<", "\\<")
@ -275,9 +273,10 @@ func renderToolInvocation(
} }
t := theme.CurrentTheme() t := theme.CurrentTheme()
style := styles.Muted(). style := styles.NewStyle().
Width(outerWidth). Foreground(t.TextMuted()).
Background(t.BackgroundPanel()). Background(t.BackgroundPanel()).
Width(outerWidth).
PaddingTop(paddingTop). PaddingTop(paddingTop).
PaddingBottom(paddingBottom). PaddingBottom(paddingBottom).
PaddingLeft(2). PaddingLeft(2).
@ -293,7 +292,9 @@ func renderToolInvocation(
if !showDetails { if !showDetails {
title = "∟ " + title title = "∟ " + title
padding := calculatePadding() padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel()) style := styles.NewStyle().
Background(t.BackgroundPanel()).
Width(outerWidth - padding - 4 - 3)
return renderContentBlock(style.Render(title), return renderContentBlock(style.Render(title),
WithAlign(lipgloss.Left), WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()), WithBorderColor(t.Accent()),
@ -334,9 +335,9 @@ func renderToolInvocation(
if e, ok := metadata.Get("error"); ok && e.(bool) == true { if e, ok := metadata.Get("error"); ok && e.(bool) == true {
if m, ok := metadata.Get("message"); ok { if m, ok := metadata.Get("message"); ok {
style = style.BorderLeftForeground(t.Error()) style = style.BorderLeftForeground(t.Error())
error = styles.BaseStyle(). error = styles.NewStyle().
Background(t.BackgroundPanel()).
Foreground(t.Error()). Foreground(t.Error()).
Background(t.BackgroundPanel()).
Render(m.(string)) Render(m.(string))
error = renderContentBlock( error = renderContentBlock(
error, error,
@ -374,7 +375,7 @@ func renderToolInvocation(
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth)) formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
} }
formattedDiff = strings.TrimSpace(formattedDiff) formattedDiff = strings.TrimSpace(formattedDiff)
formattedDiff = lipgloss.NewStyle(). formattedDiff = styles.NewStyle().
BorderStyle(lipgloss.ThickBorder()). BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()). BorderBackground(t.Background()).
BorderForeground(t.BackgroundPanel()). BorderForeground(t.BackgroundPanel()).
@ -394,7 +395,7 @@ func renderToolInvocation(
lipgloss.Center, lipgloss.Center,
lipgloss.Top, lipgloss.Top,
body, body,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
} }
} }
@ -506,7 +507,7 @@ func renderToolInvocation(
if !showDetails { if !showDetails {
title = "∟ " + title title = "∟ " + title
padding := calculatePadding() padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel()) style := styles.NewStyle().Background(t.BackgroundPanel()).Width(outerWidth - padding - 4 - 3)
paddingBottom := 0 paddingBottom := 0
if isLast { if isLast {
paddingBottom = 1 paddingBottom = 1
@ -530,7 +531,7 @@ func renderToolInvocation(
layout.Current.Viewport.Width, layout.Current.Viewport.Width,
lipgloss.Center, lipgloss.Center,
content, content,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
if showDetails && body != "" && error == "" { if showDetails && body != "" && error == "" {
content += "\n" + body content += "\n" + body

View file

@ -245,7 +245,7 @@ func (m *messagesComponent) renderView() {
m.width, m.width,
lipgloss.Center, lipgloss.Center,
block, block,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
)) ))
} }
@ -260,8 +260,8 @@ func (m *messagesComponent) header() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
width := layout.Current.Container.Width width := layout.Current.Container.Width
base := styles.BaseStyle().Background(t.Background()).Render base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.Muted().Background(t.Background()).Render muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
headerLines := []string{} headerLines := []string{}
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background())) headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" { if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
@ -271,11 +271,11 @@ func (m *messagesComponent) header() string {
} }
header := strings.Join(headerLines, "\n") header := strings.Join(headerLines, "\n")
header = styles.BaseStyle(). header = styles.NewStyle().
Background(t.Background()).
Width(width). Width(width).
PaddingLeft(2). PaddingLeft(2).
PaddingRight(2). PaddingRight(2).
Background(t.Background()).
BorderLeft(true). BorderLeft(true).
BorderRight(true). BorderRight(true).
BorderBackground(t.Background()). BorderBackground(t.Background()).
@ -306,7 +306,7 @@ func (m *messagesComponent) View() string {
m.width, m.width,
lipgloss.Center, lipgloss.Center,
m.header(), m.header(),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
), ),
m.viewport.View(), m.viewport.View(),
) )
@ -314,9 +314,9 @@ func (m *messagesComponent) View() string {
func (m *messagesComponent) home() string { func (m *messagesComponent) home() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle().Background(t.Background()) baseStyle := styles.NewStyle().Background(t.Background())
base := baseStyle.Render base := baseStyle.Render
muted := styles.Muted().Background(t.Background()).Render muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
open := ` open := `
@ -335,9 +335,9 @@ func (m *messagesComponent) home() string {
// cwd := app.Info.Path.Cwd // cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config // config := app.Info.Path.Config
versionStyle := lipgloss.NewStyle(). versionStyle := styles.NewStyle().
Background(t.Background()).
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.Background()).
Width(lipgloss.Width(logo)). Width(lipgloss.Width(logo)).
Align(lipgloss.Right) Align(lipgloss.Right)
version := versionStyle.Render(m.app.Version) version := versionStyle.Render(m.app.Version)
@ -347,14 +347,14 @@ func (m *messagesComponent) home() string {
m.width, m.width,
lipgloss.Center, lipgloss.Center,
logoAndVersion, logoAndVersion,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
m.commands.SetBackgroundColor(t.Background()) m.commands.SetBackgroundColor(t.Background())
commands := lipgloss.PlaceHorizontal( commands := lipgloss.PlaceHorizontal(
m.width, m.width,
lipgloss.Center, lipgloss.Center,
m.commands.View(), m.commands.View(),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
lines := []string{} lines := []string{}
@ -372,7 +372,7 @@ func (m *messagesComponent) home() string {
lipgloss.Center, lipgloss.Center,
lipgloss.Center, lipgloss.Center,
baseStyle.Render(strings.Join(lines, "\n")), baseStyle.Render(strings.Join(lines, "\n")),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), styles.WhitespaceStyle(t.Background()),
) )
} }

View file

@ -60,15 +60,9 @@ func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *commandsComponent) View() string { func (c *commandsComponent) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
triggerStyle := lipgloss.NewStyle(). triggerStyle := styles.NewStyle().Foreground(t.Primary()).Bold(true)
Foreground(t.Primary()). descriptionStyle := styles.NewStyle().Foreground(t.Text())
Bold(true) keybindStyle := styles.NewStyle().Foreground(t.TextMuted())
descriptionStyle := lipgloss.NewStyle().
Foreground(t.Text())
keybindStyle := lipgloss.NewStyle().
Foreground(t.TextMuted())
if c.background != nil { if c.background != nil {
triggerStyle = triggerStyle.Background(*c.background) triggerStyle = triggerStyle.Background(*c.background)
@ -99,10 +93,11 @@ func (c *commandsComponent) View() string {
} }
if len(commandsToShow) == 0 { if len(commandsToShow) == 0 {
muted := styles.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
if c.showAll { if c.showAll {
return styles.Muted().Render("No commands available") return muted.Render("No commands available")
} }
return styles.Muted().Render("No commands with triggers available") return muted.Render("No commands with triggers available")
} }
// Calculate column widths // Calculate column widths
@ -188,7 +183,7 @@ func (c *commandsComponent) View() string {
// Remove trailing newline // Remove trailing newline
result := strings.TrimSuffix(output.String(), "\n") result := strings.TrimSuffix(output.String(), "\n")
if c.background != nil { if c.background != nil {
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result) result = styles.NewStyle().Background(*c.background).Width(maxWidth).Render(result)
} }
return result return result

View file

@ -26,7 +26,7 @@ type CompletionItemI interface {
func (ci *CompletionItem) Render(selected bool, width int) string { func (ci *CompletionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle().Foreground(t.Text())
itemStyle := baseStyle. itemStyle := baseStyle.
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
@ -185,7 +185,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *completionDialogComponent) View() string { func (c *completionDialogComponent) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle().Foreground(t.Text())
maxWidth := 40 maxWidth := 40
completions := c.list.GetItems() completions := c.list.GetItems()

View file

@ -94,7 +94,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements tea.Model. // View implements tea.Model.
func (m InitDialogCmp) View() string { func (m InitDialogCmp) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle().Foreground(t.Text())
// Calculate width needed for content // Calculate width needed for content
maxWidth := 60 // Width for explanation text maxWidth := 60 // Width for explanation text

View file

@ -158,7 +158,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
} }
t := theme.CurrentTheme() t := theme.CurrentTheme()
return styles.BaseStyle(). return styles.NewStyle().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Width(maxWidth). Width(maxWidth).
Align(lipgloss.Right). Align(lipgloss.Right).

View file

@ -145,7 +145,7 @@ func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
func (p *permissionDialogComponent) renderButtons() string { func (p *permissionDialogComponent) renderButtons() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle().Foreground(t.Text())
allowStyle := baseStyle allowStyle := baseStyle
allowSessionStyle := baseStyle allowSessionStyle := baseStyle
@ -355,8 +355,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
func (p *permissionDialogComponent) styleViewport() string { func (p *permissionDialogComponent) styleViewport() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
contentStyle := lipgloss.NewStyle(). contentStyle := styles.NewStyle().Background(t.Background())
Background(t.Background())
return contentStyle.Render(p.contentViewPort.View()) return contentStyle.Render(p.contentViewPort.View())
} }

View file

@ -7,7 +7,6 @@ import (
"slices" "slices"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
@ -33,7 +32,7 @@ type sessionItem struct {
func (s sessionItem) Render(selected bool, width int) string { func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle()
var text string var text string
if s.isDeleteConfirming { if s.isDeleteConfirming {
@ -44,7 +43,7 @@ func (s sessionItem) Render(selected bool, width int) string {
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...") truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
var itemStyle lipgloss.Style var itemStyle styles.Style
if selected { if selected {
if s.isDeleteConfirming { if s.isDeleteConfirming {
// Red background for delete confirmation // Red background for delete confirmation
@ -151,9 +150,9 @@ func (s *sessionDialog) Render(background string) string {
listView := s.list.View() listView := s.list.View()
t := theme.CurrentTheme() t := theme.CurrentTheme()
helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1) helpStyle := styles.NewStyle().PaddingLeft(1).PaddingTop(1)
helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del") helpText := styles.NewStyle().Foreground(t.Text()).Render("x/del")
helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session") helpText = helpText + styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
helpText = helpStyle.Render(helpText) helpText = helpStyle.Render(helpText)
content := strings.Join([]string{listView, helpText}, "\n") content := strings.Join([]string{listView, helpText}, "\n")

View file

@ -103,7 +103,7 @@ func NewThemeDialog() ThemeDialog {
// Set the initial selection to the current theme // Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx) list.SetSelectedIndex(selectedIdx)
// Set the max width for the list to match the modal width // Set the max width for the list to match the modal width
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding) list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)

View file

@ -441,84 +441,84 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
<entry type="TextWhitespace" style="%s"/> <entry type="TextWhitespace" style="%s"/>
</style> </style>
`, `,
getColor(t.BackgroundPanel()), // Background getChromaColor(t.BackgroundPanel()), // Background
getColor(t.Text()), // Text getChromaColor(t.Text()), // Text
getColor(t.Text()), // Other getChromaColor(t.Text()), // Other
getColor(t.Error()), // Error getChromaColor(t.Error()), // Error
getColor(t.SyntaxKeyword()), // Keyword getChromaColor(t.SyntaxKeyword()), // Keyword
getColor(t.SyntaxKeyword()), // KeywordConstant getChromaColor(t.SyntaxKeyword()), // KeywordConstant
getColor(t.SyntaxKeyword()), // KeywordDeclaration getChromaColor(t.SyntaxKeyword()), // KeywordDeclaration
getColor(t.SyntaxKeyword()), // KeywordNamespace getChromaColor(t.SyntaxKeyword()), // KeywordNamespace
getColor(t.SyntaxKeyword()), // KeywordPseudo getChromaColor(t.SyntaxKeyword()), // KeywordPseudo
getColor(t.SyntaxKeyword()), // KeywordReserved getChromaColor(t.SyntaxKeyword()), // KeywordReserved
getColor(t.SyntaxType()), // KeywordType getChromaColor(t.SyntaxType()), // KeywordType
getColor(t.Text()), // Name getChromaColor(t.Text()), // Name
getColor(t.SyntaxVariable()), // NameAttribute getChromaColor(t.SyntaxVariable()), // NameAttribute
getColor(t.SyntaxType()), // NameBuiltin getChromaColor(t.SyntaxType()), // NameBuiltin
getColor(t.SyntaxVariable()), // NameBuiltinPseudo getChromaColor(t.SyntaxVariable()), // NameBuiltinPseudo
getColor(t.SyntaxType()), // NameClass getChromaColor(t.SyntaxType()), // NameClass
getColor(t.SyntaxVariable()), // NameConstant getChromaColor(t.SyntaxVariable()), // NameConstant
getColor(t.SyntaxFunction()), // NameDecorator getChromaColor(t.SyntaxFunction()), // NameDecorator
getColor(t.SyntaxVariable()), // NameEntity getChromaColor(t.SyntaxVariable()), // NameEntity
getColor(t.SyntaxType()), // NameException getChromaColor(t.SyntaxType()), // NameException
getColor(t.SyntaxFunction()), // NameFunction getChromaColor(t.SyntaxFunction()), // NameFunction
getColor(t.Text()), // NameLabel getChromaColor(t.Text()), // NameLabel
getColor(t.SyntaxType()), // NameNamespace getChromaColor(t.SyntaxType()), // NameNamespace
getColor(t.SyntaxVariable()), // NameOther getChromaColor(t.SyntaxVariable()), // NameOther
getColor(t.SyntaxKeyword()), // NameTag getChromaColor(t.SyntaxKeyword()), // NameTag
getColor(t.SyntaxVariable()), // NameVariable getChromaColor(t.SyntaxVariable()), // NameVariable
getColor(t.SyntaxVariable()), // NameVariableClass getChromaColor(t.SyntaxVariable()), // NameVariableClass
getColor(t.SyntaxVariable()), // NameVariableGlobal getChromaColor(t.SyntaxVariable()), // NameVariableGlobal
getColor(t.SyntaxVariable()), // NameVariableInstance getChromaColor(t.SyntaxVariable()), // NameVariableInstance
getColor(t.SyntaxString()), // Literal getChromaColor(t.SyntaxString()), // Literal
getColor(t.SyntaxString()), // LiteralDate getChromaColor(t.SyntaxString()), // LiteralDate
getColor(t.SyntaxString()), // LiteralString getChromaColor(t.SyntaxString()), // LiteralString
getColor(t.SyntaxString()), // LiteralStringBacktick getChromaColor(t.SyntaxString()), // LiteralStringBacktick
getColor(t.SyntaxString()), // LiteralStringChar getChromaColor(t.SyntaxString()), // LiteralStringChar
getColor(t.SyntaxString()), // LiteralStringDoc getChromaColor(t.SyntaxString()), // LiteralStringDoc
getColor(t.SyntaxString()), // LiteralStringDouble getChromaColor(t.SyntaxString()), // LiteralStringDouble
getColor(t.SyntaxString()), // LiteralStringEscape getChromaColor(t.SyntaxString()), // LiteralStringEscape
getColor(t.SyntaxString()), // LiteralStringHeredoc getChromaColor(t.SyntaxString()), // LiteralStringHeredoc
getColor(t.SyntaxString()), // LiteralStringInterpol getChromaColor(t.SyntaxString()), // LiteralStringInterpol
getColor(t.SyntaxString()), // LiteralStringOther getChromaColor(t.SyntaxString()), // LiteralStringOther
getColor(t.SyntaxString()), // LiteralStringRegex getChromaColor(t.SyntaxString()), // LiteralStringRegex
getColor(t.SyntaxString()), // LiteralStringSingle getChromaColor(t.SyntaxString()), // LiteralStringSingle
getColor(t.SyntaxString()), // LiteralStringSymbol getChromaColor(t.SyntaxString()), // LiteralStringSymbol
getColor(t.SyntaxNumber()), // LiteralNumber getChromaColor(t.SyntaxNumber()), // LiteralNumber
getColor(t.SyntaxNumber()), // LiteralNumberBin getChromaColor(t.SyntaxNumber()), // LiteralNumberBin
getColor(t.SyntaxNumber()), // LiteralNumberFloat getChromaColor(t.SyntaxNumber()), // LiteralNumberFloat
getColor(t.SyntaxNumber()), // LiteralNumberHex getChromaColor(t.SyntaxNumber()), // LiteralNumberHex
getColor(t.SyntaxNumber()), // LiteralNumberInteger getChromaColor(t.SyntaxNumber()), // LiteralNumberInteger
getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong getChromaColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
getColor(t.SyntaxNumber()), // LiteralNumberOct getChromaColor(t.SyntaxNumber()), // LiteralNumberOct
getColor(t.SyntaxOperator()), // Operator getChromaColor(t.SyntaxOperator()), // Operator
getColor(t.SyntaxKeyword()), // OperatorWord getChromaColor(t.SyntaxKeyword()), // OperatorWord
getColor(t.SyntaxPunctuation()), // Punctuation getChromaColor(t.SyntaxPunctuation()), // Punctuation
getColor(t.SyntaxComment()), // Comment getChromaColor(t.SyntaxComment()), // Comment
getColor(t.SyntaxComment()), // CommentHashbang getChromaColor(t.SyntaxComment()), // CommentHashbang
getColor(t.SyntaxComment()), // CommentMultiline getChromaColor(t.SyntaxComment()), // CommentMultiline
getColor(t.SyntaxComment()), // CommentSingle getChromaColor(t.SyntaxComment()), // CommentSingle
getColor(t.SyntaxComment()), // CommentSpecial getChromaColor(t.SyntaxComment()), // CommentSpecial
getColor(t.SyntaxKeyword()), // CommentPreproc getChromaColor(t.SyntaxKeyword()), // CommentPreproc
getColor(t.Text()), // Generic getChromaColor(t.Text()), // Generic
getColor(t.Error()), // GenericDeleted getChromaColor(t.Error()), // GenericDeleted
getColor(t.Text()), // GenericEmph getChromaColor(t.Text()), // GenericEmph
getColor(t.Error()), // GenericError getChromaColor(t.Error()), // GenericError
getColor(t.Text()), // GenericHeading getChromaColor(t.Text()), // GenericHeading
getColor(t.Success()), // GenericInserted getChromaColor(t.Success()), // GenericInserted
getColor(t.TextMuted()), // GenericOutput getChromaColor(t.TextMuted()), // GenericOutput
getColor(t.Text()), // GenericPrompt getChromaColor(t.Text()), // GenericPrompt
getColor(t.Text()), // GenericStrong getChromaColor(t.Text()), // GenericStrong
getColor(t.Text()), // GenericSubheading getChromaColor(t.Text()), // GenericSubheading
getColor(t.Error()), // GenericTraceback getChromaColor(t.Error()), // GenericTraceback
getColor(t.Text()), // TextWhitespace getChromaColor(t.Text()), // TextWhitespace
) )
r := strings.NewReader(syntaxThemeXml) r := strings.NewReader(syntaxThemeXml)
@ -527,6 +527,9 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
// Modify the style to use the provided background // Modify the style to use the provided background
s, err := style.Builder().Transform( s, err := style.Builder().Transform(
func(t chroma.StyleEntry) chroma.StyleEntry { func(t chroma.StyleEntry) chroma.StyleEntry {
if _, ok := bg.(lipgloss.NoColor); ok {
return t
}
r, g, b, _ := bg.RGBA() r, g, b, _ := bg.RGBA()
t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8)) t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
return t return t
@ -546,10 +549,18 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
} }
// getColor returns the appropriate hex color string based on terminal background // getColor returns the appropriate hex color string based on terminal background
func getColor(adaptiveColor compat.AdaptiveColor) string { func getColor(adaptiveColor compat.AdaptiveColor) *string {
return stylesi.AdaptiveColorToString(adaptiveColor) return stylesi.AdaptiveColorToString(adaptiveColor)
} }
func getChromaColor(adaptiveColor compat.AdaptiveColor) string {
color := stylesi.AdaptiveColorToString(adaptiveColor)
if color == nil {
return ""
}
return *color
}
// highlightLine applies syntax highlighting to a single line // highlightLine applies syntax highlighting to a single line
func highlightLine(fileName string, line string, bg color.Color) string { func highlightLine(fileName string, line string, bg color.Color) string {
var buf bytes.Buffer var buf bytes.Buffer
@ -561,11 +572,11 @@ func highlightLine(fileName string, line string, bg color.Color) string {
} }
// createStyles generates the lipgloss styles needed for rendering diffs // createStyles generates the lipgloss styles needed for rendering diffs
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) { func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle stylesi.Style) {
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg()) removedLineStyle = stylesi.NewStyle().Background(t.DiffRemovedBg())
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg()) addedLineStyle = stylesi.NewStyle().Background(t.DiffAddedBg())
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg()) contextLineStyle = stylesi.NewStyle().Background(t.DiffContextBg())
lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted()) lineNumberStyle = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffLineNumber())
return return
} }
@ -613,9 +624,17 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
currentPos := 0 currentPos := 0
// Get the appropriate color based on terminal background // Get the appropriate color based on terminal background
bgColor := lipgloss.Color(getColor(highlightBg)) bg := getColor(highlightBg)
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel())) fg := getColor(theme.CurrentTheme().BackgroundPanel())
var bgColor color.Color
var fgColor color.Color
if bg != nil {
bgColor = lipgloss.Color(*bg)
}
if fg != nil {
fgColor = lipgloss.Color(*fg)
}
for i := 0; i < len(content); { for i := 0; i < len(content); {
// Check if we're at an ANSI sequence // Check if we're at an ANSI sequence
isAnsi := false isAnsi := false
@ -651,12 +670,20 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
currentStyle := ansiSequences[currentPos] currentStyle := ansiSequences[currentPos]
// Apply foreground and background highlight // Apply foreground and background highlight
sb.WriteString("\x1b[38;2;") if fgColor != nil {
r, g, b, _ := fgColor.RGBA() sb.WriteString("\x1b[38;2;")
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8)) r, g, b, _ := fgColor.RGBA()
sb.WriteString("\x1b[48;2;") sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
r, g, b, _ = bgColor.RGBA() } else {
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8)) sb.WriteString("\x1b[49m")
}
if bgColor != nil {
sb.WriteString("\x1b[48;2;")
r, g, b, _ := bgColor.RGBA()
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
} else {
sb.WriteString("\x1b[39m")
}
sb.WriteString(char) sb.WriteString(char)
// Full reset of all attributes to ensure clean state // Full reset of all attributes to ensure clean state
@ -677,16 +704,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
} }
// renderLinePrefix renders the line number and marker prefix for a diff line // renderLinePrefix renders the line number and marker prefix for a diff line
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string { func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle stylesi.Style, t theme.Theme) string {
// Style the marker based on line type // Style the marker based on line type
var styledMarker string var styledMarker string
switch dl.Kind { switch dl.Kind {
case LineRemoved: case LineRemoved:
styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker) styledMarker = stylesi.NewStyle().Foreground(t.DiffRemoved()).Background(t.DiffRemovedBg()).Render(marker)
case LineAdded: case LineAdded:
styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker) styledMarker = stylesi.NewStyle().Foreground(t.DiffAdded()).Background(t.DiffAddedBg()).Render(marker)
case LineContext: case LineContext:
styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker) styledMarker = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffContextBg()).Render(marker)
default: default:
styledMarker = marker styledMarker = marker
} }
@ -695,7 +722,7 @@ func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyl
} }
// renderLineContent renders the content of a diff line with syntax and intra-line highlighting // renderLineContent renders the content of a diff line with syntax and intra-line highlighting
func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string { func renderLineContent(fileName string, dl DiffLine, bgStyle stylesi.Style, highlightColor compat.AdaptiveColor, width int) string {
// Apply syntax highlighting // Apply syntax highlighting
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground()) content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
@ -714,7 +741,9 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, hig
ansi.Truncate( ansi.Truncate(
content, content,
width, width,
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."), "...",
// stylesi.NewStyleWithColors(t.TextMuted(), bgStyle.GetBackground()).Render("..."),
// stylesi.WithForeground(stylesi.NewStyle().Background(bgStyle.GetBackground()), t.TextMuted()).Render("..."),
), ),
) )
} }
@ -725,7 +754,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
// Determine line style and marker based on line type // Determine line style and marker based on line type
var marker string var marker string
var bgStyle lipgloss.Style var bgStyle stylesi.Style
var lineNum string var lineNum string
var highlightColor compat.AdaptiveColor var highlightColor compat.AdaptiveColor
@ -733,8 +762,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
case LineRemoved: case LineRemoved:
marker = "-" marker = "-"
bgStyle = removedLineStyle bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg()) lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
highlightColor = t.DiffHighlightRemoved() highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
if dl.OldLineNo > 0 { if dl.OldLineNo > 0 {
lineNum = fmt.Sprintf("%6d ", dl.OldLineNo) lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
} else { } else {
@ -743,8 +772,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
case LineAdded: case LineAdded:
marker = "+" marker = "+"
bgStyle = addedLineStyle bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg()) lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
highlightColor = t.DiffHighlightAdded() highlightColor = t.DiffHighlightAdded() // TODO: handle "none"
if dl.NewLineNo > 0 { if dl.NewLineNo > 0 {
lineNum = fmt.Sprintf(" %7d", dl.NewLineNo) lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
} else { } else {
@ -766,7 +795,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
// Render the content // Render the content
prefixWidth := ansi.StringWidth(prefix) prefixWidth := ansi.StringWidth(prefix)
contentWidth := width - prefixWidth contentWidth := width - prefixWidth
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t) content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth)
return prefix + content return prefix + content
} }
@ -780,7 +809,7 @@ func renderDiffColumnLine(
t theme.Theme, t theme.Theme,
) string { ) string {
if dl == nil { if dl == nil {
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg()) contextLineStyle := stylesi.NewStyle().Background(t.DiffContextBg())
return contextLineStyle.Width(colWidth).Render("") return contextLineStyle.Width(colWidth).Render("")
} }
@ -788,7 +817,7 @@ func renderDiffColumnLine(
// Determine line style based on line type and column // Determine line style based on line type and column
var marker string var marker string
var bgStyle lipgloss.Style var bgStyle stylesi.Style
var lineNum string var lineNum string
var highlightColor compat.AdaptiveColor var highlightColor compat.AdaptiveColor
@ -798,8 +827,8 @@ func renderDiffColumnLine(
case LineRemoved: case LineRemoved:
marker = "-" marker = "-"
bgStyle = removedLineStyle bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg()) lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
highlightColor = t.DiffHighlightRemoved() highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
case LineAdded: case LineAdded:
marker = "?" marker = "?"
bgStyle = contextLineStyle bgStyle = contextLineStyle
@ -818,7 +847,7 @@ func renderDiffColumnLine(
case LineAdded: case LineAdded:
marker = "+" marker = "+"
bgStyle = addedLineStyle bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg()) lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
highlightColor = t.DiffHighlightAdded() highlightColor = t.DiffHighlightAdded()
case LineRemoved: case LineRemoved:
marker = "?" marker = "?"
@ -849,7 +878,7 @@ func renderDiffColumnLine(
// Render the content // Render the content
prefixWidth := ansi.StringWidth(prefix) prefixWidth := ansi.StringWidth(prefix)
contentWidth := colWidth - prefixWidth contentWidth := colWidth - prefixWidth
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t) content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth)
return prefix + content return prefix + content
} }

View file

@ -5,7 +5,6 @@ import (
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
@ -174,11 +173,11 @@ type StringItem string
func (s StringItem) Render(selected bool, width int) string { func (s StringItem) Render(selected bool, width int) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() baseStyle := styles.NewStyle()
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...") truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
var itemStyle lipgloss.Style var itemStyle styles.Style
if selected { if selected {
itemStyle = baseStyle. itemStyle = baseStyle.
Background(t.Primary()). Background(t.Primary()).
@ -187,6 +186,7 @@ func (s StringItem) Render(selected bool, width int) string {
PaddingLeft(1) PaddingLeft(1)
} else { } else {
itemStyle = baseStyle. itemStyle = baseStyle.
Foreground(t.TextMuted()).
PaddingLeft(1) PaddingLeft(1)
} }

View file

@ -90,12 +90,8 @@ func (m *Modal) Render(contentView string, background string) string {
innerWidth := outerWidth - 4 innerWidth := outerWidth - 4
// Base style for the modal baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
baseStyle := styles.BaseStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted())
// Add title if provided
var finalContent string var finalContent string
if m.title != "" { if m.title != "" {
titleStyle := baseStyle. titleStyle := baseStyle.

View file

@ -3,7 +3,7 @@ package qr
import ( import (
"strings" "strings"
"github.com/charmbracelet/lipgloss/v2" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"rsc.io/qr" "rsc.io/qr"
) )
@ -23,9 +23,7 @@ func Generate(text string) (string, int, error) {
} }
// Create lipgloss style for QR code with theme colors // Create lipgloss style for QR code with theme colors
qrStyle := lipgloss.NewStyle(). qrStyle := styles.NewStyleWithColors(t.Text(), t.Background())
Foreground(t.Text()).
Background(t.Background())
var result strings.Builder var result strings.Builder

View file

@ -36,14 +36,15 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m statusComponent) logo() string { func (m statusComponent) logo() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
open := base("open") open := base("open")
code := emphasis("code ") code := emphasis("code ")
version := base(m.app.Version) version := base(m.app.Version)
return styles.Padded(). return styles.NewStyle().
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
Padding(0, 1).
Render(open + code + version) Render(open + code + version)
} }
@ -77,7 +78,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
func (m statusComponent) View() string { func (m statusComponent) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
if m.app.Session.Id == "" { if m.app.Session.Id == "" {
return styles.BaseStyle(). return styles.NewStyle().
Background(t.Background()). Background(t.Background()).
Width(m.width). Width(m.width).
Height(2). Height(2).
@ -86,9 +87,10 @@ func (m statusComponent) View() string {
logo := m.logo() logo := m.logo()
cwd := styles.Padded(). cwd := styles.NewStyle().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.BackgroundPanel()). Background(t.BackgroundPanel()).
Padding(0, 1).
Render(m.app.Info.Path.Cwd) Render(m.app.Info.Path.Cwd)
sessionInfo := "" sessionInfo := ""
@ -111,9 +113,10 @@ func (m statusComponent) View() string {
} }
} }
sessionInfo = styles.Padded(). sessionInfo = styles.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.BackgroundElement()).
Padding(0, 1).
Render(formatTokensAndCost(tokens, contextWindow, cost)) Render(formatTokensAndCost(tokens, contextWindow, cost))
} }
@ -123,11 +126,11 @@ func (m statusComponent) View() string {
0, 0,
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo), m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
) )
spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("") spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
status := logo + cwd + spacer + sessionInfo status := logo + cwd + spacer + sessionInfo
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("") blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
return blank + "\n" + status return blank + "\n" + status
} }

View file

@ -90,9 +90,9 @@ func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
func (tm *ToastManager) renderSingleToast(toast Toast) string { func (tm *ToastManager) renderSingleToast(toast Toast) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle(). baseStyle := styles.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Text()). Foreground(t.Text()).
Background(t.BackgroundElement()).
Padding(1, 2) Padding(1, 2)
maxWidth := max(40, layout.Current.Viewport.Width/3) maxWidth := max(40, layout.Current.Viewport.Width/3)
@ -101,15 +101,14 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
// Build content with wrapping // Build content with wrapping
var content strings.Builder var content strings.Builder
if toast.Title != nil { if toast.Title != nil {
titleStyle := lipgloss.NewStyle(). titleStyle := styles.NewStyle().Foreground(toast.Color).
Foreground(toast.Color).
Bold(true) Bold(true)
content.WriteString(titleStyle.Render(*toast.Title)) content.WriteString(titleStyle.Render(*toast.Title))
content.WriteString("\n") content.WriteString("\n")
} }
// Wrap message text // Wrap message text
messageStyle := lipgloss.NewStyle() messageStyle := styles.NewStyle()
contentWidth := lipgloss.Width(toast.Message) contentWidth := lipgloss.Width(toast.Message)
if contentWidth > contentMaxWidth { if contentWidth > contentMaxWidth {
messageStyle = messageStyle.Width(contentMaxWidth) messageStyle = messageStyle.Width(contentMaxWidth)

View file

@ -18,7 +18,7 @@ type State struct {
func NewState() *State { func NewState() *State {
return &State{ return &State{
Theme: "opencode", Theme: "system",
} }
} }

View file

@ -3,6 +3,7 @@ package layout
import ( import (
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
@ -57,7 +58,7 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *container) View() string { func (c *container) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
style := lipgloss.NewStyle() style := styles.NewStyle().Background(t.Background())
width := c.width width := c.width
height := c.height height := c.height
@ -66,8 +67,6 @@ func (c *container) View() string {
width = c.maxWidth width = c.maxWidth
} }
style = style.Background(t.Background())
// Apply border if any side is enabled // Apply border if any side is enabled
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft { if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
// Adjust width and height for borders // Adjust width and height for borders

View file

@ -3,6 +3,7 @@ package layout
import ( import (
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
@ -66,7 +67,7 @@ func (f *flexLayout) View() string {
alignment, alignment,
child.View(), child.View(),
// TODO: make configurable WithBackgroundStyle // TODO: make configurable WithBackgroundStyle
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
) )
views = append(views, view) views = append(views, view)
} else { } else {
@ -78,7 +79,7 @@ func (f *flexLayout) View() string {
alignment, alignment,
child.View(), child.View(),
// TODO: make configurable WithBackgroundStyle // TODO: make configurable WithBackgroundStyle
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
) )
views = append(views, view) views = append(views, view)
} }

View file

@ -1,6 +1,9 @@
package styles package styles
import "image/color"
type TerminalInfo struct { type TerminalInfo struct {
Background color.Color
BackgroundIsDark bool BackgroundIsDark bool
} }
@ -8,6 +11,7 @@ var Terminal *TerminalInfo
func init() { func init() {
Terminal = &TerminalInfo{ Terminal = &TerminalInfo{
Background: color.Black,
BackgroundIsDark: true, BackgroundIsDark: true,
} }
} }

View file

@ -3,6 +3,7 @@ package styles
import ( import (
"github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat" "github.com/charmbracelet/lipgloss/v2/compat"
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
@ -29,7 +30,7 @@ func GetMarkdownRenderer(width int, backgroundColor compat.AdaptiveColor) *glamo
// using adaptive colors from the provided theme. // using adaptive colors from the provided theme.
func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig { func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
t := theme.CurrentTheme() t := theme.CurrentTheme()
background := stringPtr(AdaptiveColorToString(backgroundColor)) background := AdaptiveColorToString(backgroundColor)
return ansi.StyleConfig{ return ansi.StyleConfig{
Document: ansi.StyleBlock{ Document: ansi.StyleBlock{
@ -37,12 +38,12 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
BlockPrefix: "", BlockPrefix: "",
BlockSuffix: "", BlockSuffix: "",
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())), Color: AdaptiveColorToString(t.MarkdownText()),
}, },
}, },
BlockQuote: ansi.StyleBlock{ BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())), Color: AdaptiveColorToString(t.MarkdownBlockQuote()),
Italic: boolPtr(true), Italic: boolPtr(true),
Prefix: "┃ ", Prefix: "┃ ",
}, },
@ -54,108 +55,108 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
StyleBlock: ansi.StyleBlock{ StyleBlock: ansi.StyleBlock{
IndentToken: stringPtr(" "), IndentToken: stringPtr(" "),
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())), Color: AdaptiveColorToString(t.MarkdownText()),
}, },
}, },
}, },
Heading: ansi.StyleBlock{ Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n", BlockSuffix: "\n",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H1: ansi.StyleBlock{ H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "# ", Prefix: "# ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H2: ansi.StyleBlock{ H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "## ", Prefix: "## ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H3: ansi.StyleBlock{ H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "### ", Prefix: "### ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H4: ansi.StyleBlock{ H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "#### ", Prefix: "#### ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H5: ansi.StyleBlock{ H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "##### ", Prefix: "##### ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
H6: ansi.StyleBlock{ H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ", Prefix: "###### ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
}, },
Strikethrough: ansi.StylePrimitive{ Strikethrough: ansi.StylePrimitive{
CrossedOut: boolPtr(true), CrossedOut: boolPtr(true),
Color: stringPtr(AdaptiveColorToString(t.TextMuted())), Color: AdaptiveColorToString(t.TextMuted()),
}, },
Emph: ansi.StylePrimitive{ Emph: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())), Color: AdaptiveColorToString(t.MarkdownEmph()),
Italic: boolPtr(true), Italic: boolPtr(true),
}, },
Strong: ansi.StylePrimitive{ Strong: ansi.StylePrimitive{
Bold: boolPtr(true), Bold: boolPtr(true),
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())), Color: AdaptiveColorToString(t.MarkdownStrong()),
}, },
HorizontalRule: ansi.StylePrimitive{ HorizontalRule: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())), Color: AdaptiveColorToString(t.MarkdownHorizontalRule()),
Format: "\n─────────────────────────────────────────\n", Format: "\n─────────────────────────────────────────\n",
}, },
Item: ansi.StylePrimitive{ Item: ansi.StylePrimitive{
BlockPrefix: "• ", BlockPrefix: "• ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownListItem())), Color: AdaptiveColorToString(t.MarkdownListItem()),
}, },
Enumeration: ansi.StylePrimitive{ Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ", BlockPrefix: ". ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())), Color: AdaptiveColorToString(t.MarkdownListEnumeration()),
}, },
Task: ansi.StyleTask{ Task: ansi.StyleTask{
Ticked: "[✓] ", Ticked: "[✓] ",
Unticked: "[ ] ", Unticked: "[ ] ",
}, },
Link: ansi.StylePrimitive{ Link: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownLink())), Color: AdaptiveColorToString(t.MarkdownLink()),
Underline: boolPtr(true), Underline: boolPtr(true),
}, },
LinkText: ansi.StylePrimitive{ LinkText: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())), Color: AdaptiveColorToString(t.MarkdownLinkText()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
Image: ansi.StylePrimitive{ Image: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownImage())), Color: AdaptiveColorToString(t.MarkdownImage()),
Underline: boolPtr(true), Underline: boolPtr(true),
Format: "🖼 {{.text}}", Format: "🖼 {{.text}}",
}, },
ImageText: ansi.StylePrimitive{ ImageText: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownImageText())), Color: AdaptiveColorToString(t.MarkdownImageText()),
Format: "{{.text}}", Format: "{{.text}}",
}, },
Code: ansi.StyleBlock{ Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownCode())), Color: AdaptiveColorToString(t.MarkdownCode()),
Prefix: "", Prefix: "",
Suffix: "", Suffix: "",
}, },
@ -165,7 +166,7 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Prefix: " ", Prefix: " ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())), Color: AdaptiveColorToString(t.MarkdownCodeBlock()),
}, },
}, },
Chroma: &ansi.Chroma{ Chroma: &ansi.Chroma{
@ -174,109 +175,109 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
}, },
Text: ansi.StylePrimitive{ Text: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())), Color: AdaptiveColorToString(t.MarkdownText()),
}, },
Error: ansi.StylePrimitive{ Error: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.Error())), Color: AdaptiveColorToString(t.Error()),
}, },
Comment: ansi.StylePrimitive{ Comment: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxComment())), Color: AdaptiveColorToString(t.SyntaxComment()),
}, },
CommentPreproc: ansi.StylePrimitive{ CommentPreproc: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
Keyword: ansi.StylePrimitive{ Keyword: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
KeywordReserved: ansi.StylePrimitive{ KeywordReserved: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
KeywordNamespace: ansi.StylePrimitive{ KeywordNamespace: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
KeywordType: ansi.StylePrimitive{ KeywordType: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())), Color: AdaptiveColorToString(t.SyntaxType()),
}, },
Operator: ansi.StylePrimitive{ Operator: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxOperator())), Color: AdaptiveColorToString(t.SyntaxOperator()),
}, },
Punctuation: ansi.StylePrimitive{ Punctuation: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())), Color: AdaptiveColorToString(t.SyntaxPunctuation()),
}, },
Name: ansi.StylePrimitive{ Name: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())), Color: AdaptiveColorToString(t.SyntaxVariable()),
}, },
NameBuiltin: ansi.StylePrimitive{ NameBuiltin: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())), Color: AdaptiveColorToString(t.SyntaxVariable()),
}, },
NameTag: ansi.StylePrimitive{ NameTag: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
NameAttribute: ansi.StylePrimitive{ NameAttribute: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())), Color: AdaptiveColorToString(t.SyntaxFunction()),
}, },
NameClass: ansi.StylePrimitive{ NameClass: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())), Color: AdaptiveColorToString(t.SyntaxType()),
}, },
NameConstant: ansi.StylePrimitive{ NameConstant: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())), Color: AdaptiveColorToString(t.SyntaxVariable()),
}, },
NameDecorator: ansi.StylePrimitive{ NameDecorator: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())), Color: AdaptiveColorToString(t.SyntaxFunction()),
}, },
NameFunction: ansi.StylePrimitive{ NameFunction: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())), Color: AdaptiveColorToString(t.SyntaxFunction()),
}, },
LiteralNumber: ansi.StylePrimitive{ LiteralNumber: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxNumber())), Color: AdaptiveColorToString(t.SyntaxNumber()),
}, },
LiteralString: ansi.StylePrimitive{ LiteralString: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxString())), Color: AdaptiveColorToString(t.SyntaxString()),
}, },
LiteralStringEscape: ansi.StylePrimitive{ LiteralStringEscape: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())), Color: AdaptiveColorToString(t.SyntaxKeyword()),
}, },
GenericDeleted: ansi.StylePrimitive{ GenericDeleted: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.DiffRemoved())), Color: AdaptiveColorToString(t.DiffRemoved()),
}, },
GenericEmph: ansi.StylePrimitive{ GenericEmph: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())), Color: AdaptiveColorToString(t.MarkdownEmph()),
Italic: boolPtr(true), Italic: boolPtr(true),
}, },
GenericInserted: ansi.StylePrimitive{ GenericInserted: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.DiffAdded())), Color: AdaptiveColorToString(t.DiffAdded()),
}, },
GenericStrong: ansi.StylePrimitive{ GenericStrong: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())), Color: AdaptiveColorToString(t.MarkdownStrong()),
Bold: boolPtr(true), Bold: boolPtr(true),
}, },
GenericSubheading: ansi.StylePrimitive{ GenericSubheading: ansi.StylePrimitive{
BackgroundColor: background, BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())), Color: AdaptiveColorToString(t.MarkdownHeading()),
}, },
}, },
}, },
@ -293,14 +294,14 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
}, },
DefinitionDescription: ansi.StylePrimitive{ DefinitionDescription: ansi.StylePrimitive{
BlockPrefix: "\n ", BlockPrefix: "\n ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())), Color: AdaptiveColorToString(t.MarkdownLinkText()),
}, },
Text: ansi.StylePrimitive{ Text: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())), Color: AdaptiveColorToString(t.MarkdownText()),
}, },
Paragraph: ansi.StyleBlock{ Paragraph: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{ StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())), Color: AdaptiveColorToString(t.MarkdownText()),
}, },
}, },
} }
@ -308,11 +309,17 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
// AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate // AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate
// hex color string based on the current terminal background // hex color string based on the current terminal background
func AdaptiveColorToString(color compat.AdaptiveColor) string { func AdaptiveColorToString(color compat.AdaptiveColor) *string {
if Terminal.BackgroundIsDark { if Terminal.BackgroundIsDark {
if _, ok := color.Dark.(lipgloss.NoColor); ok {
return nil
}
c1, _ := colorful.MakeColor(color.Dark) c1, _ := colorful.MakeColor(color.Dark)
return c1.Hex() return stringPtr(c1.Hex())
}
if _, ok := color.Light.(lipgloss.NoColor); ok {
return nil
} }
c1, _ := colorful.MakeColor(color.Light) c1, _ := colorful.MakeColor(color.Light)
return c1.Hex() return stringPtr(c1.Hex())
} }

View file

@ -3,155 +3,8 @@ package styles
import ( import (
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat" "github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/theme"
) )
// BaseStyle returns the base style with background and foreground colors func WhitespaceStyle(bg compat.AdaptiveColor) lipgloss.WhitespaceOption {
func BaseStyle() lipgloss.Style { return lipgloss.WithWhitespaceStyle(NewStyle().Background(bg).Lipgloss())
t := theme.CurrentTheme()
return lipgloss.NewStyle().Foreground(t.Text())
}
func Panel() lipgloss.Style {
t := theme.CurrentTheme()
return lipgloss.NewStyle().
Background(t.BackgroundPanel()).
Border(lipgloss.NormalBorder(), true, false, true, false).
BorderForeground(t.BorderSubtle()).
Foreground(t.Text())
}
// Regular returns a basic unstyled lipgloss.Style
func Regular() lipgloss.Style {
return lipgloss.NewStyle()
}
func Muted() lipgloss.Style {
t := theme.CurrentTheme()
return lipgloss.NewStyle().Foreground(t.TextMuted())
}
// Bold returns a bold style
func Bold() lipgloss.Style {
return BaseStyle().Bold(true)
}
// Padded returns a style with horizontal padding
func Padded() lipgloss.Style {
return BaseStyle().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.Border())
}
// ThickBorder returns a style with a thick border
func ThickBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.ThickBorder()).
BorderForeground(t.Border())
}
// DoubleBorder returns a style with a double border
func DoubleBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.DoubleBorder()).
BorderForeground(t.Border())
}
// 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.BorderActive())
}
// 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.BorderSubtle())
}
// PrimaryColor returns the primary color from the current theme
func PrimaryColor() compat.AdaptiveColor {
return theme.CurrentTheme().Primary()
}
// SecondaryColor returns the secondary color from the current theme
func SecondaryColor() compat.AdaptiveColor {
return theme.CurrentTheme().Secondary()
}
// AccentColor returns the accent color from the current theme
func AccentColor() compat.AdaptiveColor {
return theme.CurrentTheme().Accent()
}
// ErrorColor returns the error color from the current theme
func ErrorColor() compat.AdaptiveColor {
return theme.CurrentTheme().Error()
}
// WarningColor returns the warning color from the current theme
func WarningColor() compat.AdaptiveColor {
return theme.CurrentTheme().Warning()
}
// SuccessColor returns the success color from the current theme
func SuccessColor() compat.AdaptiveColor {
return theme.CurrentTheme().Success()
}
// InfoColor returns the info color from the current theme
func InfoColor() compat.AdaptiveColor {
return theme.CurrentTheme().Info()
}
// TextColor returns the text color from the current theme
func TextColor() compat.AdaptiveColor {
return theme.CurrentTheme().Text()
}
// TextMutedColor returns the muted text color from the current theme
func TextMutedColor() compat.AdaptiveColor {
return theme.CurrentTheme().TextMuted()
}
// BackgroundColor returns the background color from the current theme
func BackgroundColor() compat.AdaptiveColor {
return theme.CurrentTheme().Background()
}
// BackgroundPanelColor returns the subtle background color from the current theme
func BackgroundPanelColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundPanel()
}
// BackgroundElementColor returns the darker background color from the current theme
func BackgroundElementColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundElement()
}
// BorderColor returns the border color from the current theme
func BorderColor() compat.AdaptiveColor {
return theme.CurrentTheme().Border()
}
// BorderActiveColor returns the active border color from the current theme
func BorderActiveColor() compat.AdaptiveColor {
return theme.CurrentTheme().BorderActive()
}
// BorderSubtleColor returns the subtle border color from the current theme
func BorderSubtleColor() compat.AdaptiveColor {
return theme.CurrentTheme().BorderSubtle()
} }

View file

@ -0,0 +1,295 @@
package styles
import (
"image/color"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// IsNoColor checks if a color is the special NoColor type
func IsNoColor(c color.Color) bool {
_, ok := c.(lipgloss.NoColor)
return ok
}
// Style wraps lipgloss.Style to provide a fluent API for handling "none" colors
type Style struct {
lipgloss.Style
}
// NewStyle creates a new Style with proper handling of "none" colors
func NewStyle() Style {
return Style{lipgloss.NewStyle()}
}
func (s Style) Lipgloss() lipgloss.Style {
return s.Style
}
// Foreground sets the foreground color, handling "none" appropriately
func (s Style) Foreground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetForeground()}
}
return Style{s.Style.Foreground(c)}
}
// Background sets the background color, handling "none" appropriately
func (s Style) Background(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBackground()}
}
return Style{s.Style.Background(c)}
}
// BorderForeground sets the border foreground color, handling "none" appropriately
func (s Style) BorderForeground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderForeground()}
}
return Style{s.Style.BorderForeground(c)}
}
// BorderBackground sets the border background color, handling "none" appropriately
func (s Style) BorderBackground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderBackground()}
}
return Style{s.Style.BorderBackground(c)}
}
// BorderTopForeground sets the border top foreground color, handling "none" appropriately
func (s Style) BorderTopForeground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderTopForeground()}
}
return Style{s.Style.BorderTopForeground(c)}
}
// BorderTopBackground sets the border top background color, handling "none" appropriately
func (s Style) BorderTopBackground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderTopBackground()}
}
return Style{s.Style.BorderTopBackground(c)}
}
// BorderBottomForeground sets the border bottom foreground color, handling "none" appropriately
func (s Style) BorderBottomForeground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderBottomForeground()}
}
return Style{s.Style.BorderBottomForeground(c)}
}
// BorderBottomBackground sets the border bottom background color, handling "none" appropriately
func (s Style) BorderBottomBackground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderBottomBackground()}
}
return Style{s.Style.BorderBottomBackground(c)}
}
// BorderLeftForeground sets the border left foreground color, handling "none" appropriately
func (s Style) BorderLeftForeground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderLeftForeground()}
}
return Style{s.Style.BorderLeftForeground(c)}
}
// BorderLeftBackground sets the border left background color, handling "none" appropriately
func (s Style) BorderLeftBackground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderLeftBackground()}
}
return Style{s.Style.BorderLeftBackground(c)}
}
// BorderRightForeground sets the border right foreground color, handling "none" appropriately
func (s Style) BorderRightForeground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderRightForeground()}
}
return Style{s.Style.BorderRightForeground(c)}
}
// BorderRightBackground sets the border right background color, handling "none" appropriately
func (s Style) BorderRightBackground(c compat.AdaptiveColor) Style {
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
return Style{s.Style.UnsetBorderRightBackground()}
}
return Style{s.Style.BorderRightBackground(c)}
}
// Render applies the style to a string
func (s Style) Render(str string) string {
return s.Style.Render(str)
}
// Common lipgloss.Style method delegations for seamless usage
func (s Style) Bold(v bool) Style {
return Style{s.Style.Bold(v)}
}
func (s Style) Italic(v bool) Style {
return Style{s.Style.Italic(v)}
}
func (s Style) Underline(v bool) Style {
return Style{s.Style.Underline(v)}
}
func (s Style) Strikethrough(v bool) Style {
return Style{s.Style.Strikethrough(v)}
}
func (s Style) Blink(v bool) Style {
return Style{s.Style.Blink(v)}
}
func (s Style) Faint(v bool) Style {
return Style{s.Style.Faint(v)}
}
func (s Style) Reverse(v bool) Style {
return Style{s.Style.Reverse(v)}
}
func (s Style) Width(i int) Style {
return Style{s.Style.Width(i)}
}
func (s Style) Height(i int) Style {
return Style{s.Style.Height(i)}
}
func (s Style) Padding(i ...int) Style {
return Style{s.Style.Padding(i...)}
}
func (s Style) PaddingTop(i int) Style {
return Style{s.Style.PaddingTop(i)}
}
func (s Style) PaddingBottom(i int) Style {
return Style{s.Style.PaddingBottom(i)}
}
func (s Style) PaddingLeft(i int) Style {
return Style{s.Style.PaddingLeft(i)}
}
func (s Style) PaddingRight(i int) Style {
return Style{s.Style.PaddingRight(i)}
}
func (s Style) Margin(i ...int) Style {
return Style{s.Style.Margin(i...)}
}
func (s Style) MarginTop(i int) Style {
return Style{s.Style.MarginTop(i)}
}
func (s Style) MarginBottom(i int) Style {
return Style{s.Style.MarginBottom(i)}
}
func (s Style) MarginLeft(i int) Style {
return Style{s.Style.MarginLeft(i)}
}
func (s Style) MarginRight(i int) Style {
return Style{s.Style.MarginRight(i)}
}
func (s Style) Border(b lipgloss.Border, sides ...bool) Style {
return Style{s.Style.Border(b, sides...)}
}
func (s Style) BorderStyle(b lipgloss.Border) Style {
return Style{s.Style.BorderStyle(b)}
}
func (s Style) BorderTop(v bool) Style {
return Style{s.Style.BorderTop(v)}
}
func (s Style) BorderBottom(v bool) Style {
return Style{s.Style.BorderBottom(v)}
}
func (s Style) BorderLeft(v bool) Style {
return Style{s.Style.BorderLeft(v)}
}
func (s Style) BorderRight(v bool) Style {
return Style{s.Style.BorderRight(v)}
}
func (s Style) Align(p ...lipgloss.Position) Style {
return Style{s.Style.Align(p...)}
}
func (s Style) AlignHorizontal(p lipgloss.Position) Style {
return Style{s.Style.AlignHorizontal(p)}
}
func (s Style) AlignVertical(p lipgloss.Position) Style {
return Style{s.Style.AlignVertical(p)}
}
func (s Style) Inline(v bool) Style {
return Style{s.Style.Inline(v)}
}
func (s Style) MaxWidth(n int) Style {
return Style{s.Style.MaxWidth(n)}
}
func (s Style) MaxHeight(n int) Style {
return Style{s.Style.MaxHeight(n)}
}
func (s Style) TabWidth(n int) Style {
return Style{s.Style.TabWidth(n)}
}
func (s Style) UnsetBold() Style {
return Style{s.Style.UnsetBold()}
}
func (s Style) UnsetItalic() Style {
return Style{s.Style.UnsetItalic()}
}
func (s Style) UnsetUnderline() Style {
return Style{s.Style.UnsetUnderline()}
}
func (s Style) UnsetStrikethrough() Style {
return Style{s.Style.UnsetStrikethrough()}
}
func (s Style) UnsetBlink() Style {
return Style{s.Style.UnsetBlink()}
}
func (s Style) UnsetFaint() Style {
return Style{s.Style.UnsetFaint()}
}
func (s Style) UnsetReverse() Style {
return Style{s.Style.UnsetReverse()}
}
func (s Style) Copy() Style {
return Style{s.Style}
}
func (s Style) Inherit(i Style) Style {
return Style{s.Style.Inherit(i.Style)}
}

View file

@ -171,7 +171,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
switch v := value.(type) { switch v := value.(type) {
case string: case string:
if strings.HasPrefix(v, "#") { if strings.HasPrefix(v, "#") || v == "none" {
return v, nil return v, nil
} }
return r.resolveReference(v) return r.resolveReference(v)
@ -205,7 +205,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
func (r *colorResolver) resolveColorValue(value any) (any, error) { func (r *colorResolver) resolveColorValue(value any) (any, error) {
switch v := value.(type) { switch v := value.(type) {
case string: case string:
if strings.HasPrefix(v, "#") { if strings.HasPrefix(v, "#") || v == "none" {
return v, nil return v, nil
} }
return r.resolveReference(v) return r.resolveReference(v)
@ -240,6 +240,12 @@ func (r *colorResolver) resolveReference(ref string) (any, error) {
func parseResolvedColor(value any) (compat.AdaptiveColor, error) { func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
switch v := value.(type) { switch v := value.(type) {
case string: case string:
if v == "none" {
return compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}, nil
}
return compat.AdaptiveColor{ return compat.AdaptiveColor{
Dark: lipgloss.Color(v), Dark: lipgloss.Color(v),
Light: lipgloss.Color(v), Light: lipgloss.Color(v),
@ -277,6 +283,9 @@ func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
func parseColorValue(value any) (color.Color, error) { func parseColorValue(value any) (color.Color, error) {
switch v := value.(type) { switch v := value.(type) {
case string: case string:
if v == "none" {
return lipgloss.NoColor{}, nil
}
return lipgloss.Color(v), nil return lipgloss.Color(v), nil
case float64: case float64:
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil

View file

@ -2,19 +2,25 @@ package theme
import ( import (
"fmt" "fmt"
"image/color"
"slices" "slices"
"strconv"
"strings" "strings"
"sync" "sync"
"github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
) )
// Manager handles theme registration, selection, and retrieval. // Manager handles theme registration, selection, and retrieval.
// It maintains a registry of available themes and tracks the currently active theme. // It maintains a registry of available themes and tracks the currently active theme.
type Manager struct { type Manager struct {
themes map[string]Theme themes map[string]Theme
currentName string currentName string
mu sync.RWMutex currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
mu sync.RWMutex
} }
// Global instance of the theme manager // Global instance of the theme manager
@ -34,6 +40,7 @@ func RegisterTheme(name string, theme Theme) {
// If this is the first theme, make it the default // If this is the first theme, make it the default
if globalManager.currentName == "" { if globalManager.currentName == "" {
globalManager.currentName = name globalManager.currentName = name
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
} }
} }
@ -44,11 +51,13 @@ func SetTheme(name string) error {
defer globalManager.mu.Unlock() defer globalManager.mu.Unlock()
delete(styles.Registry, "charm") delete(styles.Registry, "charm")
if _, exists := globalManager.themes[name]; !exists { theme, exists := globalManager.themes[name]
if !exists {
return fmt.Errorf("theme '%s' not found", name) return fmt.Errorf("theme '%s' not found", name)
} }
globalManager.currentName = name globalManager.currentName = name
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
return nil return nil
} }
@ -84,7 +93,11 @@ func AvailableThemes() []string {
names = append(names, name) names = append(names, name)
} }
slices.SortFunc(names, func(a, b string) int { slices.SortFunc(names, func(a, b string) int {
// list system theme first if a == "system" {
return -1
} else if b == "system" {
return 1
}
if a == "opencode" { if a == "opencode" {
return -1 return -1
} else if b == "opencode" { } else if b == "opencode" {
@ -103,3 +116,114 @@ func GetTheme(name string) Theme {
return globalManager.themes[name] return globalManager.themes[name]
} }
// UpdateSystemTheme updates the system theme with terminal background info
func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
dynamicTheme := NewSystemTheme(terminalBg, isDark)
globalManager.themes["system"] = dynamicTheme
if globalManager.currentName == "system" {
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
}
}
// CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
func CurrentThemeUsesAnsiColors() bool {
// globalManager.mu.RLock()
// defer globalManager.mu.RUnlock()
return globalManager.currentUsesAnsiCache
}
// isAnsiColor checks if a color represents an ANSI 0-16 color
func isAnsiColor(c color.Color) bool {
if _, ok := c.(lipgloss.NoColor); ok {
return false
}
if _, ok := c.(ansi.BasicColor); ok {
return true
}
// For other color types, check if they represent ANSI colors
// by examining their string representation
if stringer, ok := c.(fmt.Stringer); ok {
str := stringer.String()
// Check if it's a numeric ANSI color (0-15)
if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
return true
}
}
return false
}
// adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
if isAnsiColor(ac.Dark) {
return true
}
if isAnsiColor(ac.Light) {
return true
}
return false
}
// themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
func themeUsesAnsiColors(theme Theme) bool {
if theme == nil {
return false
}
return adaptiveColorUsesAnsi(theme.Primary()) ||
adaptiveColorUsesAnsi(theme.Secondary()) ||
adaptiveColorUsesAnsi(theme.Accent()) ||
adaptiveColorUsesAnsi(theme.Error()) ||
adaptiveColorUsesAnsi(theme.Warning()) ||
adaptiveColorUsesAnsi(theme.Success()) ||
adaptiveColorUsesAnsi(theme.Info()) ||
adaptiveColorUsesAnsi(theme.Text()) ||
adaptiveColorUsesAnsi(theme.TextMuted()) ||
adaptiveColorUsesAnsi(theme.Background()) ||
adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
adaptiveColorUsesAnsi(theme.Border()) ||
adaptiveColorUsesAnsi(theme.BorderActive()) ||
adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
adaptiveColorUsesAnsi(theme.DiffAdded()) ||
adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
adaptiveColorUsesAnsi(theme.DiffContext()) ||
adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
adaptiveColorUsesAnsi(theme.MarkdownText()) ||
adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
adaptiveColorUsesAnsi(theme.SyntaxString()) ||
adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
adaptiveColorUsesAnsi(theme.SyntaxType()) ||
adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
}

View file

@ -0,0 +1,299 @@
package theme
import (
"fmt"
"image/color"
"math"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// SystemTheme is a dynamic theme that derives its gray scale colors
// from the terminal's background color at runtime
type SystemTheme struct {
BaseTheme
terminalBg color.Color
terminalBgIsDark bool
}
// NewSystemTheme creates a new instance of the dynamic system theme
func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
theme := &SystemTheme{
terminalBg: terminalBg,
terminalBgIsDark: isDark,
}
theme.initializeColors()
return theme
}
// initializeColors sets up all theme colors
func (t *SystemTheme) initializeColors() {
// Generate gray scale based on terminal background
grays := t.generateGrayScale()
// Set ANSI colors for primary colors
t.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Cyan,
Light: lipgloss.Cyan,
}
t.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Magenta,
Light: lipgloss.Magenta,
}
t.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Cyan,
Light: lipgloss.Cyan,
}
// Status colors using ANSI
t.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Red,
Light: lipgloss.Red,
}
t.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Yellow,
Light: lipgloss.Yellow,
}
t.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Green,
Light: lipgloss.Green,
}
t.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Cyan,
Light: lipgloss.Cyan,
}
// Text colors
t.TextColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
// Derive muted text color from terminal foreground
t.TextMutedColor = t.generateMutedTextColor()
// Background colors
t.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
t.BackgroundPanelColor = grays[2]
t.BackgroundElementColor = grays[3]
// Border colors
t.BorderSubtleColor = grays[6]
t.BorderColor = grays[7]
t.BorderActiveColor = grays[8]
// Diff colors using ANSI colors
t.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("2"), // green
Light: lipgloss.Color("2"),
}
t.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("1"), // red
Light: lipgloss.Color("1"),
}
t.DiffContextColor = grays[7] // Use gray for context
t.DiffHunkHeaderColor = grays[7]
t.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("2"), // green
Light: lipgloss.Color("2"),
}
t.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("1"), // red
Light: lipgloss.Color("1"),
}
// Use subtle gray backgrounds for diff
t.DiffAddedBgColor = grays[2]
t.DiffRemovedBgColor = grays[2]
t.DiffContextBgColor = grays[1]
t.DiffLineNumberColor = grays[6]
t.DiffAddedLineNumberBgColor = grays[3]
t.DiffRemovedLineNumberBgColor = grays[3]
// Markdown colors using ANSI
t.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
t.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
t.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color("4"), // blue
Light: lipgloss.Color("4"),
}
t.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("6"), // cyan
Light: lipgloss.Color("6"),
}
t.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color("2"), // green
Light: lipgloss.Color("2"),
}
t.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color("3"), // yellow
Light: lipgloss.Color("3"),
}
t.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color("3"), // yellow
Light: lipgloss.Color("3"),
}
t.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
t.MarkdownHorizontalRuleColor = t.BorderColor
t.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color("4"), // blue
Light: lipgloss.Color("4"),
}
t.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color("6"), // cyan
Light: lipgloss.Color("6"),
}
t.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color("4"), // blue
Light: lipgloss.Color("4"),
}
t.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("6"), // cyan
Light: lipgloss.Color("6"),
}
t.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
// Syntax colors
t.SyntaxCommentColor = t.TextMutedColor // Use same as muted text
t.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color("5"), // magenta
Light: lipgloss.Color("5"),
}
t.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color("4"), // blue
Light: lipgloss.Color("4"),
}
t.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
t.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color("2"), // green
Light: lipgloss.Color("2"),
}
t.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color("3"), // yellow
Light: lipgloss.Color("3"),
}
t.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color("6"), // cyan
Light: lipgloss.Color("6"),
}
t.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color("6"), // cyan
Light: lipgloss.Color("6"),
}
t.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.NoColor{},
Light: lipgloss.NoColor{},
}
}
// generateGrayScale creates a gray scale based on the terminal background
func (t *SystemTheme) generateGrayScale() map[int]compat.AdaptiveColor {
grays := make(map[int]compat.AdaptiveColor)
r, g, b, _ := t.terminalBg.RGBA()
bgR := float64(r >> 8)
bgG := float64(g >> 8)
bgB := float64(b >> 8)
luminance := 0.299*bgR + 0.587*bgG + 0.114*bgB
for i := 1; i <= 12; i++ {
var stepColor string
factor := float64(i) / 12.0
if t.terminalBgIsDark {
if luminance < 10 {
grayValue := int(factor * 0.4 * 255)
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
} else {
newLum := luminance + (255-luminance)*factor*0.4
ratio := newLum / luminance
newR := math.Min(bgR*ratio, 255)
newG := math.Min(bgG*ratio, 255)
newB := math.Min(bgB*ratio, 255)
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
}
} else {
if luminance > 245 {
grayValue := int(255 - factor*0.4*255)
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
} else {
newLum := luminance * (1 - factor*0.4)
ratio := newLum / luminance
newR := math.Max(bgR*ratio, 0)
newG := math.Max(bgG*ratio, 0)
newB := math.Max(bgB*ratio, 0)
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
}
}
grays[i] = compat.AdaptiveColor{
Dark: lipgloss.Color(stepColor),
Light: lipgloss.Color(stepColor),
}
}
return grays
}
// generateMutedTextColor creates a muted gray color based on the terminal background
func (t *SystemTheme) generateMutedTextColor() compat.AdaptiveColor {
bgR, bgG, bgB, _ := t.terminalBg.RGBA()
bgRf := float64(bgR >> 8)
bgGf := float64(bgG >> 8)
bgBf := float64(bgB >> 8)
bgLum := 0.299*bgRf + 0.587*bgGf + 0.114*bgBf
var grayValue int
if t.terminalBgIsDark {
if bgLum < 10 {
// Very dark/black background
// grays[3] would be around #2e (46), so we need much lighter
grayValue = 180 // #b4b4b4
} else {
// Scale up for lighter dark backgrounds
// Ensure we're always significantly brighter than BackgroundElement
grayValue = min(int(160+(bgLum*0.3)), 200)
}
} else {
if bgLum > 245 {
// Very light/white background
// grays[3] would be around #f5 (245), so we need much darker
grayValue = 75 // #4b4b4b
} else {
// Scale down for darker light backgrounds
// Ensure we're always significantly darker than BackgroundElement
grayValue = max(int(100-((255-bgLum)*0.2)), 60)
}
}
mutedColor := fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
return compat.AdaptiveColor{
Dark: lipgloss.Color(mutedColor),
Light: lipgloss.Color(mutedColor),
}
}

View file

@ -22,6 +22,7 @@ import (
"github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client" "github.com/sst/opencode/pkg/client"
) )
@ -230,9 +231,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Batch(cmds...) return a, tea.Batch(cmds...)
case tea.BackgroundColorMsg: case tea.BackgroundColorMsg:
styles.Terminal = &styles.TerminalInfo{ styles.Terminal = &styles.TerminalInfo{
Background: msg.Color,
BackgroundIsDark: msg.IsDark(), BackgroundIsDark: msg.IsDark(),
} }
slog.Debug("Background color", "isDark", msg.IsDark()) slog.Debug("Background color", "color", msg.String(), "isDark", msg.IsDark())
return a, func() tea.Msg {
theme.UpdateSystemTheme(
styles.Terminal.Background,
styles.Terminal.BackgroundIsDark,
)
return dialog.ThemeSelectedMsg{
ThemeName: theme.CurrentThemeName(),
}
}
case modal.CloseModalMsg: case modal.CloseModalMsg:
var cmd tea.Cmd var cmd tea.Cmd
if a.modal != nil { if a.modal != nil {
@ -424,6 +435,9 @@ func (a appModel) View() string {
appView = a.toastManager.RenderOverlay(appView) appView = a.toastManager.RenderOverlay(appView)
if theme.CurrentThemeUsesAnsiColors() {
appView = util.ConvertRGBToAnsi16Colors(appView)
}
return appView return appView
} }

View file

@ -0,0 +1,93 @@
package util
import (
"regexp"
"strings"
)
var csiRE *regexp.Regexp
func init() {
csiRE = regexp.MustCompile(`\x1b\[([0-9;]+)m`)
}
var targetFGMap = map[string]string{
"0;0;0": "\x1b[30m", // Black
"128;0;0": "\x1b[31m", // Red
"0;128;0": "\x1b[32m", // Green
"128;128;0": "\x1b[33m", // Yellow
"0;0;128": "\x1b[34m", // Blue
"128;0;128": "\x1b[35m", // Magenta
"0;128;128": "\x1b[36m", // Cyan
"192;192;192": "\x1b[37m", // White (light grey)
"128;128;128": "\x1b[90m", // Bright Black (dark grey)
"255;0;0": "\x1b[91m", // Bright Red
"0;255;0": "\x1b[92m", // Bright Green
"255;255;0": "\x1b[93m", // Bright Yellow
"0;0;255": "\x1b[94m", // Bright Blue
"255;0;255": "\x1b[95m", // Bright Magenta
"0;255;255": "\x1b[96m", // Bright Cyan
"255;255;255": "\x1b[97m", // Bright White
}
var targetBGMap = map[string]string{
"0;0;0": "\x1b[40m",
"128;0;0": "\x1b[41m",
"0;128;0": "\x1b[42m",
"128;128;0": "\x1b[43m",
"0;0;128": "\x1b[44m",
"128;0;128": "\x1b[45m",
"0;128;128": "\x1b[46m",
"192;192;192": "\x1b[47m",
"128;128;128": "\x1b[100m",
"255;0;0": "\x1b[101m",
"0;255;0": "\x1b[102m",
"255;255;0": "\x1b[103m",
"0;0;255": "\x1b[104m",
"255;0;255": "\x1b[105m",
"0;255;255": "\x1b[106m",
"255;255;255": "\x1b[107m",
}
func ConvertRGBToAnsi16Colors(s string) string {
return csiRE.ReplaceAllStringFunc(s, func(seq string) string {
params := strings.Split(csiRE.FindStringSubmatch(seq)[1], ";")
out := make([]string, 0, len(params))
for i := 0; i < len(params); {
// Detect “38 | 48 ; 2 ; r ; g ; b ( ; alpha? )”
if (params[i] == "38" || params[i] == "48") &&
i+4 < len(params) &&
params[i+1] == "2" {
key := strings.Join(params[i+2:i+5], ";")
var repl string
if params[i] == "38" {
repl = targetFGMap[key]
} else {
repl = targetBGMap[key]
}
if repl != "" { // exact RGB hit
out = append(out, repl[2:len(repl)-1])
i += 5 // skip 38/48;2;r;g;b
// if i == len(params)-1 && looksLikeByte(params[i]) {
// i++ // swallow the alpha byte
// }
continue
}
}
// Normal token — keep verbatim.
out = append(out, params[i])
i++
}
return "\x1b[" + strings.Join(out, ";") + "m"
})
}
// func looksLikeByte(tok string) bool {
// v, err := strconv.Atoi(tok)
// return err == nil && v >= 0 && v <= 255
// }

View file

@ -22,6 +22,11 @@
"minimum": 0, "minimum": 0,
"maximum": 255, "maximum": 255,
"description": "ANSI color code (0-255)" "description": "ANSI color code (0-255)"
},
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
} }
] ]
} }
@ -110,6 +115,11 @@
"maximum": 255, "maximum": 255,
"description": "ANSI color code (0-255, same for dark and light)" "description": "ANSI color code (0-255, same for dark and light)"
}, },
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{ {
"type": "string", "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
@ -131,6 +141,11 @@
"maximum": 255, "maximum": 255,
"description": "ANSI color code for dark mode" "description": "ANSI color code for dark mode"
}, },
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{ {
"type": "string", "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
@ -151,6 +166,11 @@
"maximum": 255, "maximum": 255,
"description": "ANSI color code for light mode" "description": "ANSI color code for light mode"
}, },
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{ {
"type": "string", "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",

View file

@ -2,7 +2,7 @@
title: Themes title: Themes
--- ---
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
## Theme Loading Hierarchy ## Theme Loading Hierarchy
@ -37,6 +37,7 @@ Themes use a flexible JSON format with support for:
- **ANSI colors**: `3` (0-255) - **ANSI colors**: `3` (0-255)
- **Color references**: `"primary"` or custom definitions - **Color references**: `"primary"` or custom definitions
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}` - **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
- **No color**: `"none"` - Uses the terminal's default color (transparent)
### Example Theme ### Example Theme
@ -270,10 +271,30 @@ Themes use a flexible JSON format with support for:
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme. The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
### Using "none" for Terminal Defaults
The special value `\"none\"` can be used for any color to inherit the terminal's default color. This is particularly useful for creating themes that blend seamlessly with your terminal's color scheme:
- `\"text\": \"none\"` - Uses terminal's default foreground color
- `\"background\": \"none\"` - Uses terminal's default background color
## The System Theme
The `system` theme is opencode's default theme, designed to automatically adapt to your terminal's color scheme. Unlike traditional themes that use fixed colors, the system theme:
- **Generates gray scale**: Creates a custom gray scale based on your terminal's background color, ensuring optimal contrast
- **Uses ANSI colors**: Leverages standard ANSI colors (0-15) for syntax highlighting and UI elements, which respect your terminal's color palette
- **Preserves terminal defaults**: Uses `none` for text and background colors to maintain your terminal's native appearance
The system theme is ideal for users who:
- Want opencode to match their terminal's appearance
- Use custom terminal color schemes
- Prefer a consistent look across all terminal applications
## Built-in Themes ## Built-in Themes
opencode comes with several built-in themes: opencode comes with several built-in themes:
- `opencode` - Default opencode theme - `system` - Default theme that dynamically adapts to your terminal's background color
- `tokyonight` - Tokyonight theme - `tokyonight` - Tokyonight theme
- `everforest` - Everforest theme - `everforest` - Everforest theme
- `ayu` - Ayu dark theme - `ayu` - Ayu dark theme
@ -281,7 +302,8 @@ opencode comes with several built-in themes:
- `gruvbox` - Gruvbox theme - `gruvbox` - Gruvbox theme
- `kanagawa` - Kanagawa theme - `kanagawa` - Kanagawa theme
- `nord` - Nord theme - `nord` - Nord theme
- and more (see ./packages/tui/internal/theme/themes) - `matrix` - Hacker-style green on black theme
- `one-dark` - Atom One Dark inspired theme
## Using a Theme ## Using a Theme