summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-06-26 10:16:07 -0500
committerGitHub <[email protected]>2025-06-26 10:16:07 -0500
commit7d13baadc84d7377a352c6d58ed9deeea2c918be (patch)
tree575b6897431e390ae8bf4b9ccde2803446c6c67a
parentdb24bf87c01d3fff2a9594e55e9fab0d9ef52cfe (diff)
downloadopencode-7d13baadc84d7377a352c6d58ed9deeea2c918be.tar.gz
opencode-7d13baadc84d7377a352c6d58ed9deeea2c918be.zip
feat: default system theme (#419)
Co-authored-by: adamdottv <[email protected]>
-rw-r--r--packages/tui/cmd/opencode/main.go1
-rw-r--r--packages/tui/internal/app/app.go7
-rw-r--r--packages/tui/internal/completions/commands.go3
-rw-r--r--packages/tui/internal/components/chat/editor.go65
-rw-r--r--packages/tui/internal/components/chat/message.go31
-rw-r--r--packages/tui/internal/components/chat/messages.go26
-rw-r--r--packages/tui/internal/components/commands/commands.go19
-rw-r--r--packages/tui/internal/components/dialog/complete.go4
-rw-r--r--packages/tui/internal/components/dialog/init.go2
-rw-r--r--packages/tui/internal/components/dialog/models.go2
-rw-r--r--packages/tui/internal/components/dialog/permission.go5
-rw-r--r--packages/tui/internal/components/dialog/session.go11
-rw-r--r--packages/tui/internal/components/dialog/theme.go2
-rw-r--r--packages/tui/internal/components/diff/diff.go249
-rw-r--r--packages/tui/internal/components/list/list.go6
-rw-r--r--packages/tui/internal/components/modal/modal.go6
-rw-r--r--packages/tui/internal/components/qr/qr.go6
-rw-r--r--packages/tui/internal/components/status/status.go21
-rw-r--r--packages/tui/internal/components/toast/toast.go9
-rw-r--r--packages/tui/internal/config/config.go2
-rw-r--r--packages/tui/internal/layout/container.go5
-rw-r--r--packages/tui/internal/layout/flex.go5
-rw-r--r--packages/tui/internal/styles/background.go4
-rw-r--r--packages/tui/internal/styles/markdown.go117
-rw-r--r--packages/tui/internal/styles/styles.go151
-rw-r--r--packages/tui/internal/styles/utilities.go295
-rw-r--r--packages/tui/internal/theme/loader.go13
-rw-r--r--packages/tui/internal/theme/manager.go134
-rw-r--r--packages/tui/internal/theme/system.go299
-rw-r--r--packages/tui/internal/tui/tui.go16
-rw-r--r--packages/tui/internal/util/color.go93
-rw-r--r--packages/web/public/theme.json20
-rw-r--r--packages/web/src/content/docs/docs/themes.mdx28
33 files changed, 1221 insertions, 436 deletions
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 172b5ad93..cb898b5f9 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -66,6 +66,7 @@ func main() {
program := tea.NewProgram(
tui.NewModel(app_),
+ // tea.WithColorProfile(colorprofile.ANSI),
tea.WithAltScreen(),
tea.WithKeyboardEnhancements(),
tea.WithMouseCellMotion(),
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 4c156b68d..e8775921a 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -14,6 +14,7 @@ import (
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client"
@@ -103,6 +104,12 @@ func New(
}
if appState.Theme != "" {
+ if appState.Theme == "system" && styles.Terminal != nil {
+ theme.UpdateSystemTheme(
+ styles.Terminal.Background,
+ styles.Terminal.BackgroundIsDark,
+ )
+ }
theme.SetTheme(appState.Theme)
}
diff --git a/packages/tui/internal/completions/commands.go b/packages/tui/internal/completions/commands.go
index 3becb7790..21a26cbc8 100644
--- a/packages/tui/internal/completions/commands.go
+++ b/packages/tui/internal/completions/commands.go
@@ -9,6 +9,7 @@ import (
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
+ "github.com/sst/opencode/internal/styles"
"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 {
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)
return dialog.NewCompletionItem(dialog.CompletionItem{
Title: title,
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index b5d1cc034..79b80cf69 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -26,6 +26,9 @@ type EditorComponent interface {
Content() string
Lines() int
Value() string
+ Focused() bool
+ Focus() (tea.Model, tea.Cmd)
+ Blur()
Submit() (tea.Model, tea.Cmd)
Clear() (tea.Model, tea.Cmd)
Paste() (tea.Model, tea.Cmd)
@@ -48,7 +51,7 @@ type editorComponent struct {
}
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) {
@@ -69,7 +72,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ThemeSelectedMsg:
m.textarea = createTextArea(&m.textarea)
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:
if msg.IsCommand {
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 {
t := theme.CurrentTheme()
- base := styles.BaseStyle().Background(t.Background()).Render
- muted := styles.Muted().Background(t.Background()).Render
- promptStyle := lipgloss.NewStyle().
+ base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
+ muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
+ promptStyle := styles.NewStyle().Foreground(t.Primary()).
Padding(0, 0, 0, 1).
- Bold(true).
- Foreground(t.Primary())
+ Bold(true)
prompt := promptStyle.Render(">")
textarea := lipgloss.JoinHorizontal(
@@ -117,11 +119,11 @@ func (m *editorComponent) Content() string {
prompt,
m.textarea.View(),
)
- textarea = styles.BaseStyle().
+ textarea = styles.NewStyle().
+ Background(t.BackgroundElement()).
Width(m.width).
PaddingTop(1).
PaddingBottom(1).
- Background(t.BackgroundElement()).
Render(textarea)
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)
- spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("")
+ spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
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")
return content
@@ -156,6 +158,18 @@ func (m *editorComponent) View() string {
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) {
return m.width, m.height
}
@@ -297,14 +311,14 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta := textarea.New()
- ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
- ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
- ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
- ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
- ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
- ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+ ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+ ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
+ ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+ ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+ ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+ ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
+ ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+ ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Cursor.Color = t.Primary()
ta.Prompt = " "
@@ -317,18 +331,21 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta.SetHeight(existing.Height())
}
- ta.Focus()
+ // ta.Focus()
return ta
}
func createSpinner() spinner.Model {
+ t := theme.CurrentTheme()
return spinner.New(
spinner.WithSpinner(spinner.Ellipsis),
spinner.WithStyle(
- styles.
- Muted().
- Background(theme.CurrentTheme().Background()).
- Width(3)),
+ styles.NewStyle().
+ Foreground(t.Background()).
+ Foreground(t.TextMuted()).
+ Width(3).
+ Lipgloss(),
+ ),
)
}
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index c20bf6447..4f0293de5 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -129,15 +129,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
option(renderer)
}
- style := styles.BaseStyle().
+ style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
// MarginTop(renderer.marginTop).
// MarginBottom(renderer.marginBottom).
PaddingTop(renderer.paddingTop).
PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight).
- Background(t.BackgroundPanel()).
- Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder())
align := lipgloss.Left
@@ -179,13 +177,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
layout.Current.Container.Width,
align,
content,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
content = lipgloss.PlaceHorizontal(
layout.Current.Viewport.Width,
lipgloss.Center,
content,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
if renderer.marginTop > 0 {
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))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
if message.Role == client.Assistant {
- markdownWidth = width - padding - 4 - 2
+ markdownWidth = width - padding - 4 - 3
}
if message.Role == client.User {
text = strings.ReplaceAll(text, "<", "\\<")
@@ -275,9 +273,10 @@ func renderToolInvocation(
}
t := theme.CurrentTheme()
- style := styles.Muted().
- Width(outerWidth).
+ style := styles.NewStyle().
+ Foreground(t.TextMuted()).
Background(t.BackgroundPanel()).
+ Width(outerWidth).
PaddingTop(paddingTop).
PaddingBottom(paddingBottom).
PaddingLeft(2).
@@ -293,7 +292,9 @@ func renderToolInvocation(
if !showDetails {
title = "∟ " + title
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),
WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()),
@@ -334,9 +335,9 @@ func renderToolInvocation(
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
if m, ok := metadata.Get("message"); ok {
style = style.BorderLeftForeground(t.Error())
- error = styles.BaseStyle().
- Background(t.BackgroundPanel()).
+ error = styles.NewStyle().
Foreground(t.Error()).
+ Background(t.BackgroundPanel()).
Render(m.(string))
error = renderContentBlock(
error,
@@ -374,7 +375,7 @@ func renderToolInvocation(
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
}
formattedDiff = strings.TrimSpace(formattedDiff)
- formattedDiff = lipgloss.NewStyle().
+ formattedDiff = styles.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundPanel()).
@@ -394,7 +395,7 @@ func renderToolInvocation(
lipgloss.Center,
lipgloss.Top,
body,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
}
}
@@ -506,7 +507,7 @@ func renderToolInvocation(
if !showDetails {
title = "∟ " + title
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
if isLast {
paddingBottom = 1
@@ -530,7 +531,7 @@ func renderToolInvocation(
layout.Current.Viewport.Width,
lipgloss.Center,
content,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
if showDetails && body != "" && error == "" {
content += "\n" + body
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index bdb6a6f82..da45545c9 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -245,7 +245,7 @@ func (m *messagesComponent) renderView() {
m.width,
lipgloss.Center,
block,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
))
}
@@ -260,8 +260,8 @@ func (m *messagesComponent) header() string {
t := theme.CurrentTheme()
width := layout.Current.Container.Width
- base := styles.BaseStyle().Background(t.Background()).Render
- muted := styles.Muted().Background(t.Background()).Render
+ base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
+ muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
headerLines := []string{}
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
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 = styles.BaseStyle().
+ header = styles.NewStyle().
+ Background(t.Background()).
Width(width).
PaddingLeft(2).
PaddingRight(2).
- Background(t.Background()).
BorderLeft(true).
BorderRight(true).
BorderBackground(t.Background()).
@@ -306,7 +306,7 @@ func (m *messagesComponent) View() string {
m.width,
lipgloss.Center,
m.header(),
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
),
m.viewport.View(),
)
@@ -314,9 +314,9 @@ func (m *messagesComponent) View() string {
func (m *messagesComponent) home() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle().Background(t.Background())
+ baseStyle := styles.NewStyle().Background(t.Background())
base := baseStyle.Render
- muted := styles.Muted().Background(t.Background()).Render
+ muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
open := `
█▀▀█ █▀▀█ █▀▀ █▀▀▄
@@ -335,9 +335,9 @@ func (m *messagesComponent) home() string {
// cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config
- versionStyle := lipgloss.NewStyle().
- Background(t.Background()).
+ versionStyle := styles.NewStyle().
Foreground(t.TextMuted()).
+ Background(t.Background()).
Width(lipgloss.Width(logo)).
Align(lipgloss.Right)
version := versionStyle.Render(m.app.Version)
@@ -347,14 +347,14 @@ func (m *messagesComponent) home() string {
m.width,
lipgloss.Center,
logoAndVersion,
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
m.commands.SetBackgroundColor(t.Background())
commands := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.commands.View(),
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
lines := []string{}
@@ -372,7 +372,7 @@ func (m *messagesComponent) home() string {
lipgloss.Center,
lipgloss.Center,
baseStyle.Render(strings.Join(lines, "\n")),
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ styles.WhitespaceStyle(t.Background()),
)
}
diff --git a/packages/tui/internal/components/commands/commands.go b/packages/tui/internal/components/commands/commands.go
index 8031e5351..d7f334c35 100644
--- a/packages/tui/internal/components/commands/commands.go
+++ b/packages/tui/internal/components/commands/commands.go
@@ -60,15 +60,9 @@ func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *commandsComponent) View() string {
t := theme.CurrentTheme()
- triggerStyle := lipgloss.NewStyle().
- Foreground(t.Primary()).
- Bold(true)
-
- descriptionStyle := lipgloss.NewStyle().
- Foreground(t.Text())
-
- keybindStyle := lipgloss.NewStyle().
- Foreground(t.TextMuted())
+ triggerStyle := styles.NewStyle().Foreground(t.Primary()).Bold(true)
+ descriptionStyle := styles.NewStyle().Foreground(t.Text())
+ keybindStyle := styles.NewStyle().Foreground(t.TextMuted())
if c.background != nil {
triggerStyle = triggerStyle.Background(*c.background)
@@ -99,10 +93,11 @@ func (c *commandsComponent) View() string {
}
if len(commandsToShow) == 0 {
+ muted := styles.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
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
@@ -188,7 +183,7 @@ func (c *commandsComponent) View() string {
// Remove trailing newline
result := strings.TrimSuffix(output.String(), "\n")
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
diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go
index 4924885f9..f2ed30fff 100644
--- a/packages/tui/internal/components/dialog/complete.go
+++ b/packages/tui/internal/components/dialog/complete.go
@@ -26,7 +26,7 @@ type CompletionItemI interface {
func (ci *CompletionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle().Foreground(t.Text())
itemStyle := baseStyle.
Background(t.BackgroundElement()).
@@ -185,7 +185,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *completionDialogComponent) View() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle().Foreground(t.Text())
maxWidth := 40
completions := c.list.GetItems()
diff --git a/packages/tui/internal/components/dialog/init.go b/packages/tui/internal/components/dialog/init.go
index 339e31ca4..cf81e5a07 100644
--- a/packages/tui/internal/components/dialog/init.go
+++ b/packages/tui/internal/components/dialog/init.go
@@ -94,7 +94,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements tea.Model.
func (m InitDialogCmp) View() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle().Foreground(t.Text())
// Calculate width needed for content
maxWidth := 60 // Width for explanation text
diff --git a/packages/tui/internal/components/dialog/models.go b/packages/tui/internal/components/dialog/models.go
index 5da3c9eef..52ece493f 100644
--- a/packages/tui/internal/components/dialog/models.go
+++ b/packages/tui/internal/components/dialog/models.go
@@ -158,7 +158,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
}
t := theme.CurrentTheme()
- return styles.BaseStyle().
+ return styles.NewStyle().
Foreground(t.TextMuted()).
Width(maxWidth).
Align(lipgloss.Right).
diff --git a/packages/tui/internal/components/dialog/permission.go b/packages/tui/internal/components/dialog/permission.go
index 1f573e59d..5bc40624b 100644
--- a/packages/tui/internal/components/dialog/permission.go
+++ b/packages/tui/internal/components/dialog/permission.go
@@ -145,7 +145,7 @@ func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
func (p *permissionDialogComponent) renderButtons() string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle().Foreground(t.Text())
allowStyle := baseStyle
allowSessionStyle := baseStyle
@@ -355,8 +355,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
func (p *permissionDialogComponent) styleViewport() string {
t := theme.CurrentTheme()
- contentStyle := lipgloss.NewStyle().
- Background(t.Background())
+ contentStyle := styles.NewStyle().Background(t.Background())
return contentStyle.Render(p.contentViewPort.View())
}
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go
index 71f3f2e9e..6ee8d1cce 100644
--- a/packages/tui/internal/components/dialog/session.go
+++ b/packages/tui/internal/components/dialog/session.go
@@ -7,7 +7,6 @@ import (
"slices"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list"
@@ -33,7 +32,7 @@ type sessionItem struct {
func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle()
var text string
if s.isDeleteConfirming {
@@ -44,7 +43,7 @@ func (s sessionItem) Render(selected bool, width int) string {
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
- var itemStyle lipgloss.Style
+ var itemStyle styles.Style
if selected {
if s.isDeleteConfirming {
// Red background for delete confirmation
@@ -151,9 +150,9 @@ func (s *sessionDialog) Render(background string) string {
listView := s.list.View()
t := theme.CurrentTheme()
- helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
- helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
- helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
+ helpStyle := styles.NewStyle().PaddingLeft(1).PaddingTop(1)
+ helpText := styles.NewStyle().Foreground(t.Text()).Render("x/del")
+ helpText = helpText + styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
helpText = helpStyle.Render(helpText)
content := strings.Join([]string{listView, helpText}, "\n")
diff --git a/packages/tui/internal/components/dialog/theme.go b/packages/tui/internal/components/dialog/theme.go
index eea3e74e7..b6e970617 100644
--- a/packages/tui/internal/components/dialog/theme.go
+++ b/packages/tui/internal/components/dialog/theme.go
@@ -103,7 +103,7 @@ func NewThemeDialog() ThemeDialog {
// Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx)
-
+
// Set the max width for the list to match the modal width
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
diff --git a/packages/tui/internal/components/diff/diff.go b/packages/tui/internal/components/diff/diff.go
index 13f0f562f..9475c1f19 100644
--- a/packages/tui/internal/components/diff/diff.go
+++ b/packages/tui/internal/components/diff/diff.go
@@ -441,84 +441,84 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
<entry type="TextWhitespace" style="%s"/>
</style>
`,
- getColor(t.BackgroundPanel()), // Background
- getColor(t.Text()), // Text
- getColor(t.Text()), // Other
- getColor(t.Error()), // Error
-
- getColor(t.SyntaxKeyword()), // Keyword
- getColor(t.SyntaxKeyword()), // KeywordConstant
- getColor(t.SyntaxKeyword()), // KeywordDeclaration
- getColor(t.SyntaxKeyword()), // KeywordNamespace
- getColor(t.SyntaxKeyword()), // KeywordPseudo
- getColor(t.SyntaxKeyword()), // KeywordReserved
- getColor(t.SyntaxType()), // KeywordType
-
- getColor(t.Text()), // Name
- getColor(t.SyntaxVariable()), // NameAttribute
- getColor(t.SyntaxType()), // NameBuiltin
- getColor(t.SyntaxVariable()), // NameBuiltinPseudo
- getColor(t.SyntaxType()), // NameClass
- getColor(t.SyntaxVariable()), // NameConstant
- getColor(t.SyntaxFunction()), // NameDecorator
- getColor(t.SyntaxVariable()), // NameEntity
- getColor(t.SyntaxType()), // NameException
- getColor(t.SyntaxFunction()), // NameFunction
- getColor(t.Text()), // NameLabel
- getColor(t.SyntaxType()), // NameNamespace
- getColor(t.SyntaxVariable()), // NameOther
- getColor(t.SyntaxKeyword()), // NameTag
- getColor(t.SyntaxVariable()), // NameVariable
- getColor(t.SyntaxVariable()), // NameVariableClass
- getColor(t.SyntaxVariable()), // NameVariableGlobal
- getColor(t.SyntaxVariable()), // NameVariableInstance
-
- getColor(t.SyntaxString()), // Literal
- getColor(t.SyntaxString()), // LiteralDate
- getColor(t.SyntaxString()), // LiteralString
- getColor(t.SyntaxString()), // LiteralStringBacktick
- getColor(t.SyntaxString()), // LiteralStringChar
- getColor(t.SyntaxString()), // LiteralStringDoc
- getColor(t.SyntaxString()), // LiteralStringDouble
- getColor(t.SyntaxString()), // LiteralStringEscape
- getColor(t.SyntaxString()), // LiteralStringHeredoc
- getColor(t.SyntaxString()), // LiteralStringInterpol
- getColor(t.SyntaxString()), // LiteralStringOther
- getColor(t.SyntaxString()), // LiteralStringRegex
- getColor(t.SyntaxString()), // LiteralStringSingle
- getColor(t.SyntaxString()), // LiteralStringSymbol
-
- getColor(t.SyntaxNumber()), // LiteralNumber
- getColor(t.SyntaxNumber()), // LiteralNumberBin
- getColor(t.SyntaxNumber()), // LiteralNumberFloat
- getColor(t.SyntaxNumber()), // LiteralNumberHex
- getColor(t.SyntaxNumber()), // LiteralNumberInteger
- getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
- getColor(t.SyntaxNumber()), // LiteralNumberOct
-
- getColor(t.SyntaxOperator()), // Operator
- getColor(t.SyntaxKeyword()), // OperatorWord
- getColor(t.SyntaxPunctuation()), // Punctuation
-
- getColor(t.SyntaxComment()), // Comment
- getColor(t.SyntaxComment()), // CommentHashbang
- getColor(t.SyntaxComment()), // CommentMultiline
- getColor(t.SyntaxComment()), // CommentSingle
- getColor(t.SyntaxComment()), // CommentSpecial
- getColor(t.SyntaxKeyword()), // CommentPreproc
-
- getColor(t.Text()), // Generic
- getColor(t.Error()), // GenericDeleted
- getColor(t.Text()), // GenericEmph
- getColor(t.Error()), // GenericError
- getColor(t.Text()), // GenericHeading
- getColor(t.Success()), // GenericInserted
- getColor(t.TextMuted()), // GenericOutput
- getColor(t.Text()), // GenericPrompt
- getColor(t.Text()), // GenericStrong
- getColor(t.Text()), // GenericSubheading
- getColor(t.Error()), // GenericTraceback
- getColor(t.Text()), // TextWhitespace
+ getChromaColor(t.BackgroundPanel()), // Background
+ getChromaColor(t.Text()), // Text
+ getChromaColor(t.Text()), // Other
+ getChromaColor(t.Error()), // Error
+
+ getChromaColor(t.SyntaxKeyword()), // Keyword
+ getChromaColor(t.SyntaxKeyword()), // KeywordConstant
+ getChromaColor(t.SyntaxKeyword()), // KeywordDeclaration
+ getChromaColor(t.SyntaxKeyword()), // KeywordNamespace
+ getChromaColor(t.SyntaxKeyword()), // KeywordPseudo
+ getChromaColor(t.SyntaxKeyword()), // KeywordReserved
+ getChromaColor(t.SyntaxType()), // KeywordType
+
+ getChromaColor(t.Text()), // Name
+ getChromaColor(t.SyntaxVariable()), // NameAttribute
+ getChromaColor(t.SyntaxType()), // NameBuiltin
+ getChromaColor(t.SyntaxVariable()), // NameBuiltinPseudo
+ getChromaColor(t.SyntaxType()), // NameClass
+ getChromaColor(t.SyntaxVariable()), // NameConstant
+ getChromaColor(t.SyntaxFunction()), // NameDecorator
+ getChromaColor(t.SyntaxVariable()), // NameEntity
+ getChromaColor(t.SyntaxType()), // NameException
+ getChromaColor(t.SyntaxFunction()), // NameFunction
+ getChromaColor(t.Text()), // NameLabel
+ getChromaColor(t.SyntaxType()), // NameNamespace
+ getChromaColor(t.SyntaxVariable()), // NameOther
+ getChromaColor(t.SyntaxKeyword()), // NameTag
+ getChromaColor(t.SyntaxVariable()), // NameVariable
+ getChromaColor(t.SyntaxVariable()), // NameVariableClass
+ getChromaColor(t.SyntaxVariable()), // NameVariableGlobal
+ getChromaColor(t.SyntaxVariable()), // NameVariableInstance
+
+ getChromaColor(t.SyntaxString()), // Literal
+ getChromaColor(t.SyntaxString()), // LiteralDate
+ getChromaColor(t.SyntaxString()), // LiteralString
+ getChromaColor(t.SyntaxString()), // LiteralStringBacktick
+ getChromaColor(t.SyntaxString()), // LiteralStringChar
+ getChromaColor(t.SyntaxString()), // LiteralStringDoc
+ getChromaColor(t.SyntaxString()), // LiteralStringDouble
+ getChromaColor(t.SyntaxString()), // LiteralStringEscape
+ getChromaColor(t.SyntaxString()), // LiteralStringHeredoc
+ getChromaColor(t.SyntaxString()), // LiteralStringInterpol
+ getChromaColor(t.SyntaxString()), // LiteralStringOther
+ getChromaColor(t.SyntaxString()), // LiteralStringRegex
+ getChromaColor(t.SyntaxString()), // LiteralStringSingle
+ getChromaColor(t.SyntaxString()), // LiteralStringSymbol
+
+ getChromaColor(t.SyntaxNumber()), // LiteralNumber
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberBin
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberFloat
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberHex
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberInteger
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
+ getChromaColor(t.SyntaxNumber()), // LiteralNumberOct
+
+ getChromaColor(t.SyntaxOperator()), // Operator
+ getChromaColor(t.SyntaxKeyword()), // OperatorWord
+ getChromaColor(t.SyntaxPunctuation()), // Punctuation
+
+ getChromaColor(t.SyntaxComment()), // Comment
+ getChromaColor(t.SyntaxComment()), // CommentHashbang
+ getChromaColor(t.SyntaxComment()), // CommentMultiline
+ getChromaColor(t.SyntaxComment()), // CommentSingle
+ getChromaColor(t.SyntaxComment()), // CommentSpecial
+ getChromaColor(t.SyntaxKeyword()), // CommentPreproc
+
+ getChromaColor(t.Text()), // Generic
+ getChromaColor(t.Error()), // GenericDeleted
+ getChromaColor(t.Text()), // GenericEmph
+ getChromaColor(t.Error()), // GenericError
+ getChromaColor(t.Text()), // GenericHeading
+ getChromaColor(t.Success()), // GenericInserted
+ getChromaColor(t.TextMuted()), // GenericOutput
+ getChromaColor(t.Text()), // GenericPrompt
+ getChromaColor(t.Text()), // GenericStrong
+ getChromaColor(t.Text()), // GenericSubheading
+ getChromaColor(t.Error()), // GenericTraceback
+ getChromaColor(t.Text()), // TextWhitespace
)
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
s, err := style.Builder().Transform(
func(t chroma.StyleEntry) chroma.StyleEntry {
+ if _, ok := bg.(lipgloss.NoColor); ok {
+ return t
+ }
r, g, b, _ := bg.RGBA()
t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
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
-func getColor(adaptiveColor compat.AdaptiveColor) string {
+func getColor(adaptiveColor compat.AdaptiveColor) *string {
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
func highlightLine(fileName string, line string, bg color.Color) string {
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
-func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
- removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
- addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
- contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
- lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted())
+func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle stylesi.Style) {
+ removedLineStyle = stylesi.NewStyle().Background(t.DiffRemovedBg())
+ addedLineStyle = stylesi.NewStyle().Background(t.DiffAddedBg())
+ contextLineStyle = stylesi.NewStyle().Background(t.DiffContextBg())
+ lineNumberStyle = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffLineNumber())
return
}
@@ -613,9 +624,17 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
currentPos := 0
// Get the appropriate color based on terminal background
- bgColor := lipgloss.Color(getColor(highlightBg))
- fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
+ bg := getColor(highlightBg)
+ 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); {
// Check if we're at an ANSI sequence
isAnsi := false
@@ -651,12 +670,20 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
currentStyle := ansiSequences[currentPos]
// Apply foreground and background highlight
- sb.WriteString("\x1b[38;2;")
- r, g, b, _ := fgColor.RGBA()
- sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
- sb.WriteString("\x1b[48;2;")
- r, g, b, _ = bgColor.RGBA()
- sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+ if fgColor != nil {
+ sb.WriteString("\x1b[38;2;")
+ r, g, b, _ := fgColor.RGBA()
+ sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+ } else {
+ 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)
// 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
-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
var styledMarker string
switch dl.Kind {
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:
- styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
+ styledMarker = stylesi.NewStyle().Foreground(t.DiffAdded()).Background(t.DiffAddedBg()).Render(marker)
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:
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
-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
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
@@ -714,7 +741,9 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, hig
ansi.Truncate(
content,
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
var marker string
- var bgStyle lipgloss.Style
+ var bgStyle stylesi.Style
var lineNum string
var highlightColor compat.AdaptiveColor
@@ -733,8 +762,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
case LineRemoved:
marker = "-"
bgStyle = removedLineStyle
- lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
- highlightColor = t.DiffHighlightRemoved()
+ lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
+ highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
if dl.OldLineNo > 0 {
lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
} else {
@@ -743,8 +772,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
case LineAdded:
marker = "+"
bgStyle = addedLineStyle
- lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
- highlightColor = t.DiffHighlightAdded()
+ lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
+ highlightColor = t.DiffHighlightAdded() // TODO: handle "none"
if dl.NewLineNo > 0 {
lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
} else {
@@ -766,7 +795,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
// Render the content
prefixWidth := ansi.StringWidth(prefix)
contentWidth := width - prefixWidth
- content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
+ content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth)
return prefix + content
}
@@ -780,7 +809,7 @@ func renderDiffColumnLine(
t theme.Theme,
) string {
if dl == nil {
- contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
+ contextLineStyle := stylesi.NewStyle().Background(t.DiffContextBg())
return contextLineStyle.Width(colWidth).Render("")
}
@@ -788,7 +817,7 @@ func renderDiffColumnLine(
// Determine line style based on line type and column
var marker string
- var bgStyle lipgloss.Style
+ var bgStyle stylesi.Style
var lineNum string
var highlightColor compat.AdaptiveColor
@@ -798,8 +827,8 @@ func renderDiffColumnLine(
case LineRemoved:
marker = "-"
bgStyle = removedLineStyle
- lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
- highlightColor = t.DiffHighlightRemoved()
+ lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
+ highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
case LineAdded:
marker = "?"
bgStyle = contextLineStyle
@@ -818,7 +847,7 @@ func renderDiffColumnLine(
case LineAdded:
marker = "+"
bgStyle = addedLineStyle
- lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
+ lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
highlightColor = t.DiffHighlightAdded()
case LineRemoved:
marker = "?"
@@ -849,7 +878,7 @@ func renderDiffColumnLine(
// Render the content
prefixWidth := ansi.StringWidth(prefix)
contentWidth := colWidth - prefixWidth
- content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
+ content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth)
return prefix + content
}
diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go
index d5cc4b4f9..fe03f5c28 100644
--- a/packages/tui/internal/components/list/list.go
+++ b/packages/tui/internal/components/list/list.go
@@ -5,7 +5,6 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -174,11 +173,11 @@ type StringItem string
func (s StringItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
+ baseStyle := styles.NewStyle()
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
- var itemStyle lipgloss.Style
+ var itemStyle styles.Style
if selected {
itemStyle = baseStyle.
Background(t.Primary()).
@@ -187,6 +186,7 @@ func (s StringItem) Render(selected bool, width int) string {
PaddingLeft(1)
} else {
itemStyle = baseStyle.
+ Foreground(t.TextMuted()).
PaddingLeft(1)
}
diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go
index 62cafe843..6bce64247 100644
--- a/packages/tui/internal/components/modal/modal.go
+++ b/packages/tui/internal/components/modal/modal.go
@@ -90,12 +90,8 @@ func (m *Modal) Render(contentView string, background string) string {
innerWidth := outerWidth - 4
- // Base style for the modal
- baseStyle := styles.BaseStyle().
- Background(t.BackgroundElement()).
- Foreground(t.TextMuted())
+ baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
- // Add title if provided
var finalContent string
if m.title != "" {
titleStyle := baseStyle.
diff --git a/packages/tui/internal/components/qr/qr.go b/packages/tui/internal/components/qr/qr.go
index 82d597a3f..ccf28200b 100644
--- a/packages/tui/internal/components/qr/qr.go
+++ b/packages/tui/internal/components/qr/qr.go
@@ -3,7 +3,7 @@ package qr
import (
"strings"
- "github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"rsc.io/qr"
)
@@ -23,9 +23,7 @@ func Generate(text string) (string, int, error) {
}
// Create lipgloss style for QR code with theme colors
- qrStyle := lipgloss.NewStyle().
- Foreground(t.Text()).
- Background(t.Background())
+ qrStyle := styles.NewStyleWithColors(t.Text(), t.Background())
var result strings.Builder
diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go
index 62d207088..fb5ff8ced 100644
--- a/packages/tui/internal/components/status/status.go
+++ b/packages/tui/internal/components/status/status.go
@@ -36,14 +36,15 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m statusComponent) logo() string {
t := theme.CurrentTheme()
- base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
- emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
+ base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
+ emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
open := base("open")
code := emphasis("code ")
version := base(m.app.Version)
- return styles.Padded().
+ return styles.NewStyle().
Background(t.BackgroundElement()).
+ Padding(0, 1).
Render(open + code + version)
}
@@ -77,7 +78,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
func (m statusComponent) View() string {
t := theme.CurrentTheme()
if m.app.Session.Id == "" {
- return styles.BaseStyle().
+ return styles.NewStyle().
Background(t.Background()).
Width(m.width).
Height(2).
@@ -86,9 +87,10 @@ func (m statusComponent) View() string {
logo := m.logo()
- cwd := styles.Padded().
+ cwd := styles.NewStyle().
Foreground(t.TextMuted()).
Background(t.BackgroundPanel()).
+ Padding(0, 1).
Render(m.app.Info.Path.Cwd)
sessionInfo := ""
@@ -111,9 +113,10 @@ func (m statusComponent) View() string {
}
}
- sessionInfo = styles.Padded().
- Background(t.BackgroundElement()).
+ sessionInfo = styles.NewStyle().
Foreground(t.TextMuted()).
+ Background(t.BackgroundElement()).
+ Padding(0, 1).
Render(formatTokensAndCost(tokens, contextWindow, cost))
}
@@ -123,11 +126,11 @@ func (m statusComponent) View() string {
0,
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
- blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
+ blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
return blank + "\n" + status
}
diff --git a/packages/tui/internal/components/toast/toast.go b/packages/tui/internal/components/toast/toast.go
index 09b3c6288..2de6bf619 100644
--- a/packages/tui/internal/components/toast/toast.go
+++ b/packages/tui/internal/components/toast/toast.go
@@ -90,9 +90,9 @@ func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
func (tm *ToastManager) renderSingleToast(toast Toast) string {
t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle().
- Background(t.BackgroundElement()).
+ baseStyle := styles.NewStyle().
Foreground(t.Text()).
+ Background(t.BackgroundElement()).
Padding(1, 2)
maxWidth := max(40, layout.Current.Viewport.Width/3)
@@ -101,15 +101,14 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
// Build content with wrapping
var content strings.Builder
if toast.Title != nil {
- titleStyle := lipgloss.NewStyle().
- Foreground(toast.Color).
+ titleStyle := styles.NewStyle().Foreground(toast.Color).
Bold(true)
content.WriteString(titleStyle.Render(*toast.Title))
content.WriteString("\n")
}
// Wrap message text
- messageStyle := lipgloss.NewStyle()
+ messageStyle := styles.NewStyle()
contentWidth := lipgloss.Width(toast.Message)
if contentWidth > contentMaxWidth {
messageStyle = messageStyle.Width(contentMaxWidth)
diff --git a/packages/tui/internal/config/config.go b/packages/tui/internal/config/config.go
index 29db8657e..687685df3 100644
--- a/packages/tui/internal/config/config.go
+++ b/packages/tui/internal/config/config.go
@@ -18,7 +18,7 @@ type State struct {
func NewState() *State {
return &State{
- Theme: "opencode",
+ Theme: "system",
}
}
diff --git a/packages/tui/internal/layout/container.go b/packages/tui/internal/layout/container.go
index 3eda158cc..250034ebc 100644
--- a/packages/tui/internal/layout/container.go
+++ b/packages/tui/internal/layout/container.go
@@ -3,6 +3,7 @@ package layout
import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/styles"
"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 {
t := theme.CurrentTheme()
- style := lipgloss.NewStyle()
+ style := styles.NewStyle().Background(t.Background())
width := c.width
height := c.height
@@ -66,8 +67,6 @@ func (c *container) View() string {
width = c.maxWidth
}
- style = style.Background(t.Background())
-
// Apply border if any side is enabled
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
// Adjust width and height for borders
diff --git a/packages/tui/internal/layout/flex.go b/packages/tui/internal/layout/flex.go
index 35f6ba164..320a95203 100644
--- a/packages/tui/internal/layout/flex.go
+++ b/packages/tui/internal/layout/flex.go
@@ -3,6 +3,7 @@ package layout
import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@@ -66,7 +67,7 @@ func (f *flexLayout) View() string {
alignment,
child.View(),
// TODO: make configurable WithBackgroundStyle
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
)
views = append(views, view)
} else {
@@ -78,7 +79,7 @@ func (f *flexLayout) View() string {
alignment,
child.View(),
// TODO: make configurable WithBackgroundStyle
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+ lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
)
views = append(views, view)
}
diff --git a/packages/tui/internal/styles/background.go b/packages/tui/internal/styles/background.go
index 144ee819d..99b05b456 100644
--- a/packages/tui/internal/styles/background.go
+++ b/packages/tui/internal/styles/background.go
@@ -1,6 +1,9 @@
package styles
+import "image/color"
+
type TerminalInfo struct {
+ Background color.Color
BackgroundIsDark bool
}
@@ -8,6 +11,7 @@ var Terminal *TerminalInfo
func init() {
Terminal = &TerminalInfo{
+ Background: color.Black,
BackgroundIsDark: true,
}
}
diff --git a/packages/tui/internal/styles/markdown.go b/packages/tui/internal/styles/markdown.go
index f28e26793..14db75466 100644
--- a/packages/tui/internal/styles/markdown.go
+++ b/packages/tui/internal/styles/markdown.go
@@ -3,6 +3,7 @@ package styles
import (
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/lucasb-eyer/go-colorful"
"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.
func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
t := theme.CurrentTheme()
- background := stringPtr(AdaptiveColorToString(backgroundColor))
+ background := AdaptiveColorToString(backgroundColor)
return ansi.StyleConfig{
Document: ansi.StyleBlock{
@@ -37,12 +38,12 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
BlockPrefix: "",
BlockSuffix: "",
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+ Color: AdaptiveColorToString(t.MarkdownText()),
},
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())),
+ Color: AdaptiveColorToString(t.MarkdownBlockQuote()),
Italic: boolPtr(true),
Prefix: "┃ ",
},
@@ -54,108 +55,108 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
StyleBlock: ansi.StyleBlock{
IndentToken: stringPtr(" "),
StylePrimitive: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+ Color: AdaptiveColorToString(t.MarkdownText()),
},
},
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "# ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "## ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "### ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "#### ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "##### ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+ Color: AdaptiveColorToString(t.MarkdownHeading()),
Bold: boolPtr(true),
},
},
Strikethrough: ansi.StylePrimitive{
CrossedOut: boolPtr(true),
- Color: stringPtr(AdaptiveColorToString(t.TextMuted())),
+ Color: AdaptiveColorToString(t.TextMuted()),
},
Emph: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
+ Color: AdaptiveColorToString(t.MarkdownEmph()),
Italic: boolPtr(true),
},
Strong: ansi.StylePrimitive{
Bold: boolPtr(true),
- Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
+ Color: AdaptiveColorToString(t.MarkdownStrong()),
},
HorizontalRule: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())),
+ Color: AdaptiveColorToString(t.MarkdownHorizontalRule()),
Format: "\n─────────────────────────────────────────\n",
},
Item: ansi.StylePrimitive{
BlockPrefix: "• ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownListItem())),
+ Color: AdaptiveColorToString(t.MarkdownListItem()),
},
Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())),
+ Color: AdaptiveColorToString(t.MarkdownListEnumeration()),
},
Task: ansi.StyleTask{
Ticked: "[✓] ",
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownLink())),
+ Color: AdaptiveColorToString(t.MarkdownLink()),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
+ Color: AdaptiveColorToString(t.MarkdownLinkText()),
Bold: boolPtr(true),
},
Image: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownImage())),
+ Color: AdaptiveColorToString(t.MarkdownImage()),
Underline: boolPtr(true),
Format: "🖼 {{.text}}",
},
ImageText: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownImageText())),
+ Color: AdaptiveColorToString(t.MarkdownImageText()),
Format: "{{.text}}",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.MarkdownCode())),
+ Color: AdaptiveColorToString(t.MarkdownCode()),
Prefix: "",
Suffix: "",
},
@@ -165,7 +166,7 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
StylePrimitive: ansi.StylePrimitive{
BackgroundColor: background,
Prefix: " ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())),
+ Color: AdaptiveColorToString(t.MarkdownCodeBlock()),
},
},
Chroma: &ansi.Chroma{
@@ -174,109 +175,109 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
},
Text: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+ Color: AdaptiveColorToString(t.MarkdownText()),
},
Error: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.Error())),
+ Color: AdaptiveColorToString(t.Error()),
},
Comment: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxComment())),
+ Color: AdaptiveColorToString(t.SyntaxComment()),
},
CommentPreproc: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
Keyword: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
KeywordReserved: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
KeywordNamespace: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
KeywordType: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
+ Color: AdaptiveColorToString(t.SyntaxType()),
},
Operator: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxOperator())),
+ Color: AdaptiveColorToString(t.SyntaxOperator()),
},
Punctuation: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())),
+ Color: AdaptiveColorToString(t.SyntaxPunctuation()),
},
Name: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+ Color: AdaptiveColorToString(t.SyntaxVariable()),
},
NameBuiltin: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+ Color: AdaptiveColorToString(t.SyntaxVariable()),
},
NameTag: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
NameAttribute: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+ Color: AdaptiveColorToString(t.SyntaxFunction()),
},
NameClass: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
+ Color: AdaptiveColorToString(t.SyntaxType()),
},
NameConstant: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+ Color: AdaptiveColorToString(t.SyntaxVariable()),
},
NameDecorator: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+ Color: AdaptiveColorToString(t.SyntaxFunction()),
},
NameFunction: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+ Color: AdaptiveColorToString(t.SyntaxFunction()),
},
LiteralNumber: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxNumber())),
+ Color: AdaptiveColorToString(t.SyntaxNumber()),
},
LiteralString: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxString())),
+ Color: AdaptiveColorToString(t.SyntaxString()),
},
LiteralStringEscape: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+ Color: AdaptiveColorToString(t.SyntaxKeyword()),
},
GenericDeleted: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.DiffRemoved())),
+ Color: AdaptiveColorToString(t.DiffRemoved()),
},
GenericEmph: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
+ Color: AdaptiveColorToString(t.MarkdownEmph()),
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.DiffAdded())),
+ Color: AdaptiveColorToString(t.DiffAdded()),
},
GenericStrong: ansi.StylePrimitive{
BackgroundColor: background,
- Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
+ Color: AdaptiveColorToString(t.MarkdownStrong()),
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
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{
BlockPrefix: "\n ❯ ",
- Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
+ Color: AdaptiveColorToString(t.MarkdownLinkText()),
},
Text: ansi.StylePrimitive{
- Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+ Color: AdaptiveColorToString(t.MarkdownText()),
},
Paragraph: ansi.StyleBlock{
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
// 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 _, ok := color.Dark.(lipgloss.NoColor); ok {
+ return nil
+ }
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)
- return c1.Hex()
+ return stringPtr(c1.Hex())
}
diff --git a/packages/tui/internal/styles/styles.go b/packages/tui/internal/styles/styles.go
index 733fce55c..b8905f8e4 100644
--- a/packages/tui/internal/styles/styles.go
+++ b/packages/tui/internal/styles/styles.go
@@ -3,155 +3,8 @@ package styles
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
- "github.com/sst/opencode/internal/theme"
)
-// BaseStyle returns the base style with background and foreground colors
-func BaseStyle() lipgloss.Style {
- 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()
+func WhitespaceStyle(bg compat.AdaptiveColor) lipgloss.WhitespaceOption {
+ return lipgloss.WithWhitespaceStyle(NewStyle().Background(bg).Lipgloss())
}
diff --git a/packages/tui/internal/styles/utilities.go b/packages/tui/internal/styles/utilities.go
new file mode 100644
index 000000000..29d10f5c5
--- /dev/null
+++ b/packages/tui/internal/styles/utilities.go
@@ -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)}
+}
diff --git a/packages/tui/internal/theme/loader.go b/packages/tui/internal/theme/loader.go
index 7df46b7fe..82c2fcd24 100644
--- a/packages/tui/internal/theme/loader.go
+++ b/packages/tui/internal/theme/loader.go
@@ -171,7 +171,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
switch v := value.(type) {
case string:
- if strings.HasPrefix(v, "#") {
+ if strings.HasPrefix(v, "#") || v == "none" {
return v, nil
}
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) {
switch v := value.(type) {
case string:
- if strings.HasPrefix(v, "#") {
+ if strings.HasPrefix(v, "#") || v == "none" {
return v, nil
}
return r.resolveReference(v)
@@ -240,6 +240,12 @@ func (r *colorResolver) resolveReference(ref string) (any, error) {
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
switch v := value.(type) {
case string:
+ if v == "none" {
+ return compat.AdaptiveColor{
+ Dark: lipgloss.NoColor{},
+ Light: lipgloss.NoColor{},
+ }, nil
+ }
return compat.AdaptiveColor{
Dark: 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) {
switch v := value.(type) {
case string:
+ if v == "none" {
+ return lipgloss.NoColor{}, nil
+ }
return lipgloss.Color(v), nil
case float64:
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
diff --git a/packages/tui/internal/theme/manager.go b/packages/tui/internal/theme/manager.go
index 87fbc131d..583e8c491 100644
--- a/packages/tui/internal/theme/manager.go
+++ b/packages/tui/internal/theme/manager.go
@@ -2,19 +2,25 @@ package theme
import (
"fmt"
+ "image/color"
"slices"
+ "strconv"
"strings"
"sync"
"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.
// It maintains a registry of available themes and tracks the currently active theme.
type Manager struct {
- themes map[string]Theme
- currentName string
- mu sync.RWMutex
+ themes map[string]Theme
+ currentName string
+ currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
+ mu sync.RWMutex
}
// 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 globalManager.currentName == "" {
globalManager.currentName = name
+ globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
}
}
@@ -44,11 +51,13 @@ func SetTheme(name string) error {
defer globalManager.mu.Unlock()
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)
}
globalManager.currentName = name
+ globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
return nil
}
@@ -84,7 +93,11 @@ func AvailableThemes() []string {
names = append(names, name)
}
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" {
return -1
} else if b == "opencode" {
@@ -103,3 +116,114 @@ func GetTheme(name string) Theme {
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())
+}
diff --git a/packages/tui/internal/theme/system.go b/packages/tui/internal/theme/system.go
new file mode 100644
index 000000000..7524bb3f9
--- /dev/null
+++ b/packages/tui/internal/theme/system.go
@@ -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),
+ }
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 503af9fee..500ab56d4 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -22,6 +22,7 @@ import (
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
+ "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"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...)
case tea.BackgroundColorMsg:
styles.Terminal = &styles.TerminalInfo{
+ Background: msg.Color,
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:
var cmd tea.Cmd
if a.modal != nil {
@@ -424,6 +435,9 @@ func (a appModel) View() string {
appView = a.toastManager.RenderOverlay(appView)
+ if theme.CurrentThemeUsesAnsiColors() {
+ appView = util.ConvertRGBToAnsi16Colors(appView)
+ }
return appView
}
diff --git a/packages/tui/internal/util/color.go b/packages/tui/internal/util/color.go
new file mode 100644
index 000000000..f0d73bcb2
--- /dev/null
+++ b/packages/tui/internal/util/color.go
@@ -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
+// }
diff --git a/packages/web/public/theme.json b/packages/web/public/theme.json
index e8e939ea8..0b1b95f02 100644
--- a/packages/web/public/theme.json
+++ b/packages/web/public/theme.json
@@ -22,6 +22,11 @@
"minimum": 0,
"maximum": 255,
"description": "ANSI color code (0-255)"
+ },
+ {
+ "type": "string",
+ "enum": ["none"],
+ "description": "No color (uses terminal default)"
}
]
}
@@ -112,6 +117,11 @@
},
{
"type": "string",
+ "enum": ["none"],
+ "description": "No color (uses terminal default)"
+ },
+ {
+ "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color in the theme or defs"
},
@@ -133,6 +143,11 @@
},
{
"type": "string",
+ "enum": ["none"],
+ "description": "No color (uses terminal default)"
+ },
+ {
+ "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for dark mode"
}
@@ -153,6 +168,11 @@
},
{
"type": "string",
+ "enum": ["none"],
+ "description": "No color (uses terminal default)"
+ },
+ {
+ "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for light mode"
}
diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx
index 487bcad32..436af67c3 100644
--- a/packages/web/src/content/docs/docs/themes.mdx
+++ b/packages/web/src/content/docs/docs/themes.mdx
@@ -2,7 +2,7 @@
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
@@ -37,6 +37,7 @@ Themes use a flexible JSON format with support for:
- **ANSI colors**: `3` (0-255)
- **Color references**: `"primary"` or custom definitions
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
+- **No color**: `"none"` - Uses the terminal's default color (transparent)
### 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.
+### 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
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
- `everforest` - Everforest theme
- `ayu` - Ayu dark theme
@@ -281,7 +302,8 @@ opencode comes with several built-in themes:
- `gruvbox` - Gruvbox theme
- `kanagawa` - Kanagawa 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