From 2e26b58d160cc7cfc5c403cd63cbb30fad9fe8d0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:16:07 -0500 Subject: [PATCH] feat: default system theme (#419) Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com> --- packages/tui/cmd/opencode/main.go | 1 + packages/tui/internal/app/app.go | 7 + packages/tui/internal/completions/commands.go | 3 +- .../tui/internal/components/chat/editor.go | 65 ++-- .../tui/internal/components/chat/message.go | 31 +- .../tui/internal/components/chat/messages.go | 26 +- .../internal/components/commands/commands.go | 19 +- .../internal/components/dialog/complete.go | 4 +- .../tui/internal/components/dialog/init.go | 2 +- .../tui/internal/components/dialog/models.go | 2 +- .../internal/components/dialog/permission.go | 5 +- .../tui/internal/components/dialog/session.go | 11 +- .../tui/internal/components/dialog/theme.go | 2 +- packages/tui/internal/components/diff/diff.go | 235 ++++++++------ packages/tui/internal/components/list/list.go | 6 +- .../tui/internal/components/modal/modal.go | 6 +- packages/tui/internal/components/qr/qr.go | 6 +- .../tui/internal/components/status/status.go | 21 +- .../tui/internal/components/toast/toast.go | 9 +- packages/tui/internal/config/config.go | 2 +- packages/tui/internal/layout/container.go | 5 +- packages/tui/internal/layout/flex.go | 5 +- packages/tui/internal/styles/background.go | 4 + packages/tui/internal/styles/markdown.go | 117 +++---- packages/tui/internal/styles/styles.go | 151 +-------- packages/tui/internal/styles/utilities.go | 295 +++++++++++++++++ packages/tui/internal/theme/loader.go | 13 +- packages/tui/internal/theme/manager.go | 134 +++++++- packages/tui/internal/theme/system.go | 299 ++++++++++++++++++ packages/tui/internal/tui/tui.go | 16 +- packages/tui/internal/util/color.go | 93 ++++++ packages/web/public/theme.json | 20 ++ packages/web/src/content/docs/docs/themes.mdx | 28 +- 33 files changed, 1214 insertions(+), 429 deletions(-) create mode 100644 packages/tui/internal/styles/utilities.go create mode 100644 packages/tui/internal/theme/system.go create mode 100644 packages/tui/internal/util/color.go diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go index 172b5ad9..cb898b5f 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 4c156b68..e8775921 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 3becb779..21a26cbc 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 b5d1cc03..79b80cf6 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 c20bf644..4f0293de 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 bdb6a6f8..da45545c 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 8031e535..d7f334c3 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 4924885f..f2ed30ff 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 339e31ca..cf81e5a0 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 5da3c9ee..52ece493 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 1f573e59..5bc40624 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 71f3f2e9..6ee8d1cc 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 eea3e74e..b6e97061 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 13f0f562..9475c1f1 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 `, - getColor(t.BackgroundPanel()), // Background - getColor(t.Text()), // Text - getColor(t.Text()), // Other - getColor(t.Error()), // Error + getChromaColor(t.BackgroundPanel()), // Background + getChromaColor(t.Text()), // Text + getChromaColor(t.Text()), // Other + getChromaColor(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 + 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 - 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 + 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 - 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 + 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 - 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 + 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 - getColor(t.SyntaxOperator()), // Operator - getColor(t.SyntaxKeyword()), // OperatorWord - getColor(t.SyntaxPunctuation()), // Punctuation + getChromaColor(t.SyntaxOperator()), // Operator + getChromaColor(t.SyntaxKeyword()), // OperatorWord + getChromaColor(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 + getChromaColor(t.SyntaxComment()), // Comment + getChromaColor(t.SyntaxComment()), // CommentHashbang + getChromaColor(t.SyntaxComment()), // CommentMultiline + getChromaColor(t.SyntaxComment()), // CommentSingle + getChromaColor(t.SyntaxComment()), // CommentSpecial + getChromaColor(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.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 d5cc4b4f..fe03f5c2 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 62cafe84..6bce6424 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 82d597a3..ccf28200 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 62d20708..fb5ff8ce 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 09b3c628..2de6bf61 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 29db8657..687685df 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 3eda158c..250034eb 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 35f6ba16..320a9520 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 144ee819..99b05b45 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 f28e2679..14db7546 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 733fce55..b8905f8e 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 00000000..29d10f5c --- /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 7df46b7f..82c2fcd2 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 87fbc131..583e8c49 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 00000000..7524bb3f --- /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 503af9fe..500ab56d 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 00000000..f0d73bcb --- /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 e8e939ea..0b1b95f0 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)" } ] } @@ -110,6 +115,11 @@ "maximum": 255, "description": "ANSI color code (0-255, same for dark and light)" }, + { + "type": "string", + "enum": ["none"], + "description": "No color (uses terminal default)" + }, { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", @@ -131,6 +141,11 @@ "maximum": 255, "description": "ANSI color code for dark mode" }, + { + "type": "string", + "enum": ["none"], + "description": "No color (uses terminal default)" + }, { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", @@ -151,6 +166,11 @@ "maximum": 255, "description": "ANSI color code for light mode" }, + { + "type": "string", + "enum": ["none"], + "description": "No color (uses terminal default)" + }, { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$", diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx index 487bcad3..436af67c 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