From a41a6901ae7e9b9058335e840d0a539499084908 Mon Sep 17 00:00:00 2001 From: Marco Caceffo Date: Thu, 3 Jul 2025 13:16:29 +0200 Subject: [PATCH 1/4] transparent theme option --- internal/config/config.go | 19 ++- internal/tui/components/core/status.go | 160 +++++++++++++------ internal/tui/components/dialog/commands.go | 24 ++- internal/tui/components/dialog/complete.go | 15 +- internal/tui/components/dialog/filepicker.go | 15 +- internal/tui/components/dialog/models.go | 13 +- internal/tui/components/dialog/permission.go | 18 ++- internal/tui/components/dialog/session.go | 15 +- internal/tui/components/dialog/theme.go | 30 ++-- internal/tui/styles/background.go | 96 +++++++++++ internal/tui/styles/styles.go | 2 +- internal/tui/theme/manager.go | 23 ++- internal/tui/theme/theme.go | 27 ++++ 13 files changed, 369 insertions(+), 88 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 630fac9b6..807d484e5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -71,7 +71,8 @@ type LSPConfig struct { // TUIConfig defines the configuration for the Terminal User Interface. type TUIConfig struct { - Theme string `json:"theme,omitempty"` + Theme string `json:"theme,omitempty"` + TransparentBackground bool `json:"transparentBackground,omitempty"` } // ShellConfig defines the configuration for the shell used by the bash tool. @@ -231,6 +232,7 @@ func setDefaults(debug bool) { viper.SetDefault("data.directory", defaultDataDirectory) viper.SetDefault("contextPaths", defaultContextPaths) viper.SetDefault("tui.theme", "opencode") + viper.SetDefault("tui.transparentBackground", false) viper.SetDefault("autoCompact", true) // Set default shell from environment or fallback to /bin/bash @@ -929,6 +931,21 @@ func UpdateTheme(themeName string) error { }) } +// UpdateTransparentBackground updates the transparent background setting in the configuration. +func UpdateTransparentBackground(enabled bool) error { + if cfg == nil { + return fmt.Errorf("config not loaded") + } + + // Update the in-memory config + cfg.TUI.TransparentBackground = enabled + + // Update the file config + return updateCfgFile(func(config *Config) { + config.TUI.TransparentBackground = enabled + }) +} + // Tries to load Github token from all possible locations func LoadGitHubToken() (string, error) { // First check environment variable diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 0dc227a80..bdccefd22 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -77,11 +77,18 @@ func getHelpWidget() string { t := theme.CurrentTheme() helpText := "ctrl+? help" - return styles.Padded(). - Background(t.TextMuted()). - Foreground(t.BackgroundDarker()). - Bold(true). - Render(helpText) + helpStyle := styles.Padded().Bold(true) + + // In transparency mode, remove background + if theme.IsTransparentBackground() { + helpStyle = helpStyle.Foreground(t.TextMuted()) + } else { + helpStyle = helpStyle. + Background(t.TextMuted()). + Foreground(t.BackgroundDarker()) + } + + return helpStyle.Render(helpText) } func formatTokensAndCost(tokens, contextWindow int64, cost float64) string { @@ -128,35 +135,63 @@ func (m statusCmp) View() string { if m.session.ID != "" { totalTokens := m.session.PromptTokens + m.session.CompletionTokens tokens := formatTokensAndCost(totalTokens, model.ContextWindow, m.session.Cost) - tokensStyle := styles.Padded(). - Background(t.Text()). - Foreground(t.BackgroundSecondary()) + tokensStyle := styles.Padded() + + // In transparency mode, remove background + if theme.IsTransparentBackground() { + tokensStyle = tokensStyle.Foreground(t.Text()) + } else { + tokensStyle = tokensStyle. + Background(t.Text()). + Foreground(t.BackgroundSecondary()) + } + percentage := (float64(totalTokens) / float64(model.ContextWindow)) * 100 if percentage > 80 { + // Even in transparency mode, show warning background for high token usage tokensStyle = tokensStyle.Background(t.Warning()) } tokenInfoWidth = lipgloss.Width(tokens) + 2 status += tokensStyle.Render(tokens) } - diagnostics := styles.Padded(). - Background(t.BackgroundDarker()). - Render(m.projectDiagnostics()) + diagnosticsStyle := styles.Padded() + + // In transparency mode, remove background + if theme.IsTransparentBackground() { + diagnosticsStyle = diagnosticsStyle.Foreground(t.Text()) + } else { + diagnosticsStyle = diagnosticsStyle.Background(t.BackgroundDarker()) + } + + diagnostics := diagnosticsStyle.Render(m.projectDiagnostics()) availableWidht := max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokenInfoWidth) if m.info.Msg != "" { - infoStyle := styles.Padded(). - Foreground(t.Background()). - Width(availableWidht) - - switch m.info.Type { - case util.InfoTypeInfo: - infoStyle = infoStyle.Background(t.Info()) - case util.InfoTypeWarn: - infoStyle = infoStyle.Background(t.Warning()) - case util.InfoTypeError: - infoStyle = infoStyle.Background(t.Error()) + infoStyle := styles.Padded().Width(availableWidht) + + if theme.IsTransparentBackground() { + // In transparent mode, use colored foreground on transparent background + switch m.info.Type { + case util.InfoTypeInfo: + infoStyle = infoStyle.Foreground(t.Info()) + case util.InfoTypeWarn: + infoStyle = infoStyle.Foreground(t.Warning()) + case util.InfoTypeError: + infoStyle = infoStyle.Foreground(t.Error()) + } + } else { + // In non-transparent mode, use colored background with contrasting text + infoStyle = infoStyle.Foreground(t.Background()) + switch m.info.Type { + case util.InfoTypeInfo: + infoStyle = infoStyle.Background(t.Info()) + case util.InfoTypeWarn: + infoStyle = infoStyle.Background(t.Warning()) + case util.InfoTypeError: + infoStyle = infoStyle.Background(t.Error()) + } } infoWidth := availableWidht - 10 @@ -167,11 +202,18 @@ func (m statusCmp) View() string { } status += infoStyle.Render(msg) } else { - status += styles.Padded(). - Foreground(t.Text()). - Background(t.BackgroundSecondary()). - Width(availableWidht). - Render("") + emptyStyle := styles.Padded().Width(availableWidht) + + // In transparency mode, remove background + if theme.IsTransparentBackground() { + emptyStyle = emptyStyle.Foreground(t.Text()) + } else { + emptyStyle = emptyStyle. + Foreground(t.Text()). + Background(t.BackgroundSecondary()) + } + + status += emptyStyle.Render("") } status += diagnostics @@ -193,10 +235,14 @@ func (m *statusCmp) projectDiagnostics() string { // If any server is initializing, show that status if initializing { - return lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Warning()). - Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon)) + initStyle := lipgloss.NewStyle().Foreground(t.Warning()) + + // In transparency mode, remove background + if !theme.IsTransparentBackground() { + initStyle = initStyle.Background(t.BackgroundDarker()) + } + + return initStyle.Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon)) } errorDiagnostics := []protocol.Diagnostic{} @@ -227,31 +273,35 @@ func (m *statusCmp) projectDiagnostics() string { diagnostics := []string{} if len(errorDiagnostics) > 0 { - errStr := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Error()). - Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics))) + errStyle := lipgloss.NewStyle().Foreground(t.Error()) + if !theme.IsTransparentBackground() { + errStyle = errStyle.Background(t.BackgroundDarker()) + } + errStr := errStyle.Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics))) diagnostics = append(diagnostics, errStr) } if len(warnDiagnostics) > 0 { - warnStr := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Warning()). - Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics))) + warnStyle := lipgloss.NewStyle().Foreground(t.Warning()) + if !theme.IsTransparentBackground() { + warnStyle = warnStyle.Background(t.BackgroundDarker()) + } + warnStr := warnStyle.Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics))) diagnostics = append(diagnostics, warnStr) } if len(hintDiagnostics) > 0 { - hintStr := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Text()). - Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics))) + hintStyle := lipgloss.NewStyle().Foreground(t.Text()) + if !theme.IsTransparentBackground() { + hintStyle = hintStyle.Background(t.BackgroundDarker()) + } + hintStr := hintStyle.Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics))) diagnostics = append(diagnostics, hintStr) } if len(infoDiagnostics) > 0 { - infoStr := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). - Foreground(t.Info()). - Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics))) + infoStyle := lipgloss.NewStyle().Foreground(t.Info()) + if !theme.IsTransparentBackground() { + infoStyle = infoStyle.Background(t.BackgroundDarker()) + } + infoStr := infoStyle.Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics))) diagnostics = append(diagnostics, infoStr) } @@ -277,10 +327,18 @@ func (m statusCmp) model() string { } model := models.SupportedModels[coder.Model] - return styles.Padded(). - Background(t.Secondary()). - Foreground(t.Background()). - Render(model.Name) + modelStyle := styles.Padded() + + // In transparency mode, remove background + if theme.IsTransparentBackground() { + modelStyle = modelStyle.Foreground(t.Secondary()) + } else { + modelStyle = modelStyle. + Background(t.Secondary()). + Foreground(t.Background()) + } + + return modelStyle.Render(model.Name) } func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp { diff --git a/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go index 25069b8a6..bea5f89cf 100644 --- a/internal/tui/components/dialog/commands.go +++ b/internal/tui/components/dialog/commands.go @@ -29,13 +29,23 @@ func (ci Command) Render(selected bool, width int) string { Background(t.Background()) if selected { - itemStyle = itemStyle. - Background(t.Primary()). - Foreground(t.Background()). - Bold(true) - descStyle = descStyle. - Background(t.Primary()). - Foreground(t.Background()) + if theme.IsTransparentBackground() { + itemStyle = itemStyle. + Background(t.Background()). + Foreground(t.Primary()). + Bold(true) + descStyle = descStyle. + Background(t.Background()). + Foreground(t.Primary()) + } else { + itemStyle = itemStyle. + Background(t.Primary()). + Foreground(t.Background()). + Bold(true) + descStyle = descStyle. + Background(t.Primary()). + Foreground(t.Background()) + } } title := itemStyle.Padding(0, 1).Render(ci.Title) diff --git a/internal/tui/components/dialog/complete.go b/internal/tui/components/dialog/complete.go index 1ce66e12a..d1d55767a 100644 --- a/internal/tui/components/dialog/complete.go +++ b/internal/tui/components/dialog/complete.go @@ -34,10 +34,17 @@ func (ci *CompletionItem) Render(selected bool, width int) string { Padding(0, 1) if selected { - itemStyle = itemStyle. - Background(t.Background()). - Foreground(t.Primary()). - Bold(true) + if theme.IsTransparentBackground() { + itemStyle = itemStyle. + Background(t.Background()). + Foreground(t.Primary()). + Bold(true) + } else { + itemStyle = itemStyle. + Background(t.Primary()). + Foreground(t.Background()). + Bold(true) + } } title := itemStyle.Render( diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go index 3b9a0dc6c..0298e791d 100644 --- a/internal/tui/components/dialog/filepicker.go +++ b/internal/tui/components/dialog/filepicker.go @@ -290,10 +290,17 @@ func (f *filepickerCmp) View() string { itemStyle := styles.BaseStyle().Width(adjustedWidth) if i == f.cursor { - itemStyle = itemStyle. - Background(t.Primary()). - Foreground(t.Background()). - Bold(true) + if theme.IsTransparentBackground() { + itemStyle = itemStyle. + Background(t.Background()). + Foreground(t.Primary()). + Bold(true) + } else { + itemStyle = itemStyle. + Background(t.Primary()). + Foreground(t.Background()). + Bold(true) + } } filename := file.Name() diff --git a/internal/tui/components/dialog/models.go b/internal/tui/components/dialog/models.go index 77c2a02ac..729b2756e 100644 --- a/internal/tui/components/dialog/models.go +++ b/internal/tui/components/dialog/models.go @@ -205,8 +205,17 @@ func (m *modelDialogCmp) View() string { for i := m.scrollOffset; i < endIdx; i++ { itemStyle := baseStyle.Width(maxDialogWidth) if i == m.selectedIdx { - itemStyle = itemStyle.Background(t.Primary()). - Foreground(t.Background()).Bold(true) + if theme.IsTransparentBackground() { + itemStyle = itemStyle. + Background(t.Background()). + Foreground(t.Primary()). + Bold(true) + } else { + itemStyle = itemStyle. + Background(t.Primary()). + Foreground(t.Background()). + Bold(true) + } } modelItems = append(modelItems, itemStyle.Render(m.models[i].Name)) } diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 6c135098a..6fe1d98dc 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -160,17 +160,29 @@ func (p *permissionDialogCmp) renderButtons() string { // Style the selected button switch p.selectedOption { case 0: - allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background()) + if theme.IsTransparentBackground() { + allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) + } else { + allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background()) + } allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) case 1: allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) - allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background()) + if theme.IsTransparentBackground() { + allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) + } else { + allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background()) + } denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) case 2: allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) - denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background()) + if theme.IsTransparentBackground() { + denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) + } else { + denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background()) + } } allowButton := allowStyle.Padding(0, 1).Render("Allow (a)") diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index a29fa7131..e63d6222a 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -153,10 +153,17 @@ func (s *sessionDialogCmp) View() string { itemStyle := baseStyle.Width(maxWidth) if i == s.selectedIdx { - itemStyle = itemStyle. - Background(t.Primary()). - Foreground(t.Background()). - Bold(true) + if theme.IsTransparentBackground() { + itemStyle = itemStyle. + Background(t.Background()). + Foreground(t.Primary()). + Bold(true) + } else { + itemStyle = itemStyle. + Background(t.Primary()). + Foreground(t.Background()). + Bold(true) + } } sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title)) diff --git a/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go index d35d3e2b6..bf23d72e9 100644 --- a/internal/tui/components/dialog/theme.go +++ b/internal/tui/components/dialog/theme.go @@ -122,6 +122,25 @@ func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return t, nil } +// applySelectionStyle applies consistent selection styling for both transparent and non-transparent themes +func applySelectionStyle(baseStyle lipgloss.Style, isSelected bool, currentTheme theme.Theme) lipgloss.Style { + if !isSelected { + return baseStyle + } + + if theme.IsTransparentBackground() { + return baseStyle. + Background(currentTheme.Background()). + Foreground(currentTheme.Primary()). + Bold(true) + } + + return baseStyle. + Background(currentTheme.Primary()). + Foreground(currentTheme.Background()). + Bold(true) +} + func (t *themeDialogCmp) View() string { currentTheme := theme.CurrentTheme() baseStyle := styles.BaseStyle() @@ -148,15 +167,7 @@ func (t *themeDialogCmp) View() string { // Build the theme list themeItems := make([]string, 0, len(t.themes)) for i, themeName := range t.themes { - itemStyle := baseStyle.Width(maxWidth) - - if i == t.selectedIdx { - itemStyle = itemStyle. - Background(currentTheme.Primary()). - Foreground(currentTheme.Background()). - Bold(true) - } - + itemStyle := applySelectionStyle(baseStyle.Width(maxWidth), i == t.selectedIdx, currentTheme) themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName)) } @@ -195,4 +206,3 @@ func NewThemeDialogCmp() ThemeDialog { currentTheme: "", } } - diff --git a/internal/tui/styles/background.go b/internal/tui/styles/background.go index 2fbb34efb..8eb04354e 100644 --- a/internal/tui/styles/background.go +++ b/internal/tui/styles/background.go @@ -26,7 +26,103 @@ func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) { // ForceReplaceBackgroundWithLipgloss replaces any ANSI background color codes // in `input` with a single 24‑bit background (48;2;R;G;B). +// If the background color is empty (transparent), it removes all background colors. func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string { + // Check if this is a transparent background (empty AdaptiveColor) + if adaptiveColor, ok := newBgColor.(lipgloss.AdaptiveColor); ok { + if adaptiveColor.Light == "" && adaptiveColor.Dark == "" { + // For transparent backgrounds, just remove all background colors + return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string { + const ( + escPrefixLen = 2 // "\x1b[" + escSuffixLen = 1 // "m" + ) + + raw := seq + start := escPrefixLen + end := len(raw) - escSuffixLen + + var sb strings.Builder + // reserve enough space for non-background codes + sb.Grow(end - start) + + // scan from start..end, token by token + for i := start; i < end; { + // find the next ';' or end + j := i + for j < end && raw[j] != ';' { + j++ + } + token := raw[i:j] + + // fast‑path: skip "48;5;N" or "48;2;R;G;B" + if len(token) == 2 && token[0] == '4' && token[1] == '8' { + k := j + 1 + if k < end { + // find next token + l := k + for l < end && raw[l] != ';' { + l++ + } + next := raw[k:l] + if next == "5" { + // skip "48;5;N" + m := l + 1 + for m < end && raw[m] != ';' { + m++ + } + i = m + 1 + continue + } else if next == "2" { + // skip "48;2;R;G;B" + m := l + 1 + for count := 0; count < 3 && m < end; count++ { + for m < end && raw[m] != ';' { + m++ + } + m++ + } + i = m + continue + } + } + } + + // decide whether to keep this token + // manually parse ASCII digits to int + isNum := true + val := 0 + for p := i; p < j; p++ { + c := raw[p] + if c < '0' || c > '9' { + isNum = false + break + } + val = val*10 + int(c-'0') + } + keep := !isNum || + ((val < 40 || val > 47) && (val < 100 || val > 107) && val != 49) + + if keep { + if sb.Len() > 0 { + sb.WriteByte(';') + } + sb.WriteString(token) + } + // advance past this token (and the semicolon) + i = j + 1 + } + + // For transparent backgrounds, don't add any background + if sb.Len() == 0 { + return "" // Remove the entire escape sequence if it only had background colors + } + return "\x1b[" + sb.String() + "m" + }) + } + } + + // For non-transparent backgrounds, proceed with the original logic // Precompute our new-bg sequence once r, g, b := getColorRGB(newBgColor) newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b) diff --git a/internal/tui/styles/styles.go b/internal/tui/styles/styles.go index 7094b5373..73b6728d2 100644 --- a/internal/tui/styles/styles.go +++ b/internal/tui/styles/styles.go @@ -6,7 +6,7 @@ import ( ) var ( - ImageBakcground = "#212121" + ImageBackground = "#212121" ) // Style generation functions that use the current theme diff --git a/internal/tui/theme/manager.go b/internal/tui/theme/manager.go index a81ba45c1..3ffee7bec 100644 --- a/internal/tui/theme/manager.go +++ b/internal/tui/theme/manager.go @@ -63,6 +63,7 @@ func SetTheme(name string) error { // CurrentTheme returns the currently active theme. // If no theme is set, it returns nil. +// If transparency is enabled in config, it returns a transparent version of the theme. func CurrentTheme() Theme { globalManager.mu.RLock() defer globalManager.mu.RUnlock() @@ -71,7 +72,15 @@ func CurrentTheme() Theme { return nil } - return globalManager.themes[globalManager.currentName] + theme := globalManager.themes[globalManager.currentName] + + // Check if transparency is enabled in config + cfg := config.Get() + if cfg != nil && cfg.TUI.TransparentBackground { + return NewTransparentTheme(theme) + } + + return theme } // CurrentThemeName returns the name of the currently active theme. @@ -116,3 +125,15 @@ func updateConfigTheme(themeName string) error { // Use the config package to update the theme return config.UpdateTheme(themeName) } + +// SetTransparentBackground updates the transparent background setting. +func SetTransparentBackground(enabled bool) error { + // Use the config package to update the transparency setting + return config.UpdateTransparentBackground(enabled) +} + +// IsTransparentBackground returns whether transparent background is enabled. +func IsTransparentBackground() bool { + cfg := config.Get() + return cfg != nil && cfg.TUI.TransparentBackground +} diff --git a/internal/tui/theme/theme.go b/internal/tui/theme/theme.go index 4ee14a07f..f8a39c4d5 100644 --- a/internal/tui/theme/theme.go +++ b/internal/tui/theme/theme.go @@ -76,6 +76,33 @@ type Theme interface { SyntaxPunctuation() lipgloss.AdaptiveColor } +// TransparentTheme wraps a Theme to provide transparent background variants +type TransparentTheme struct { + Theme +} + +// NewTransparentTheme creates a new transparent theme wrapper +func NewTransparentTheme(theme Theme) *TransparentTheme { + return &TransparentTheme{Theme: theme} +} + +// Override background methods to return transparent colors +func (t *TransparentTheme) Background() lipgloss.AdaptiveColor { + return lipgloss.AdaptiveColor{Light: "", Dark: ""} +} + +func (t *TransparentTheme) BackgroundSecondary() lipgloss.AdaptiveColor { + return lipgloss.AdaptiveColor{Light: "", Dark: ""} +} + +func (t *TransparentTheme) BackgroundDarker() lipgloss.AdaptiveColor { + return lipgloss.AdaptiveColor{Light: "", Dark: ""} +} + +func (t *TransparentTheme) DiffContextBg() lipgloss.AdaptiveColor { + return lipgloss.AdaptiveColor{Light: "", Dark: ""} +} + // BaseTheme provides a default implementation of the Theme interface // that can be embedded in concrete theme implementations. type BaseTheme struct { From 2c6205889fe5e64ebe1ecffa3291389298dc0a14 Mon Sep 17 00:00:00 2001 From: Marco Caceffo Date: Thu, 3 Jul 2025 17:46:03 +0200 Subject: [PATCH 2/4] updated readme --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index eee06acd9..b0895c9a5 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,20 @@ You can enable or disable this feature in your configuration file: } ``` +### Theme Configuration + +OpenCode supports various themes with an optional transparent background mode. The transparent background feature removes all background colors from the interface, making it perfect for terminal transparency or minimal setups. + +To enable transparent background mode, add the following to your configuration file: + +```json +{ + "tui": { + "theme": "opencode", + "transparentBackground": true + } +} +``` ### Environment Variables You can configure OpenCode using environment variables: @@ -173,6 +187,10 @@ This is useful if you want to use a different shell than your default system she "maxTokens": 80 } }, + "tui": { + "theme": "opencode", + "transparentBackground": false + }, "shell": { "path": "/bin/bash", "args": ["-l"] From 385ce8db37c50f111a4d1e29554d26d25657fa3a Mon Sep 17 00:00:00 2001 From: Marco Caceffo Date: Thu, 3 Jul 2025 17:49:27 +0200 Subject: [PATCH 3/4] fixed quit dialog --- internal/tui/components/dialog/quit.go | 36 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index f755fa272..23e554de3 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -90,11 +90,21 @@ func (q *quitDialogCmp) View() string { spacerStyle := baseStyle.Background(t.Background()) if q.selectedNo { - noStyle = noStyle.Background(t.Primary()).Foreground(t.Background()) - yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()) + if theme.IsTransparentBackground() { + noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()).Bold(true) + yesStyle = yesStyle.Background(t.Background()).Foreground(t.Text()) + } else { + noStyle = noStyle.Background(t.Primary()).Foreground(t.Background()) + yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()) + } } else { - yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background()) - noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()) + if theme.IsTransparentBackground() { + yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()).Bold(true) + noStyle = noStyle.Background(t.Background()).Foreground(t.Text()) + } else { + yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background()) + noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()) + } } yesButton := yesStyle.Padding(0, 1).Render("Yes") @@ -108,14 +118,16 @@ func (q *quitDialogCmp) View() string { buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons } - content := baseStyle.Render( - lipgloss.JoinVertical( - lipgloss.Center, - question, - "", - buttons, - ), - ) + content := baseStyle. + Background(t.Background()). + Render( + lipgloss.JoinVertical( + lipgloss.Center, + question, + "", + buttons, + ), + ) return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). From f0f46dbcd5101930568416326fbfb15e8adf037a Mon Sep 17 00:00:00 2001 From: Marco Caceffo Date: Thu, 3 Jul 2025 17:54:57 +0200 Subject: [PATCH 4/4] removed shadow in transparent theme --- internal/tui/layout/overlay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go index 3a14dbc5e..b31ede8cb 100644 --- a/internal/tui/layout/overlay.go +++ b/internal/tui/layout/overlay.go @@ -43,7 +43,7 @@ func PlaceOverlay( bgHeight := len(bgLines) fgHeight := len(fgLines) - if shadow { + if shadow && !theme.IsTransparentBackground() { t := theme.CurrentTheme() baseStyle := styles.BaseStyle()