From 82de14371d45bf672686ca5d340c4567a56c2364 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdottv@users.noreply.github.com> Date: Thu, 1 May 2025 05:49:26 -0500 Subject: [PATCH] feat: themes (#113) * feat: themes * feat: flexoki theme * feat: onedark theme * feat: monokai pro theme * feat: opencode theme (default) * feat: dracula theme * feat: tokyonight theme * feat: tron theme * some small fixes --------- Co-authored-by: Kujtim Hoxha --- cmd/schema/main.go | 23 + internal/app/app.go | 19 + internal/config/config.go | 68 ++ internal/diff/diff.go | 532 ++++----- internal/tui/components/chat/chat.go | 72 +- internal/tui/components/chat/editor.go | 62 +- internal/tui/components/chat/list.go | 76 +- internal/tui/components/chat/message.go | 179 +-- internal/tui/components/chat/sidebar.go | 72 +- internal/tui/components/core/status.go | 92 +- internal/tui/components/dialog/commands.go | 38 +- internal/tui/components/dialog/help.go | 48 +- internal/tui/components/dialog/init.go | 50 +- internal/tui/components/dialog/models.go | 29 +- internal/tui/components/dialog/permission.go | 122 +- internal/tui/components/dialog/quit.go | 26 +- internal/tui/components/dialog/session.go | 32 +- internal/tui/components/dialog/theme.go | 198 ++++ internal/tui/components/logs/details.go | 28 +- internal/tui/components/logs/table.go | 11 +- internal/tui/layout/container.go | 37 +- internal/tui/layout/overlay.go | 10 +- internal/tui/layout/split.go | 22 +- internal/tui/page/chat.go | 1 - internal/tui/page/logs.go | 6 +- internal/tui/styles/huh.go | 46 - internal/tui/styles/markdown.go | 1101 ++++-------------- internal/tui/styles/styles.go | 315 +++-- internal/tui/theme/catppuccin.go | 248 ++++ internal/tui/theme/dracula.go | 274 +++++ internal/tui/theme/flexoki.go | 282 +++++ internal/tui/theme/gruvbox.go | 302 +++++ internal/tui/theme/manager.go | 118 ++ internal/tui/theme/monokai.go | 273 +++++ internal/tui/theme/onedark.go | 274 +++++ internal/tui/theme/opencode.go | 277 +++++ internal/tui/theme/theme.go | 208 ++++ internal/tui/theme/theme_test.go | 89 ++ internal/tui/theme/tokyonight.go | 274 +++++ internal/tui/theme/tron.go | 276 +++++ internal/tui/tui.go | 61 +- opencode-schema.json | 247 ++-- 42 files changed, 4595 insertions(+), 1923 deletions(-) create mode 100644 internal/tui/components/dialog/theme.go delete mode 100644 internal/tui/styles/huh.go create mode 100644 internal/tui/theme/catppuccin.go create mode 100644 internal/tui/theme/dracula.go create mode 100644 internal/tui/theme/flexoki.go create mode 100644 internal/tui/theme/gruvbox.go create mode 100644 internal/tui/theme/manager.go create mode 100644 internal/tui/theme/monokai.go create mode 100644 internal/tui/theme/onedark.go create mode 100644 internal/tui/theme/opencode.go create mode 100644 internal/tui/theme/theme.go create mode 100644 internal/tui/theme/theme_test.go create mode 100644 internal/tui/theme/tokyonight.go create mode 100644 internal/tui/theme/tron.go diff --git a/cmd/schema/main.go b/cmd/schema/main.go index cd550d3fe7bdf02e31afae4bc1a6099a1b0884af..adc2b462624a7f39ee6d18e0f0d0463d85582a51 100644 --- a/cmd/schema/main.go +++ b/cmd/schema/main.go @@ -98,6 +98,29 @@ func generateSchema() map[string]any { }, } + schema["properties"].(map[string]any)["tui"] = map[string]any{ + "type": "object", + "description": "Terminal User Interface configuration", + "properties": map[string]any{ + "theme": map[string]any{ + "type": "string", + "description": "TUI theme name", + "default": "opencode", + "enum": []string{ + "opencode", + "catppuccin", + "dracula", + "flexoki", + "gruvbox", + "monokai", + "onedark", + "tokyonight", + "tron", + }, + }, + }, + } + // Add MCP servers schema["properties"].(map[string]any)["mcpServers"] = map[string]any{ "type": "object", diff --git a/internal/app/app.go b/internal/app/app.go index 5438633d143ebb8ea42b794fd17743827b7f92a9..db2ce7da736791520160f7514a7b0631a2738b8f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -16,6 +16,7 @@ import ( "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/session" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type App struct { @@ -49,6 +50,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) { LSPClients: make(map[string]*lsp.Client), } + // Initialize theme based on configuration + app.initTheme() + // Initialize LSP clients in the background go app.initLSPClients(ctx) @@ -73,6 +77,21 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) { return app, nil } +// initTheme sets the application theme based on the configuration +func (app *App) initTheme() { + cfg := config.Get() + if cfg == nil || cfg.TUI.Theme == "" { + return // Use default theme + } + + // Try to set the theme from config + err := theme.SetTheme(cfg.TUI.Theme) + if err != nil { + logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err) + } else { + logging.Debug("Set theme from config", "theme", cfg.TUI.Theme) + } +} // Shutdown performs a clean shutdown of the application func (app *App) Shutdown() { diff --git a/internal/config/config.go b/internal/config/config.go index 482e71c8d147b6ef70edc65469c401cc7288b00d..a2aca4eecbc5dcde47c5de9a4ccc205674f49e19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,9 +2,11 @@ package config import ( + "encoding/json" "fmt" "log/slog" "os" + "path/filepath" "strings" "github.com/opencode-ai/opencode/internal/llm/models" @@ -65,6 +67,11 @@ type LSPConfig struct { Options any `json:"options"` } +// TUIConfig defines the configuration for the Terminal User Interface. +type TUIConfig struct { + Theme string `json:"theme,omitempty"` +} + // Config is the main configuration structure for the application. type Config struct { Data Data `json:"data"` @@ -76,6 +83,7 @@ type Config struct { Debug bool `json:"debug,omitempty"` DebugLSP bool `json:"debugLSP,omitempty"` ContextPaths []string `json:"contextPaths,omitempty"` + TUI TUIConfig `json:"tui"` } // Application constants @@ -203,6 +211,7 @@ func configureViper() { func setDefaults(debug bool) { viper.SetDefault("data.directory", defaultDataDirectory) viper.SetDefault("contextPaths", defaultContextPaths) + viper.SetDefault("tui.theme", "opencode") if debug { viper.SetDefault("debug", true) @@ -714,3 +723,62 @@ func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error { return nil } + +// UpdateTheme updates the theme in the configuration and writes it to the config file. +func UpdateTheme(themeName string) error { + if cfg == nil { + return fmt.Errorf("config not loaded") + } + + // Update the in-memory config + cfg.TUI.Theme = themeName + + // Get the config file path + configFile := viper.ConfigFileUsed() + var configData []byte + if configFile == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName)) + logging.Info("config file not found, creating new one", "path", configFile) + configData = []byte(`{}`) + } else { + // Read the existing config file + data, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + configData = data + } + + // Parse the JSON + var configMap map[string]interface{} + if err := json.Unmarshal(configData, &configMap); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Update just the theme value + tuiConfig, ok := configMap["tui"].(map[string]interface{}) + if !ok { + // TUI config doesn't exist yet, create it + configMap["tui"] = map[string]interface{}{"theme": themeName} + } else { + // Update existing TUI config + tuiConfig["theme"] = themeName + configMap["tui"] = tuiConfig + } + + // Write the updated config back to file + updatedData, err := json.MarshalIndent(configMap, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configFile, updatedData, 0o644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} diff --git a/internal/diff/diff.go b/internal/diff/diff.go index a2edb7e740c420921d06b3e70fa31ae7144dff8c..8f5e669d3c2c6741ea6facaab3f447283ed5a5ab 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -16,6 +16,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/opencode-ai/opencode/internal/config" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -68,143 +69,6 @@ type linePair struct { right *DiffLine } -// ------------------------------------------------------------------------- -// Style Configuration -// ------------------------------------------------------------------------- - -// StyleConfig defines styling for diff rendering -type StyleConfig struct { - ShowHeader bool - ShowHunkHeader bool - FileNameFg lipgloss.Color - // Background colors - RemovedLineBg lipgloss.Color - AddedLineBg lipgloss.Color - ContextLineBg lipgloss.Color - HunkLineBg lipgloss.Color - RemovedLineNumberBg lipgloss.Color - AddedLineNamerBg lipgloss.Color - - // Foreground colors - HunkLineFg lipgloss.Color - RemovedFg lipgloss.Color - AddedFg lipgloss.Color - LineNumberFg lipgloss.Color - RemovedHighlightFg lipgloss.Color - AddedHighlightFg lipgloss.Color - - // Highlight settings - HighlightStyle string - RemovedHighlightBg lipgloss.Color - AddedHighlightBg lipgloss.Color -} - -// StyleOption is a function that modifies a StyleConfig -type StyleOption func(*StyleConfig) - -// NewStyleConfig creates a StyleConfig with default values -func NewStyleConfig(opts ...StyleOption) StyleConfig { - // Default color scheme - config := StyleConfig{ - ShowHeader: true, - ShowHunkHeader: true, - FileNameFg: lipgloss.Color("#a0a0a0"), - RemovedLineBg: lipgloss.Color("#3A3030"), - AddedLineBg: lipgloss.Color("#303A30"), - ContextLineBg: lipgloss.Color("#212121"), - HunkLineBg: lipgloss.Color("#212121"), - HunkLineFg: lipgloss.Color("#a0a0a0"), - RemovedFg: lipgloss.Color("#7C4444"), - AddedFg: lipgloss.Color("#478247"), - LineNumberFg: lipgloss.Color("#888888"), - HighlightStyle: "dracula", - RemovedHighlightBg: lipgloss.Color("#612726"), - AddedHighlightBg: lipgloss.Color("#256125"), - RemovedLineNumberBg: lipgloss.Color("#332929"), - AddedLineNamerBg: lipgloss.Color("#293229"), - RemovedHighlightFg: lipgloss.Color("#FADADD"), - AddedHighlightFg: lipgloss.Color("#DAFADA"), - } - - // Apply all provided options - for _, opt := range opts { - opt(&config) - } - - return config -} - -// Style option functions -func WithFileNameFg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.FileNameFg = color } -} - -func WithRemovedLineBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.RemovedLineBg = color } -} - -func WithAddedLineBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.AddedLineBg = color } -} - -func WithContextLineBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.ContextLineBg = color } -} - -func WithRemovedFg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.RemovedFg = color } -} - -func WithAddedFg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.AddedFg = color } -} - -func WithLineNumberFg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.LineNumberFg = color } -} - -func WithHighlightStyle(style string) StyleOption { - return func(s *StyleConfig) { s.HighlightStyle = style } -} - -func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption { - return func(s *StyleConfig) { - s.RemovedHighlightBg = bg - s.RemovedHighlightFg = fg - } -} - -func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption { - return func(s *StyleConfig) { - s.AddedHighlightBg = bg - s.AddedHighlightFg = fg - } -} - -func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.RemovedLineNumberBg = color } -} - -func WithAddedLineNumberBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.AddedLineNamerBg = color } -} - -func WithHunkLineBg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.HunkLineBg = color } -} - -func WithHunkLineFg(color lipgloss.Color) StyleOption { - return func(s *StyleConfig) { s.HunkLineFg = color } -} - -func WithShowHeader(show bool) StyleOption { - return func(s *StyleConfig) { s.ShowHeader = show } -} - -func WithShowHunkHeader(show bool) StyleOption { - return func(s *StyleConfig) { s.ShowHunkHeader = show } -} - // ------------------------------------------------------------------------- // Parse Configuration // ------------------------------------------------------------------------- @@ -233,7 +97,6 @@ func WithContextSize(size int) ParseOption { // SideBySideConfig configures the rendering of side-by-side diffs type SideBySideConfig struct { TotalWidth int - Style StyleConfig } // SideBySideOption modifies a SideBySideConfig @@ -243,7 +106,6 @@ type SideBySideOption func(*SideBySideConfig) func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig { config := SideBySideConfig{ TotalWidth: 160, // Default width for side-by-side view - Style: NewStyleConfig(), } for _, opt := range opts { @@ -262,20 +124,6 @@ func WithTotalWidth(width int) SideBySideOption { } } -// WithStyle sets the styling configuration -func WithStyle(style StyleConfig) SideBySideOption { - return func(s *SideBySideConfig) { - s.Style = style - } -} - -// WithStyleOptions applies the specified style options -func WithStyleOptions(opts ...StyleOption) SideBySideOption { - return func(s *SideBySideConfig) { - s.Style = NewStyleConfig(opts...) - } -} - // ------------------------------------------------------------------------- // Diff Parsing // ------------------------------------------------------------------------- @@ -382,7 +230,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) { } // HighlightIntralineChanges updates lines in a hunk to show character-level differences -func HighlightIntralineChanges(h *Hunk, style StyleConfig) { +func HighlightIntralineChanges(h *Hunk) { var updated []DiffLine dmp := diffmatchpatch.New() @@ -476,6 +324,8 @@ func pairLines(lines []DiffLine) []linePair { // SyntaxHighlight applies syntax highlighting to text based on file extension func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error { + t := theme.CurrentTheme() + // Determine the language lexer to use l := lexers.Match(fileName) if l == nil { @@ -491,93 +341,175 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos if f == nil { f = formatters.Fallback } - theme := ` - -` +`, + getColor(t.Background()), // Background + getColor(t.Text()), // Text + getColor(t.Text()), // Other + getColor(t.Error()), // Error + + getColor(t.SyntaxKeyword()), // Keyword + getColor(t.SyntaxKeyword()), // KeywordConstant + getColor(t.SyntaxKeyword()), // KeywordDeclaration + getColor(t.SyntaxKeyword()), // KeywordNamespace + getColor(t.SyntaxKeyword()), // KeywordPseudo + getColor(t.SyntaxKeyword()), // KeywordReserved + getColor(t.SyntaxType()), // KeywordType + + getColor(t.Text()), // Name + getColor(t.SyntaxVariable()), // NameAttribute + getColor(t.SyntaxType()), // NameBuiltin + getColor(t.SyntaxVariable()), // NameBuiltinPseudo + getColor(t.SyntaxType()), // NameClass + getColor(t.SyntaxVariable()), // NameConstant + getColor(t.SyntaxFunction()), // NameDecorator + getColor(t.SyntaxVariable()), // NameEntity + getColor(t.SyntaxType()), // NameException + getColor(t.SyntaxFunction()), // NameFunction + getColor(t.Text()), // NameLabel + getColor(t.SyntaxType()), // NameNamespace + getColor(t.SyntaxVariable()), // NameOther + getColor(t.SyntaxKeyword()), // NameTag + getColor(t.SyntaxVariable()), // NameVariable + getColor(t.SyntaxVariable()), // NameVariableClass + getColor(t.SyntaxVariable()), // NameVariableGlobal + getColor(t.SyntaxVariable()), // NameVariableInstance + + getColor(t.SyntaxString()), // Literal + getColor(t.SyntaxString()), // LiteralDate + getColor(t.SyntaxString()), // LiteralString + getColor(t.SyntaxString()), // LiteralStringBacktick + getColor(t.SyntaxString()), // LiteralStringChar + getColor(t.SyntaxString()), // LiteralStringDoc + getColor(t.SyntaxString()), // LiteralStringDouble + getColor(t.SyntaxString()), // LiteralStringEscape + getColor(t.SyntaxString()), // LiteralStringHeredoc + getColor(t.SyntaxString()), // LiteralStringInterpol + getColor(t.SyntaxString()), // LiteralStringOther + getColor(t.SyntaxString()), // LiteralStringRegex + getColor(t.SyntaxString()), // LiteralStringSingle + getColor(t.SyntaxString()), // LiteralStringSymbol + + getColor(t.SyntaxNumber()), // LiteralNumber + getColor(t.SyntaxNumber()), // LiteralNumberBin + getColor(t.SyntaxNumber()), // LiteralNumberFloat + getColor(t.SyntaxNumber()), // LiteralNumberHex + getColor(t.SyntaxNumber()), // LiteralNumberInteger + getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong + getColor(t.SyntaxNumber()), // LiteralNumberOct + + getColor(t.SyntaxOperator()), // Operator + getColor(t.SyntaxKeyword()), // OperatorWord + getColor(t.SyntaxPunctuation()), // Punctuation + + getColor(t.SyntaxComment()), // Comment + getColor(t.SyntaxComment()), // CommentHashbang + getColor(t.SyntaxComment()), // CommentMultiline + getColor(t.SyntaxComment()), // CommentSingle + getColor(t.SyntaxComment()), // CommentSpecial + getColor(t.SyntaxKeyword()), // CommentPreproc + + getColor(t.Text()), // Generic + getColor(t.Error()), // GenericDeleted + getColor(t.Text()), // GenericEmph + getColor(t.Error()), // GenericError + getColor(t.Text()), // GenericHeading + getColor(t.Success()), // GenericInserted + getColor(t.TextMuted()), // GenericOutput + getColor(t.Text()), // GenericPrompt + getColor(t.Text()), // GenericStrong + getColor(t.Text()), // GenericSubheading + getColor(t.Error()), // GenericTraceback + getColor(t.Text()), // TextWhitespace + ) - r := strings.NewReader(theme) + r := strings.NewReader(syntaxThemeXml) style := chroma.MustNewXMLStyle(r) + // Modify the style to use the provided background s, err := style.Builder().Transform( func(t chroma.StyleEntry) chroma.StyleEntry { @@ -599,6 +531,14 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos return f.Format(w, s, it) } +// getColor returns the appropriate hex color string based on terminal background +func getColor(adaptiveColor lipgloss.AdaptiveColor) string { + if lipgloss.HasDarkBackground() { + return adaptiveColor.Dark + } + return adaptiveColor.Light +} + // highlightLine applies syntax highlighting to a single line func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string { var buf bytes.Buffer @@ -610,11 +550,11 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri } // createStyles generates the lipgloss styles needed for rendering diffs -func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) { - removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg) - addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg) - contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg) - lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg) +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().Foreground(t.DiffLineNumber()) return } @@ -623,9 +563,20 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context // Rendering Functions // ------------------------------------------------------------------------- +func lipglossToHex(color lipgloss.Color) string { + r, g, b, a := color.RGBA() + + // Scale uint32 values (0-65535) to uint8 (0-255). + r8 := uint8(r >> 8) + g8 := uint8(g >> 8) + b8 := uint8(b >> 8) + a8 := uint8(a >> 8) + + return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8) +} + // applyHighlighting applies intra-line highlighting to a piece of text -func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color, -) string { +func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string { // Find all ANSI sequences in the content ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`) ansiMatches := ansiRegex.FindAllStringIndex(content, -1) @@ -663,6 +614,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType, inSelection := false currentPos := 0 + // Get the appropriate color based on terminal background + bgColor := lipgloss.Color(getColor(highlightBg)) + fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background())) + for i := 0; i < len(content); { // Check if we're at an ANSI sequence isAnsi := false @@ -697,12 +652,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType, // Get the current styling currentStyle := ansiSequences[currentPos] - // Apply background highlight + // 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, _ := highlightBg.RGBA() + r, g, b, _ = bgColor.RGBA() sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8)) sb.WriteString(char) - sb.WriteString("\x1b[49m") // Reset only background + // Reset foreground and background + sb.WriteString("\x1b[39m") // Reapply the original ANSI sequence sb.WriteString(currentStyle) @@ -719,22 +678,24 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType, } // renderLeftColumn formats the left side of a side-by-side diff -func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string { +func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string { + t := theme.CurrentTheme() + if dl == nil { - contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg) + contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg()) return contextLineStyle.Width(colWidth).Render("") } - removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles) + removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t) // Determine line style based on line type var marker string var bgStyle lipgloss.Style switch dl.Kind { case LineRemoved: - marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-") + marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-") bgStyle = removedLineStyle - lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg) + lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg()) case LineAdded: marker = "?" bgStyle = contextLineStyle @@ -757,7 +718,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC // Apply intra-line highlighting for removed lines if dl.Kind == LineRemoved && len(dl.Segments) > 0 { - content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg) + content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved()) } // Add a padding space for removed lines @@ -771,28 +732,30 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC ansi.Truncate( lineText, colWidth, - lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."), + lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."), ), ) } // renderRightColumn formats the right side of a side-by-side diff -func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string { +func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string { + t := theme.CurrentTheme() + if dl == nil { - contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg) + contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg()) return contextLineStyle.Width(colWidth).Render("") } - _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles) + _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t) // Determine line style based on line type var marker string var bgStyle lipgloss.Style switch dl.Kind { case LineAdded: - marker = addedLineStyle.Foreground(styles.AddedFg).Render("+") + marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+") bgStyle = addedLineStyle - lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg) + lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg()) case LineRemoved: marker = "?" bgStyle = contextLineStyle @@ -815,7 +778,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style // Apply intra-line highlighting for added lines if dl.Kind == LineAdded && len(dl.Segments) > 0 { - content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg) + content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded()) } // Add a padding space for added lines @@ -829,7 +792,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style ansi.Truncate( lineText, colWidth, - lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."), + lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."), ), ) } @@ -848,7 +811,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str copy(hunkCopy.Lines, h.Lines) // Highlight changes within lines - HighlightIntralineChanges(&hunkCopy, config.Style) + HighlightIntralineChanges(&hunkCopy) // Pair lines for side-by-side display pairs := pairLines(hunkCopy.Lines) @@ -860,8 +823,8 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str rightWidth := config.TotalWidth - colWidth var sb strings.Builder for _, p := range pairs { - leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style) - rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style) + leftStr := renderLeftColumn(fileName, p.left, leftWidth) + rightStr := renderRightColumn(fileName, p.right, rightWidth) sb.WriteString(leftStr + rightStr + "\n") } @@ -876,54 +839,7 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) { } var sb strings.Builder - config := NewSideBySideConfig(opts...) - - if config.Style.ShowHeader { - removeIcon := lipgloss.NewStyle(). - Background(config.Style.RemovedLineBg). - Foreground(config.Style.RemovedFg). - Render("⏹") - addIcon := lipgloss.NewStyle(). - Background(config.Style.AddedLineBg). - Foreground(config.Style.AddedFg). - Render("⏹") - - fileName := lipgloss.NewStyle(). - Background(config.Style.ContextLineBg). - Foreground(config.Style.FileNameFg). - Render(" " + diffResult.OldFile) - sb.WriteString( - lipgloss.NewStyle(). - Background(config.Style.ContextLineBg). - Padding(0, 1, 0, 1). - Foreground(config.Style.FileNameFg). - BorderStyle(lipgloss.NormalBorder()). - BorderTop(true). - BorderBottom(true). - BorderForeground(config.Style.FileNameFg). - BorderBackground(config.Style.ContextLineBg). - Width(config.TotalWidth). - Render( - lipgloss.JoinHorizontal(lipgloss.Top, - removeIcon, - addIcon, - fileName, - ), - ) + "\n", - ) - } - for _, h := range diffResult.Hunks { - // Render hunk header - if config.Style.ShowHunkHeader { - sb.WriteString( - lipgloss.NewStyle(). - Background(config.Style.HunkLineBg). - Foreground(config.Style.HunkLineFg). - Width(config.TotalWidth). - Render(h.Header) + "\n", - ) - } sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...)) } @@ -944,8 +860,8 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in removals = 0 ) - lines := strings.Split(unified, "\n") - for _, line := range lines { + lines := strings.SplitSeq(unified, "\n") + for line := range lines { if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") { additions++ } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index f4c05590327b86c87625d588a49317422263a7f2..ca094ca7c4e096a28010d7ac6d9bb4db341a90cd 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -9,6 +9,7 @@ import ( "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/version" ) @@ -22,12 +23,29 @@ type SessionClearedMsg struct{} type EditorFocusMsg bool +func header(width int) string { + return lipgloss.JoinVertical( + lipgloss.Top, + logo(width), + repo(width), + "", + cwd(width), + ) +} + func lspsConfigured(width int) string { cfg := config.Get() title := "LSP Configuration" title = ansi.Truncate(title, width, "…") - lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + lsps := baseStyle. + Width(width). + Foreground(t.Primary()). + Bold(true). + Render(title) // Get LSP names and sort them for consistent ordering var lspNames []string @@ -39,16 +57,19 @@ func lspsConfigured(width int) string { var lspViews []string for _, name := range lspNames { lsp := cfg.LSP[name] - lspName := styles.BaseStyle.Foreground(styles.Forground).Render( - fmt.Sprintf("• %s", name), - ) + lspName := baseStyle. + Foreground(t.Text()). + Render(fmt.Sprintf("• %s", name)) + cmd := lsp.Command cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…") - lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" (%s)", cmd), - ) + + lspPath := baseStyle. + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" (%s)", cmd)) + lspViews = append(lspViews, - styles.BaseStyle. + baseStyle. Width(width). Render( lipgloss.JoinHorizontal( @@ -59,7 +80,8 @@ func lspsConfigured(width int) string { ), ) } - return styles.BaseStyle. + + return baseStyle. Width(width). Render( lipgloss.JoinVertical( @@ -75,10 +97,14 @@ func lspsConfigured(width int) string { func logo(width int) string { logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode") + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() - version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version) + versionText := baseStyle. + Foreground(t.TextMuted()). + Render(version.Version) - return styles.BaseStyle. + return baseStyle. Bold(true). Width(width). Render( @@ -86,34 +112,28 @@ func logo(width int) string { lipgloss.Left, logo, " ", - version, + versionText, ), ) } func repo(width int) string { repo := "https://github.com/opencode-ai/opencode" - return styles.BaseStyle. - Foreground(styles.ForgroundDim). + t := theme.CurrentTheme() + + return styles.BaseStyle(). + Foreground(t.TextMuted()). Width(width). Render(repo) } func cwd(width int) string { cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory()) - return styles.BaseStyle. - Foreground(styles.ForgroundDim). + t := theme.CurrentTheme() + + return styles.BaseStyle(). + Foreground(t.TextMuted()). Width(width). Render(cwd) } -func header(width int) string { - header := lipgloss.JoinVertical( - lipgloss.Top, - logo(width), - repo(width), - "", - cwd(width), - ) - return header -} diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 88ac3e759d39901f6b24612f77cad2ed78c81ada..3548cbb0b8d7b9ee7f73701dc74fb156f36615ee 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -10,8 +10,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/app" "github.com/opencode-ai/opencode/internal/session" + "github.com/opencode-ai/opencode/internal/tui/components/dialog" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd { func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case dialog.ThemeChangedMsg: + m.textarea = CreateTextArea(&m.textarea) + return m, nil case SessionSelectedMsg: if msg.ID != m.session.ID { m.session = msg @@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *editorCmp) View() string { - style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true) + t := theme.CurrentTheme() + + // Style the prompt with theme colors + style := lipgloss.NewStyle(). + Padding(0, 0, 0, 1). + Bold(true). + Foreground(t.Primary()) return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View()) } @@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding { return bindings } +func CreateTextArea(existing *textarea.Model) textarea.Model { + t := theme.CurrentTheme() + bgColor := t.Background() + textColor := t.Text() + textMutedColor := t.TextMuted() + + ta := textarea.New() + ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor) + ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor) + ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor) + ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor) + ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor) + ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor) + ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor) + ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor) + + ta.Prompt = " " + ta.ShowLineNumbers = false + ta.CharLimit = -1 + + if existing != nil { + ta.SetValue(existing.Value()) + ta.SetWidth(existing.Width()) + ta.SetHeight(existing.Height()) + } + + ta.Focus() + return ta +} + func NewEditorCmp(app *app.App) tea.Model { - ti := textarea.New() - ti.Prompt = " " - ti.ShowLineNumbers = false - ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background) - ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background) - ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background) - ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background) - - ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background) - ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background) - ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background) - ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background) - ti.CharLimit = -1 - ti.Focus() + ta := CreateTextArea(nil) + return &editorCmp{ app: app, - textarea: ti, + textarea: ta, } } + diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index fa7332d5fd382a540e29a4c96a784f1c6bd1c6bd..12f1681fad7d417d00a61c24257fd53f4c2cb38c 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -14,7 +14,9 @@ import ( "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/session" + "github.com/opencode-ai/opencode/internal/tui/components/dialog" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd { func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - + case dialog.ThemeChangedMsg: + m.rerender() + return m, nil case SessionSelectedMsg: if msg.ID != m.session.ID { cmd := m.SetSession(msg) @@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string { func (m *messagesCmp) renderView() { m.uiMessages = make([]uiMessage, 0) pos := 0 + baseStyle := styles.BaseStyle() if m.width == 0 { return @@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() { messages := make([]string, 0) for _, v := range m.uiMessages { messages = append(messages, v.content, - styles.BaseStyle. + baseStyle. Width(m.width). - Render( - "", - ), + Render(""), ) } m.viewport.SetContent( - styles.BaseStyle. + baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() { } func (m *messagesCmp) View() string { + baseStyle := styles.BaseStyle() + if m.rendering { - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -258,14 +263,14 @@ func (m *messagesCmp) View() string { ) } if len(m.messages) == 0 { - content := styles.BaseStyle. + content := baseStyle. Width(m.width). Height(m.height - 1). Render( m.initialScreen(), ) - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -277,7 +282,7 @@ func (m *messagesCmp) View() string { ) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool { func (m *messagesCmp) working() string { text := "" if m.IsAgentWorking() && len(m.messages) > 0 { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + task := "Thinking..." lastMessage := m.messages[len(m.messages)-1] if hasToolsWithoutResponse(m.messages) { @@ -338,42 +346,49 @@ func (m *messagesCmp) working() string { task = "Generating..." } if task != "" { - text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render( - fmt.Sprintf("%s %s ", m.spinner.View(), task), - ) + text += baseStyle. + Width(m.width). + Foreground(t.Primary()). + Bold(true). + Render(fmt.Sprintf("%s %s ", m.spinner.View(), task)) } } return text } func (m *messagesCmp) help() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + text := "" if m.app.CoderAgent.IsBusy() { text += lipgloss.JoinHorizontal( lipgloss.Left, - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), + baseStyle.Foreground(t.Text()).Bold(true).Render("esc"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"), ) } else { text += lipgloss.JoinHorizontal( lipgloss.Left, - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"), ) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render(text) } func (m *messagesCmp) initialScreen() string { - return styles.BaseStyle.Width(m.width).Render( + baseStyle := styles.BaseStyle() + + return baseStyle.Width(m.width).Render( lipgloss.JoinVertical( lipgloss.Top, header(m.width), @@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string { ) } +func (m *messagesCmp) rerender() { + for _, msg := range m.messages { + delete(m.cachedContent, msg.ID) + } + m.renderView() +} + func (m *messagesCmp) SetSize(width, height int) tea.Cmd { if m.width == width && m.height == height { return nil @@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd { m.height = height m.viewport.Width = width m.viewport.Height = height - 2 - for _, msg := range m.messages { - delete(m.cachedContent, msg.ID) - } - m.uiMessages = make([]uiMessage, 0) - m.renderView() + m.rerender() return nil } diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index 53ec7ea3dae4ea4d52c3a1eec387fdce5655849a..d6c87480457282d43ad7746848bc4bb283ce3329 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -6,10 +6,8 @@ import ( "fmt" "path/filepath" "strings" - "sync" "time" - "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/opencode-ai/opencode/internal/config" @@ -19,6 +17,7 @@ import ( "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type uiMessageType int @@ -31,8 +30,6 @@ const ( maxResultHeight = 10 ) -var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false)) - type uiMessage struct { ID string messageType uiMessageType @@ -41,46 +38,37 @@ type uiMessage struct { content string } -type renderCache struct { - mutex sync.Mutex - cache map[string][]uiMessage -} - func toMarkdown(content string, focused bool, width int) string { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(false)), - glamour.WithWordWrap(width), - ) - if focused { - r, _ = glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(width), - ) - } + r := styles.GetMarkdownRenderer(width) rendered, _ := r.Render(content) return rendered } func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string { - style := styles.BaseStyle. + t := theme.CurrentTheme() + + style := styles.BaseStyle(). Width(width - 1). BorderLeft(true). - Foreground(styles.ForgroundDim). - BorderForeground(styles.PrimaryColor). + Foreground(t.TextMuted()). + BorderForeground(t.Primary()). BorderStyle(lipgloss.ThickBorder()) + if isUser { - style = style. - BorderForeground(styles.Blue) + style = style.BorderForeground(t.Secondary()) } + + // Apply markdown formatting and handle background color parts := []string{ - styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background), + styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()), } - // remove newline at the end + // Remove newline at the end parts[0] = strings.TrimSuffix(parts[0], "\n") if len(info) > 0 { parts = append(parts, info...) } + rendered := style.Render( lipgloss.JoinVertical( lipgloss.Left, @@ -121,26 +109,37 @@ func renderAssistantMessage( finishData := msg.FinishPart() info := []string{} + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + // Add finish info if available if finished { switch finishData.Reason { case message.FinishReasonEndTurn: - took := formatTimeDifference(msg.CreatedAt, finishData.Time) - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took), - )) + took := formatTimestampDiff(msg.CreatedAt, finishData.Time) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)), + ) case message.FinishReasonCanceled: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")), + ) case message.FinishReasonError: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")), + ) case message.FinishReasonPermissionDenied: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")), + ) } } if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) { @@ -414,32 +413,36 @@ func truncateHeight(content string, height int) string { } func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if response.IsError { errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " ")) errContent = ansi.Truncate(errContent, width-1, "...") - return styles.BaseStyle. + return baseStyle. Width(width). - Foreground(styles.Error). + Foreground(t.Error()). Render(errContent) } + resultContent := truncateHeight(response.Content, maxResultHeight) switch toolCall.Name { case agent.AgentToolName: return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, false, width), - styles.Background, + t.Background(), ) case tools.BashToolName: resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.EditToolName: metadata := tools.EditResponseMetadata{} json.Unmarshal([]byte(response.Metadata), &metadata) truncDiff := truncateHeight(metadata.Diff, maxResultHeight) - formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle)) + formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width)) return formattedDiff case tools.FetchToolName: var params tools.FetchParams @@ -454,16 +457,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.GlobToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.GrepToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.LSToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.SourcegraphToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.ViewToolName: metadata := tools.ViewResponseMetadata{} json.Unmarshal([]byte(response.Metadata), &metadata) @@ -476,7 +479,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight)) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.WriteToolName: params := tools.WriteParams{} @@ -492,13 +495,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight)) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) default: resultContent = fmt.Sprintf("```text\n%s\n```", resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) } } @@ -515,39 +518,31 @@ func renderToolMessage( if nested { width = width - 3 } - style := styles.BaseStyle. + + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + style := baseStyle. Width(width - 1). BorderLeft(true). BorderStyle(lipgloss.ThickBorder()). PaddingLeft(1). - BorderForeground(styles.ForgroundDim) + BorderForeground(t.TextMuted()) response := findToolResponse(toolCall.ID, allMessages) - toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) + toolNameText := baseStyle.Foreground(t.TextMuted()). + Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) if !toolCall.Finished { // Get a brief description of what the tool is doing toolAction := getToolAction(toolCall.Name) - // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ") - // truncatedInput := toolInput - // if len(truncatedInput) > 10 { - // truncatedInput = truncatedInput[len(truncatedInput)-10:] - // } - // - // truncatedInput = styles.BaseStyle. - // Italic(true). - // Width(width - 2 - lipgloss.Width(toolName)). - // Background(styles.BackgroundDim). - // Foreground(styles.ForgroundMid). - // Render(truncatedInput) - - progressText := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundDim). + progressText := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(fmt.Sprintf("%s", toolAction)) - content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText)) + content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText)) toolMsg := uiMessage{ messageType: toolMessageType, position: position, @@ -556,37 +551,39 @@ func renderToolMessage( } return toolMsg } - params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall) + + params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall) responseContent := "" if response != nil { responseContent = renderToolResponse(toolCall, *response, width-2) responseContent = strings.TrimSuffix(responseContent, "\n") } else { - responseContent = styles.BaseStyle. + responseContent = baseStyle. Italic(true). Width(width - 2). - Foreground(styles.ForgroundDim). + Foreground(t.TextMuted()). Render("Waiting for response...") } parts := []string{} if !nested { - params := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundDim). + formattedParams := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(params) - parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params)) + parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams)) } else { - prefix := styles.BaseStyle. - Foreground(styles.ForgroundDim). + prefix := baseStyle. + Foreground(t.TextMuted()). Render(" └ ") - params := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundMid). + formattedParams := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(params) - parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params)) + parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams)) } + if toolCall.Name == agent.AgentToolName { taskMessages, _ := messagesService.List(context.Background(), toolCall.ID) toolCalls := []message.ToolCall{} @@ -622,3 +619,15 @@ func renderToolMessage( } return toolMsg } + +// Helper function to format the time difference between two Unix timestamps +func formatTimestampDiff(start, end int64) string { + diffSeconds := float64(end-start) / 1000.0 // Convert to seconds + if diffSeconds < 1 { + return fmt.Sprintf("%dms", int(diffSeconds*1000)) + } + if diffSeconds < 60 { + return fmt.Sprintf("%.1fs", diffSeconds) + } + return fmt.Sprintf("%.1fm", diffSeconds/60) +} diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index 5baac3cd4691f5363aad1f5107fe170aee24c94f..a66249b368cd28274333395cb79a5b127b3fb8f9 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -14,6 +14,7 @@ import ( "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type sidebarCmp struct { @@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *sidebarCmp) View() string { - return styles.BaseStyle. + baseStyle := styles.BaseStyle() + + return baseStyle. Width(m.width). PaddingLeft(4). PaddingRight(2). @@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string { } func (m *sidebarCmp) sessionSection() string { - sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session") - sessionValue := styles.BaseStyle. - Foreground(styles.Forground). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + sessionKey := baseStyle. + Foreground(t.Primary()). + Bold(true). + Render("Session") + + sessionValue := baseStyle. + Foreground(t.Text()). Width(m.width - lipgloss.Width(sessionKey)). Render(fmt.Sprintf(": %s", m.session.Title)) + return lipgloss.JoinHorizontal( lipgloss.Left, sessionKey, @@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string { } func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + stats := "" if additions > 0 && removals > 0 { - additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions)) - removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals)) - content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals) - stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content) + additionsStr := baseStyle. + Foreground(t.Success()). + PaddingLeft(1). + Render(fmt.Sprintf("+%d", additions)) + + removalsStr := baseStyle. + Foreground(t.Error()). + PaddingLeft(1). + Render(fmt.Sprintf("-%d", removals)) + + content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr) + stats = baseStyle.Width(lipgloss.Width(content)).Render(content) } else if additions > 0 { - additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions))) - stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions) + additionsStr := fmt.Sprintf(" %s", baseStyle. + PaddingLeft(1). + Foreground(t.Success()). + Render(fmt.Sprintf("+%d", additions))) + stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr) } else if removals > 0 { - removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals))) - stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals) + removalsStr := fmt.Sprintf(" %s", baseStyle. + PaddingLeft(1). + Foreground(t.Error()). + Render(fmt.Sprintf("-%d", removals))) + stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr) } - filePathStr := styles.BaseStyle.Render(filePath) - return styles.BaseStyle. + filePathStr := baseStyle.Render(filePath) + + return baseStyle. Width(m.width). Render( lipgloss.JoinHorizontal( @@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri } func (m *sidebarCmp) modifiedFiles() string { - modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:") + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + modifiedFiles := baseStyle. + Width(m.width). + Foreground(t.Primary()). + Bold(true). + Render("Modified Files:") // If no modified files, show a placeholder message if m.modFiles == nil || len(m.modFiles) == 0 { @@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string { if remainingWidth > 0 { message += strings.Repeat(" ", remainingWidth) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( lipgloss.Top, modifiedFiles, - styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message), + baseStyle.Foreground(t.TextMuted()).Render(message), ), ) } @@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string { fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals)) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 9fefdbabd1fb241aea567fdbc820dff32ef3cfe6..7b8a87231cb88eccbe551b1fccbe8d5f97d0e103 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -15,12 +15,13 @@ import ( "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/components/chat" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) type StatusCmp interface { tea.Model - SetHelpMsg(string) + SetHelpWidgetMsg(string) } type statusCmp struct { @@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help") +var helpWidget = "" + +// getHelpWidget returns the help widget with current theme colors +func getHelpWidget(helpText string) string { + t := theme.CurrentTheme() + if helpText == "" { + helpText = "ctrl+? help" + } + + return styles.Padded(). + Background(t.TextMuted()). + Foreground(t.BackgroundDarker()). + Bold(true). + Render(helpText) +} func formatTokensAndCost(tokens int64, cost float64) string { // Format tokens in human-readable format (e.g., 110K, 1.2M) @@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string { } func (m statusCmp) View() string { - status := helpWidget + t := theme.CurrentTheme() + + // Initialize the help widget + status := getHelpWidget("") + if m.session.ID != "" { tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost) - tokensStyle := styles.Padded. - Background(styles.Forground). - Foreground(styles.BackgroundDim). + tokensStyle := styles.Padded(). + Background(t.Text()). + Foreground(t.BackgroundSecondary()). Render(tokens) status += tokensStyle } - diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics()) + diagnostics := styles.Padded(). + Background(t.BackgroundDarker()). + Render(m.projectDiagnostics()) + if m.info.Msg != "" { - infoStyle := styles.Padded. - Foreground(styles.Base). + infoStyle := styles.Padded(). + Foreground(t.Background()). Width(m.availableFooterMsgWidth(diagnostics)) + switch m.info.Type { case util.InfoTypeInfo: - infoStyle = infoStyle.Background(styles.BorderColor) + infoStyle = infoStyle.Background(t.Info()) case util.InfoTypeWarn: - infoStyle = infoStyle.Background(styles.Peach) + infoStyle = infoStyle.Background(t.Warning()) case util.InfoTypeError: - infoStyle = infoStyle.Background(styles.Red) + infoStyle = infoStyle.Background(t.Error()) } + // Truncate message if it's longer than available width msg := m.info.Msg availWidth := m.availableFooterMsgWidth(diagnostics) - 10 @@ -130,9 +154,9 @@ func (m statusCmp) View() string { } status += infoStyle.Render(msg) } else { - status += styles.Padded. - Foreground(styles.Base). - Background(styles.BackgroundDim). + status += styles.Padded(). + Foreground(t.Text()). + Background(t.BackgroundSecondary()). Width(m.availableFooterMsgWidth(diagnostics)). Render("") } @@ -143,6 +167,8 @@ func (m statusCmp) View() string { } func (m *statusCmp) projectDiagnostics() string { + t := theme.CurrentTheme() + // Check if any LSP server is still initializing initializing := false for _, client := range m.lspClients { @@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string { // If any server is initializing, show that status if initializing { return lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Peach). + Background(t.BackgroundDarker()). + Foreground(t.Warning()). Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon)) } @@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string { if len(errorDiagnostics) > 0 { errStr := lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Error). + Background(t.BackgroundDarker()). + Foreground(t.Error()). Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics))) diagnostics = append(diagnostics, errStr) } if len(warnDiagnostics) > 0 { warnStr := lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Warning). + Background(t.BackgroundDarker()). + Foreground(t.Warning()). Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics))) diagnostics = append(diagnostics, warnStr) } if len(hintDiagnostics) > 0 { hintStr := lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Text). + Background(t.BackgroundDarker()). + Foreground(t.Text()). Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics))) diagnostics = append(diagnostics, hintStr) } if len(infoDiagnostics) > 0 { infoStr := lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Peach). + Background(t.BackgroundDarker()). + Foreground(t.Info()). Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics))) diagnostics = append(diagnostics, infoStr) } @@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int { } func (m statusCmp) model() string { + t := theme.CurrentTheme() + cfg := config.Get() coder, ok := cfg.Agents[config.AgentCoder] @@ -237,14 +265,22 @@ func (m statusCmp) model() string { return "Unknown" } model := models.SupportedModels[coder.Model] - return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name) + + return styles.Padded(). + Background(t.Secondary()). + Foreground(t.Background()). + Render(model.Name) } -func (m statusCmp) SetHelpMsg(s string) { - helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s) +func (m statusCmp) SetHelpWidgetMsg(s string) { + // Update the help widget text using the getHelpWidget function + helpWidget = getHelpWidget(s) } func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp { + // Initialize the help widget with default text + helpWidget = getHelpWidget("") + return &statusCmp{ messageTTL: 10 * time.Second, lspClients: lspClients, diff --git a/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go index 5a1888cd296469e7251d2d4db69161a4c9c454bb..c725f020cbb6c899f69ecf50a86998c2141b806d 100644 --- a/internal/tui/components/dialog/commands.go +++ b/internal/tui/components/dialog/commands.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (c *commandDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if len(c.commands) == 0 { - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(40). Render("No commands available") } @@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string { for i := startIdx; i < endIdx; i++ { cmd := c.commands[i] - itemStyle := styles.BaseStyle.Width(maxWidth) - descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim) + itemStyle := baseStyle.Width(maxWidth) + descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted()) if i == c.selectedIdx { itemStyle = itemStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) descStyle = descStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background) + Background(t.Primary()). + Foreground(t.Background()) } title := itemStyle.Padding(0, 1).Render(cmd.Title) @@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string { } } - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). @@ -187,15 +191,15 @@ func (c *commandDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), - styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)), - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go index ef3ab3d789c27fd49bbb705b854eaf8e655e69f0..1f161c7d2cd025307f261b90a154fb2f35efbdae 100644 --- a/internal/tui/components/dialog/help.go +++ b/internal/tui/components/dialog/help.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type helpCmp struct { @@ -53,10 +54,21 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding { } func (h *helpCmp) render() string { - helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0) - helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + helpKeyStyle := styles.Bold(). + Background(t.Background()). + Foreground(t.Text()). + Padding(0, 1, 0, 0) + + helpDescStyle := styles.Regular(). + Background(t.Background()). + Foreground(t.TextMuted()) + // Compile list of bindings to render bindings := removeDuplicateBindings(h.keys) + // Enumerate through each group of bindings, populating a series of // pairs of columns, one for keys, one for descriptions var ( @@ -64,6 +76,7 @@ func (h *helpCmp) render() string { width int rows = 10 - 2 ) + for i := 0; i < len(bindings); i += rows { var ( keys []string @@ -73,11 +86,12 @@ func (h *helpCmp) render() string { keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key)) descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc)) } + // Render pair of columns; beyond the first pair, render a three space // left margin, in order to visually separate the pairs. var cols []string if len(pairs) > 0 { - cols = []string{styles.BaseStyle.Render(" ")} + cols = []string{baseStyle.Render(" ")} } maxDescWidth := 0 @@ -89,7 +103,7 @@ func (h *helpCmp) render() string { for i := range descs { remainingWidth := maxDescWidth - lipgloss.Width(descs[i]) if remainingWidth > 0 { - descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth)) + descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) } } maxKeyWidth := 0 @@ -101,7 +115,7 @@ func (h *helpCmp) render() string { for i := range keys { remainingWidth := maxKeyWidth - lipgloss.Width(keys[i]) if remainingWidth > 0 { - keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth)) + keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth)) } } @@ -110,7 +124,7 @@ func (h *helpCmp) render() string { strings.Join(descs, "\n"), ) - pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...)) + pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...)) // check whether it exceeds the maximum width avail (the width of the // terminal, subtracting 2 for the borders). width += lipgloss.Width(pair) @@ -130,9 +144,9 @@ func (h *helpCmp) render() string { lipgloss.Left, // x lipgloss.Top, // y lastPair, // content - lipgloss.WithWhitespaceBackground(styles.Background), // background + lipgloss.WithWhitespaceBackground(t.Background()), )) - content := styles.BaseStyle.Width(h.width).Render( + content := baseStyle.Width(h.width).Render( lipgloss.JoinHorizontal( lipgloss.Top, prefix..., @@ -140,8 +154,9 @@ func (h *helpCmp) render() string { ) return content } + // Join pairs of columns and enclose in a border - content := styles.BaseStyle.Width(h.width).Render( + content := baseStyle.Width(h.width).Render( lipgloss.JoinHorizontal( lipgloss.Top, pairs..., @@ -151,22 +166,25 @@ func (h *helpCmp) render() string { } func (h *helpCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + content := h.render() - header := styles.BaseStyle. + header := baseStyle. Bold(true). Width(lipgloss.Width(content)). - Foreground(styles.PrimaryColor). + Foreground(t.Primary()). Render("Keyboard Shortcuts") - return styles.BaseStyle.Padding(1). + return baseStyle.Padding(1). Border(lipgloss.RoundedBorder()). - BorderForeground(styles.ForgroundDim). + BorderForeground(t.TextMuted()). Width(h.width). - BorderBackground(styles.Background). + BorderBackground(t.Background()). Render( lipgloss.JoinVertical(lipgloss.Center, header, - styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))), + baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))), content, ), ) diff --git a/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go index bfe2323fdaf2eff80e29920b236bf4cc4ab56c81..77c76584d9bf1abaceb2206c3f577c8834c3cc04 100644 --- a/internal/tui/components/dialog/init.go +++ b/internal/tui/components/dialog/init.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -92,55 +93,58 @@ 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() + // Calculate width needed for content maxWidth := 60 // Width for explanation text - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). Render("Initialize Project") - explanation := styles.BaseStyle. - Foreground(styles.Forground). + explanation := baseStyle. + Foreground(t.Text()). Width(maxWidth). Padding(0, 1). Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.") - question := styles.BaseStyle. - Foreground(styles.Forground). + question := baseStyle. + Foreground(t.Text()). Width(maxWidth). Padding(1, 1). Render("Would you like to initialize this project?") maxWidth = min(maxWidth, m.width-10) - yesStyle := styles.BaseStyle - noStyle := styles.BaseStyle + yesStyle := baseStyle + noStyle := baseStyle if m.selected == 0 { yesStyle = yesStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) noStyle = noStyle. - Background(styles.Background). - Foreground(styles.PrimaryColor) + Background(t.Background()). + Foreground(t.Primary()) } else { noStyle = noStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) yesStyle = yesStyle. - Background(styles.Background). - Foreground(styles.PrimaryColor) + Background(t.Background()). + Foreground(t.Primary()) } yes := yesStyle.Padding(0, 3).Render("Yes") no := noStyle.Padding(0, 3).Render("No") - buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no) - buttons = styles.BaseStyle. + buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no) + buttons = baseStyle. Width(maxWidth). Padding(1, 0). Render(buttons) @@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), explanation, question, buttons, - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/models.go b/internal/tui/components/dialog/models.go index d10d5c8cc6b695ea34d4665309e33b38013f2172..48b7ce03f470c0dc6089dc74c39901967cebfd38 100644 --- a/internal/tui/components/dialog/models.go +++ b/internal/tui/components/dialog/models.go @@ -12,6 +12,7 @@ import ( "github.com/opencode-ai/opencode/internal/llm/models" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -185,10 +186,13 @@ func (m *modelDialogCmp) switchProvider(offset int) { } func (m *modelDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + // Capitalize first letter of provider name providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:]) - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxDialogWidth). Padding(0, 0, 1). @@ -199,10 +203,10 @@ func (m *modelDialogCmp) View() string { modelItems := make([]string, 0, endIdx-m.scrollOffset) for i := m.scrollOffset; i < endIdx; i++ { - itemStyle := styles.BaseStyle.Width(maxDialogWidth) + itemStyle := baseStyle.Width(maxDialogWidth) if i == m.selectedIdx { - itemStyle = itemStyle.Background(styles.PrimaryColor). - Foreground(styles.Background).Bold(true) + itemStyle = itemStyle.Background(t.Primary()). + Foreground(t.Background()).Bold(true) } modelItems = append(modelItems, itemStyle.Render(m.models[i].Name)) } @@ -212,14 +216,14 @@ func (m *modelDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), + baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), scrollIndicator, ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } @@ -249,8 +253,11 @@ func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string { return "" } - return styles.BaseStyle. - Foreground(styles.PrimaryColor). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + return baseStyle. + Foreground(t.Primary()). Width(maxWidth). Align(lipgloss.Right). Bold(true). diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 1a1e0783b6f32f48fb52b7fc9fbe7c29eda19582..fb12a2cd5470a991e49fe3b79105ecb2790a22ae 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -7,13 +7,13 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/diff" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -149,25 +149,28 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd { } func (p *permissionDialogCmp) renderButtons() string { - allowStyle := styles.BaseStyle - allowSessionStyle := styles.BaseStyle - denyStyle := styles.BaseStyle - spacerStyle := styles.BaseStyle.Background(styles.Background) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + allowStyle := baseStyle + allowSessionStyle := baseStyle + denyStyle := baseStyle + spacerStyle := baseStyle.Background(t.Background()) // Style the selected button switch p.selectedOption { case 0: - allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + 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(styles.Background).Foreground(styles.PrimaryColor) - allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) + allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background()) + denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary()) case 2: - allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor) - denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background) + allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary()) + allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary()) + denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background()) } allowButton := allowStyle.Padding(0, 1).Render("Allow (a)") @@ -192,15 +195,18 @@ func (p *permissionDialogCmp) renderButtons() string { } func (p *permissionDialogCmp) renderHeader() string { - toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool") - toolValue := styles.BaseStyle. - Foreground(styles.Forground). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool") + toolValue := baseStyle. + Foreground(t.Text()). Width(p.width - lipgloss.Width(toolKey)). Render(fmt.Sprintf(": %s", p.permission.ToolName)) - pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path") - pathValue := styles.BaseStyle. - Foreground(styles.Forground). + pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path") + pathValue := baseStyle. + Foreground(t.Text()). Width(p.width - lipgloss.Width(pathKey)). Render(fmt.Sprintf(": %s", p.permission.Path)) @@ -210,45 +216,45 @@ func (p *permissionDialogCmp) renderHeader() string { toolKey, toolValue, ), - styles.BaseStyle.Render(strings.Repeat(" ", p.width)), + baseStyle.Render(strings.Repeat(" ", p.width)), lipgloss.JoinHorizontal( lipgloss.Left, pathKey, pathValue, ), - styles.BaseStyle.Render(strings.Repeat(" ", p.width)), + baseStyle.Render(strings.Repeat(" ", p.width)), } // Add tool-specific header information switch p.permission.ToolName { case tools.BashToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command")) case tools.EditToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff")) case tools.WriteToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff")) case tools.FetchToolName: - headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL")) + headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL")) } - return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) + return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) } func (p *permissionDialogCmp) renderBashContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.Command) // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - finalContent := styles.BaseStyle. + finalContent := baseStyle. Width(p.contentViewPort.Width). Render(renderedContent) p.contentViewPort.SetContent(finalContent) @@ -295,39 +301,45 @@ func (p *permissionDialogCmp) renderWriteContent() string { } func (p *permissionDialogCmp) renderFetchContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok { content := fmt.Sprintf("```bash\n%s\n```", pr.URL) // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - p.contentViewPort.SetContent(renderedContent) + finalContent := baseStyle. + Width(p.contentViewPort.Width). + Render(renderedContent) + p.contentViewPort.SetContent(finalContent) return p.styleViewport() } return "" } func (p *permissionDialogCmp) renderDefaultContent() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + content := p.permission.Description // Use the cache for markdown rendering renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.CatppuccinMarkdownStyle()), - glamour.WithWordWrap(p.width-10), - ) + r := styles.GetMarkdownRenderer(p.width-10) s, err := r.Render(content) - return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err + return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err }) - p.contentViewPort.SetContent(renderedContent) + finalContent := baseStyle. + Width(p.contentViewPort.Width). + Render(renderedContent) + p.contentViewPort.SetContent(finalContent) if renderedContent == "" { return "" @@ -337,17 +349,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string { } func (p *permissionDialogCmp) styleViewport() string { + t := theme.CurrentTheme() contentStyle := lipgloss.NewStyle(). - Background(styles.Background) + Background(t.Background()) return contentStyle.Render(p.contentViewPort.View()) } func (p *permissionDialogCmp) render() string { - title := styles.BaseStyle. + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + title := baseStyle. Bold(true). Width(p.width - 4). - Foreground(styles.PrimaryColor). + Foreground(t.Primary()). Render("Permission Required") // Render header headerContent := p.renderHeader() @@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string { content := lipgloss.JoinVertical( lipgloss.Top, title, - styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))), + baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))), headerContent, contentFinal, buttons, - styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)), + baseStyle.Render(strings.Repeat(" ", p.width-4)), ) - return styles.BaseStyle. + return baseStyle. Padding(1, 0, 0, 1). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(p.width). Height(p.height). Render( diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index 38c7dc1a1f3b389fa3c48278780a93a48f0541a2..f755fa272547657c84a40125421d9e7411a1530e 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (q *quitDialogCmp) View() string { - yesStyle := styles.BaseStyle - noStyle := styles.BaseStyle - spacerStyle := styles.BaseStyle.Background(styles.Background) + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + yesStyle := baseStyle + noStyle := baseStyle + spacerStyle := baseStyle.Background(t.Background()) if q.selectedNo { - noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + noStyle = noStyle.Background(t.Primary()).Foreground(t.Background()) + yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary()) } else { - yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background) - noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor) + yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background()) + noStyle = noStyle.Background(t.Background()).Foreground(t.Primary()) } yesButton := yesStyle.Padding(0, 1).Render("Yes") @@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string { buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons } - content := styles.BaseStyle.Render( + content := baseStyle.Render( lipgloss.JoinVertical( lipgloss.Center, question, @@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string { ), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index 90a07358fb3f252cc674c3dbc9b88849e72c06f5..a29fa7131ed1b1abbe7ae170eb2af107ad5c5647 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -7,6 +7,7 @@ import ( "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (s *sessionDialogCmp) View() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + if len(s.sessions) == 0 { - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(40). Render("No sessions available") } @@ -146,20 +150,20 @@ func (s *sessionDialogCmp) View() string { for i := startIdx; i < endIdx; i++ { sess := s.sessions[i] - itemStyle := styles.BaseStyle.Width(maxWidth) + itemStyle := baseStyle.Width(maxWidth) if i == s.selectedIdx { itemStyle = itemStyle. - Background(styles.PrimaryColor). - Foreground(styles.Background). + Background(t.Primary()). + Foreground(t.Background()). Bold(true) } sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title)) } - title := styles.BaseStyle. - Foreground(styles.PrimaryColor). + title := baseStyle. + Foreground(t.Primary()). Bold(true). Width(maxWidth). Padding(0, 1). @@ -168,15 +172,15 @@ func (s *sessionDialogCmp) View() string { content := lipgloss.JoinVertical( lipgloss.Left, title, - styles.BaseStyle.Width(maxWidth).Render(""), - styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)), - styles.BaseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)), + baseStyle.Width(maxWidth).Render(""), ) - return styles.BaseStyle.Padding(1, 2). + return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). - BorderBackground(styles.Background). - BorderForeground(styles.ForgroundDim). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()). Width(lipgloss.Width(content) + 4). Render(content) } diff --git a/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go new file mode 100644 index 0000000000000000000000000000000000000000..d35d3e2b6df04ca6049728224533293ba0ef2285 --- /dev/null +++ b/internal/tui/components/dialog/theme.go @@ -0,0 +1,198 @@ +package dialog + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/opencode-ai/opencode/internal/tui/layout" + "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" + "github.com/opencode-ai/opencode/internal/tui/util" +) + +// ThemeChangedMsg is sent when the theme is changed +type ThemeChangedMsg struct { + ThemeName string +} + +// CloseThemeDialogMsg is sent when the theme dialog is closed +type CloseThemeDialogMsg struct{} + +// ThemeDialog interface for the theme switching dialog +type ThemeDialog interface { + tea.Model + layout.Bindings +} + +type themeDialogCmp struct { + themes []string + selectedIdx int + width int + height int + currentTheme string +} + +type themeKeyMap struct { + Up key.Binding + Down key.Binding + Enter key.Binding + Escape key.Binding + J key.Binding + K key.Binding +} + +var themeKeys = themeKeyMap{ + Up: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("↑", "previous theme"), + ), + Down: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("↓", "next theme"), + ), + Enter: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select theme"), + ), + Escape: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close"), + ), + J: key.NewBinding( + key.WithKeys("j"), + key.WithHelp("j", "next theme"), + ), + K: key.NewBinding( + key.WithKeys("k"), + key.WithHelp("k", "previous theme"), + ), +} + +func (t *themeDialogCmp) Init() tea.Cmd { + // Load available themes and update selectedIdx based on current theme + t.themes = theme.AvailableThemes() + t.currentTheme = theme.CurrentThemeName() + + // Find the current theme in the list + for i, name := range t.themes { + if name == t.currentTheme { + t.selectedIdx = i + break + } + } + + return nil +} + +func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K): + if t.selectedIdx > 0 { + t.selectedIdx-- + } + return t, nil + case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J): + if t.selectedIdx < len(t.themes)-1 { + t.selectedIdx++ + } + return t, nil + case key.Matches(msg, themeKeys.Enter): + if len(t.themes) > 0 { + previousTheme := theme.CurrentThemeName() + selectedTheme := t.themes[t.selectedIdx] + if previousTheme == selectedTheme { + return t, util.CmdHandler(CloseThemeDialogMsg{}) + } + if err := theme.SetTheme(selectedTheme); err != nil { + return t, util.ReportError(err) + } + return t, util.CmdHandler(ThemeChangedMsg{ + ThemeName: selectedTheme, + }) + } + case key.Matches(msg, themeKeys.Escape): + return t, util.CmdHandler(CloseThemeDialogMsg{}) + } + case tea.WindowSizeMsg: + t.width = msg.Width + t.height = msg.Height + } + return t, nil +} + +func (t *themeDialogCmp) View() string { + currentTheme := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + if len(t.themes) == 0 { + return baseStyle.Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderBackground(currentTheme.Background()). + BorderForeground(currentTheme.TextMuted()). + Width(40). + Render("No themes available") + } + + // Calculate max width needed for theme names + maxWidth := 40 // Minimum width + for _, themeName := range t.themes { + if len(themeName) > maxWidth-4 { // Account for padding + maxWidth = len(themeName) + 4 + } + } + + maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow + + // Build the theme list + themeItems := make([]string, 0, len(t.themes)) + for i, themeName := range t.themes { + itemStyle := baseStyle.Width(maxWidth) + + if i == t.selectedIdx { + itemStyle = itemStyle. + Background(currentTheme.Primary()). + Foreground(currentTheme.Background()). + Bold(true) + } + + themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName)) + } + + title := baseStyle. + Foreground(currentTheme.Primary()). + Bold(true). + Width(maxWidth). + Padding(0, 1). + Render("Select Theme") + + content := lipgloss.JoinVertical( + lipgloss.Left, + title, + baseStyle.Width(maxWidth).Render(""), + baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)), + baseStyle.Width(maxWidth).Render(""), + ) + + return baseStyle.Padding(1, 2). + Border(lipgloss.RoundedBorder()). + BorderBackground(currentTheme.Background()). + BorderForeground(currentTheme.TextMuted()). + Width(lipgloss.Width(content) + 4). + Render(content) +} + +func (t *themeDialogCmp) BindingKeys() []key.Binding { + return layout.KeyMapToSlice(themeKeys) +} + +// NewThemeDialogCmp creates a new theme switching dialog +func NewThemeDialogCmp() ThemeDialog { + return &themeDialogCmp{ + themes: []string{}, + selectedIdx: 0, + currentTheme: "", + } +} + diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 8aaa7a41cd88842716834686912153ca9eb72566..9d7713bbf048beea68dc0d007af948df2b2a6127 100644 --- a/internal/tui/components/logs/details.go +++ b/internal/tui/components/logs/details.go @@ -12,6 +12,7 @@ import ( "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type DetailComponent interface { @@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (i *detailCmp) updateContent() { var content strings.Builder + t := theme.CurrentTheme() // Format the header with timestamp and level - timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0) + timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted()) levelStyle := getLevelStyle(i.currentLog.Level) header := lipgloss.JoinHorizontal( @@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() { content.WriteString("\n\n") // Message with styling - messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text) + messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) content.WriteString(messageStyle.Render("Message:")) content.WriteString("\n") content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message)) @@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() { // Attributes section if len(i.currentLog.Attributes) > 0 { - attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text) + attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) content.WriteString(attrHeaderStyle.Render("Attributes:")) content.WriteString("\n") // Create a table-like display for attributes - keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true) - valueStyle := lipgloss.NewStyle().Foreground(styles.Text) + keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true) + valueStyle := lipgloss.NewStyle().Foreground(t.Text()) for _, attr := range i.currentLog.Attributes { attrLine := fmt.Sprintf("%s: %s", @@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() { func getLevelStyle(level string) lipgloss.Style { style := lipgloss.NewStyle().Bold(true) - + t := theme.CurrentTheme() + switch strings.ToLower(level) { case "info": - return style.Foreground(styles.Blue) + return style.Foreground(t.Info()) case "warn", "warning": - return style.Foreground(styles.Warning) + return style.Foreground(t.Warning()) case "error", "err": - return style.Foreground(styles.Error) + return style.Foreground(t.Error()) case "debug": - return style.Foreground(styles.Green) + return style.Foreground(t.Success()) default: - return style.Foreground(styles.Text) + return style.Foreground(t.Text()) } } func (i *detailCmp) View() string { - return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background) + t := theme.CurrentTheme() + return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background()) } func (i *detailCmp) GetSize() (int, int) { diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index bffa7b1adfb2239a40c59859d0cdde2b40b0bfd7..8d59f967f0a53a80133a3c5d3b7d0e6785bda96e 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -11,6 +11,7 @@ import ( "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -61,7 +62,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (i *tableCmp) View() string { - return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background) + t := theme.CurrentTheme() + defaultStyles := table.DefaultStyles() + defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary()) + i.table.SetStyles(defaultStyles) + return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background()) } func (i *tableCmp) GetSize() (int, int) { @@ -121,11 +126,9 @@ func NewLogsTable() TableComponent { {Title: "Message", Width: 10}, {Title: "Attributes", Width: 10}, } - defaultStyles := table.DefaultStyles() - defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary) + tableModel := table.New( table.WithColumns(columns), - table.WithStyles(defaultStyles), ) tableModel.Focus() return &tableCmp{ diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index b92df5bb82fcc4c0549450b949bbd3c4fbe1d5b6..83aef587938cc1f7ed56690bab98e10c9351b350 100644 --- a/internal/tui/layout/container.go +++ b/internal/tui/layout/container.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type Container interface { @@ -29,9 +29,6 @@ type container struct { borderBottom bool borderLeft bool borderStyle lipgloss.Border - borderColor lipgloss.TerminalColor - - backgroundColor lipgloss.TerminalColor } func (c *container) Init() tea.Cmd { @@ -45,13 +42,12 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (c *container) View() string { + t := theme.CurrentTheme() style := lipgloss.NewStyle() width := c.width height := c.height - // Apply background color if specified - if c.backgroundColor != nil { - style = style.Background(c.backgroundColor) - } + + style = style.Background(t.Background()) // Apply border if any side is enabled if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft { @@ -69,11 +65,7 @@ func (c *container) View() string { width-- } style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft) - - // Apply border color if specified - if c.borderColor != nil { - style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor) - } + style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal()) } style = style. Width(width). @@ -132,11 +124,10 @@ func (c *container) BindingKeys() []key.Binding { type ContainerOption func(*container) func NewContainer(content tea.Model, options ...ContainerOption) Container { + c := &container{ - content: content, - borderColor: styles.BorderColor, - borderStyle: lipgloss.NormalBorder(), - backgroundColor: styles.Background, + content: content, + borderStyle: lipgloss.NormalBorder(), } for _, option := range options { @@ -201,12 +192,6 @@ func WithBorderStyle(style lipgloss.Border) ContainerOption { } } -func WithBorderColor(color lipgloss.TerminalColor) ContainerOption { - return func(c *container) { - c.borderColor = color - } -} - func WithRoundedBorder() ContainerOption { return WithBorderStyle(lipgloss.RoundedBorder()) } @@ -218,9 +203,3 @@ func WithThickBorder() ContainerOption { func WithDoubleBorder() ContainerOption { return WithBorderStyle(lipgloss.DoubleBorder()) } - -func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption { - return func(c *container) { - c.backgroundColor = color - } -} diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go index 379747e69b61eb295e171b82112e8119698f0bc7..3a14dbc5eeb9d9245dfd1b1c6a4ddda7f791b5f1 100644 --- a/internal/tui/layout/overlay.go +++ b/internal/tui/layout/overlay.go @@ -9,6 +9,7 @@ import ( "github.com/muesli/reflow/truncate" "github.com/muesli/termenv" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -43,12 +44,15 @@ func PlaceOverlay( fgHeight := len(fgLines) if shadow { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + var shadowbg string = "" shadowchar := lipgloss.NewStyle(). - Background(styles.BackgroundDarker). - Foreground(styles.Background). + Background(t.BackgroundDarker()). + Foreground(t.Background()). Render("░") - bgchar := styles.BaseStyle.Render(" ") + bgchar := baseStyle.Render(" ") for i := 0; i <= fgHeight; i++ { if i == 0 { shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n" diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index 6763e408cc8fc88484da9c0e291dee10b4d0d14b..2684a8447cbe4fec4e3d389cf148cec622bfd72b 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -4,7 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type SplitPaneLayout interface { @@ -29,8 +29,6 @@ type splitPaneLayout struct { rightPanel Container leftPanel Container bottomPanel Container - - backgroundColor lipgloss.TerminalColor } type SplitPaneOption func(*splitPaneLayout) @@ -113,11 +111,13 @@ func (s *splitPaneLayout) View() string { finalView = topSection } - if s.backgroundColor != nil && finalView != "" { + if finalView != "" { + t := theme.CurrentTheme() + style := lipgloss.NewStyle(). Width(s.width). Height(s.height). - Background(s.backgroundColor) + Background(t.Background()) return style.Render(finalView) } @@ -241,10 +241,10 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding { } func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout { + layout := &splitPaneLayout{ - ratio: 0.7, - verticalRatio: 0.9, // Default 80% for top section, 20% for bottom - backgroundColor: styles.Background, + ratio: 0.7, + verticalRatio: 0.9, // Default 90% for top section, 10% for bottom } for _, option := range options { option(layout) @@ -270,12 +270,6 @@ func WithRatio(ratio float64) SplitPaneOption { } } -func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption { - return func(s *splitPaneLayout) { - s.backgroundColor = color - } -} - func WithBottomPanel(panel Container) SplitPaneOption { return func(s *splitPaneLayout) { s.bottomPanel = panel diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index e801d73cbb6324ff99de9c3859b11c66f69ac7e0..62a5b9f4f2f40f2ff467ba9f7ffd0daf1eca56ca 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -142,7 +142,6 @@ func NewChatPage(app *app.App) tea.Model { chat.NewMessagesCmp(app), layout.WithPadding(1, 1, 0, 1), ) - editorContainer := layout.NewContainer( chat.NewEditorCmp(app), layout.WithBorder(true, false, false, false), diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index a285e4041c86e0c4c4dcb133934cc6c0ac1d4357..9bd545287f4c35c1ea99d33efd5f274275d63a4f 100644 --- a/internal/tui/page/logs.go +++ b/internal/tui/page/logs.go @@ -42,7 +42,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (p *logsPage) View() string { - style := styles.BaseStyle.Width(p.width).Height(p.height) + style := styles.BaseStyle().Width(p.width).Height(p.height) return style.Render(lipgloss.JoinVertical(lipgloss.Top, p.table.View(), p.details.View(), @@ -77,7 +77,7 @@ func (p *logsPage) Init() tea.Cmd { func NewLogsPage() LogPage { return &logsPage{ - table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)), - details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)), + table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()), + details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()), } } diff --git a/internal/tui/styles/huh.go b/internal/tui/styles/huh.go deleted file mode 100644 index d0e8727584268ffb0f0bef0f357774e485c3f692..0000000000000000000000000000000000000000 --- a/internal/tui/styles/huh.go +++ /dev/null @@ -1,46 +0,0 @@ -package styles - -import ( - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" -) - -func HuhTheme() *huh.Theme { - t := huh.ThemeBase() - - t.Focused.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) - t.Focused.Title = t.Focused.Title.Foreground(Text) - t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(Text) - t.Focused.Directory = t.Focused.Directory.Foreground(Text) - t.Focused.Description = t.Focused.Description.Foreground(SubText0) - t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(Red) - t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(Red) - t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(Blue) - t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(Blue) - t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(Blue) - t.Focused.Option = t.Focused.Option.Foreground(Text) - t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(Blue) - t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(Green) - t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(Green) - t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(Text) - t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(Text) - t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(Base).Background(Blue) - t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(Text).Background(Base) - - t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Teal) - t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(Overlay0) - t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(Blue) - - t.Blurred = t.Focused - t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder()) - - t.Help.Ellipsis = t.Help.Ellipsis.Foreground(SubText0) - t.Help.ShortKey = t.Help.ShortKey.Foreground(SubText0) - t.Help.ShortDesc = t.Help.ShortDesc.Foreground(Ovelay1) - t.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(SubText0) - t.Help.FullKey = t.Help.FullKey.Foreground(SubText0) - t.Help.FullDesc = t.Help.FullDesc.Foreground(Ovelay1) - t.Help.FullSeparator = t.Help.FullSeparator.Foreground(SubText0) - - return t -} diff --git a/internal/tui/styles/markdown.go b/internal/tui/styles/markdown.go index 52816eab3ac0d104a0add3cf935c4327f21fc469..6b43d97cfeeea6829d298816b6f90f9d61e91629 100644 --- a/internal/tui/styles/markdown.go +++ b/internal/tui/styles/markdown.go @@ -1,8 +1,10 @@ package styles import ( + "github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/lipgloss" + "github.com/opencode-ai/opencode/internal/tui/theme" ) const defaultMargin = 1 @@ -12,930 +14,271 @@ func boolPtr(b bool) *bool { return &b } func stringPtr(s string) *string { return &s } func uintPtr(u uint) *uint { return &u } -// CatppuccinMarkdownStyle is the Catppuccin Mocha style for Glamour markdown rendering. -func CatppuccinMarkdownStyle() ansi.StyleConfig { - isDark := lipgloss.HasDarkBackground() - if isDark { - return catppuccinDark - } - return catppuccinLight +// returns a glamour TermRenderer configured with the current theme +func GetMarkdownRenderer(width int) *glamour.TermRenderer { + r, _ := glamour.NewTermRenderer( + glamour.WithStyles(generateMarkdownStyleConfig()), + glamour.WithWordWrap(width), + ) + return r } -var catppuccinDark = ansi.StyleConfig{ - Document: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "\n", - BlockSuffix: "", - Color: stringPtr(dark.Text().Hex), - }, - Margin: uintPtr(defaultMargin), - }, - BlockQuote: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(dark.Yellow().Hex), - Italic: boolPtr(true), - Prefix: "┃ ", - }, - Indent: uintPtr(1), - IndentToken: stringPtr(BaseStyle.Render(" ")), - }, - List: ansi.StyleList{ - LevelIndent: defaultMargin, - StyleBlock: ansi.StyleBlock{ - IndentToken: stringPtr(BaseStyle.Render(" ")), - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(dark.Text().Hex), - }, - }, - }, - Heading: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockSuffix: "\n", - Color: stringPtr(dark.Mauve().Hex), - Bold: boolPtr(true), - }, - }, - H1: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "# ", - Color: stringPtr(dark.Lavender().Hex), - Bold: boolPtr(true), - BlockPrefix: "\n", - }, - }, - H2: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "## ", - Color: stringPtr(dark.Mauve().Hex), - Bold: boolPtr(true), - }, - }, - H3: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "### ", - Color: stringPtr(dark.Pink().Hex), - Bold: boolPtr(true), - }, - }, - H4: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "#### ", - Color: stringPtr(dark.Flamingo().Hex), - Bold: boolPtr(true), - }, - }, - H5: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "##### ", - Color: stringPtr(dark.Rosewater().Hex), - Bold: boolPtr(true), - }, - }, - H6: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "###### ", - Color: stringPtr(dark.Rosewater().Hex), - Bold: boolPtr(true), - }, - }, - Strikethrough: ansi.StylePrimitive{ - CrossedOut: boolPtr(true), - Color: stringPtr(dark.Overlay1().Hex), - }, - Emph: ansi.StylePrimitive{ - Color: stringPtr(dark.Yellow().Hex), - Italic: boolPtr(true), - }, - Strong: ansi.StylePrimitive{ - Bold: boolPtr(true), - Color: stringPtr(dark.Peach().Hex), - }, - HorizontalRule: ansi.StylePrimitive{ - Color: stringPtr(dark.Overlay0().Hex), - Format: "\n─────────────────────────────────────────\n", - }, - Item: ansi.StylePrimitive{ - BlockPrefix: "• ", - Color: stringPtr(dark.Blue().Hex), - }, - Enumeration: ansi.StylePrimitive{ - BlockPrefix: ". ", - Color: stringPtr(dark.Sky().Hex), - }, - Task: ansi.StyleTask{ - StylePrimitive: ansi.StylePrimitive{}, - Ticked: "[✓] ", - Unticked: "[ ] ", - }, - Link: ansi.StylePrimitive{ - Color: stringPtr(dark.Sky().Hex), - Underline: boolPtr(true), - }, - LinkText: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - Bold: boolPtr(true), - }, - Image: ansi.StylePrimitive{ - Color: stringPtr(dark.Sapphire().Hex), - Underline: boolPtr(true), - Format: "🖼 {{.text}}", - }, - ImageText: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - Format: "{{.text}}", - }, - Code: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(dark.Green().Hex), - Prefix: "", - Suffix: "", - }, - }, - CodeBlock: ansi.StyleCodeBlock{ - StyleBlock: ansi.StyleBlock{ +// creates an ansi.StyleConfig for markdown rendering +// using adaptive colors from the provided theme. +func generateMarkdownStyleConfig() ansi.StyleConfig { + t := theme.CurrentTheme() + + return ansi.StyleConfig{ + Document: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Prefix: " ", - Color: stringPtr(dark.Text().Hex), + BlockPrefix: "", + BlockSuffix: "", + Color: stringPtr(adaptiveColorToString(t.MarkdownText())), }, - Margin: uintPtr(defaultMargin), }, - Chroma: &ansi.Chroma{ - Text: ansi.StylePrimitive{ - Color: stringPtr(dark.Text().Hex), - }, - Error: ansi.StylePrimitive{ - Color: stringPtr(dark.Text().Hex), - }, - Comment: ansi.StylePrimitive{ - Color: stringPtr(dark.Overlay1().Hex), - }, - CommentPreproc: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - Keyword: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - KeywordReserved: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - KeywordNamespace: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - KeywordType: ansi.StylePrimitive{ - Color: stringPtr(dark.Sky().Hex), - }, - Operator: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - Punctuation: ansi.StylePrimitive{ - Color: stringPtr(dark.Text().Hex), - }, - Name: ansi.StylePrimitive{ - Color: stringPtr(dark.Sky().Hex), - }, - NameBuiltin: ansi.StylePrimitive{ - Color: stringPtr(dark.Sky().Hex), - }, - NameTag: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - NameAttribute: ansi.StylePrimitive{ - Color: stringPtr(dark.Green().Hex), - }, - NameClass: ansi.StylePrimitive{ - Color: stringPtr(dark.Sky().Hex), - }, - NameConstant: ansi.StylePrimitive{ - Color: stringPtr(dark.Mauve().Hex), - }, - NameDecorator: ansi.StylePrimitive{ - Color: stringPtr(dark.Green().Hex), - }, - NameFunction: ansi.StylePrimitive{ - Color: stringPtr(dark.Green().Hex), - }, - LiteralNumber: ansi.StylePrimitive{ - Color: stringPtr(dark.Teal().Hex), - }, - LiteralString: ansi.StylePrimitive{ - Color: stringPtr(dark.Yellow().Hex), - }, - LiteralStringEscape: ansi.StylePrimitive{ - Color: stringPtr(dark.Pink().Hex), - }, - GenericDeleted: ansi.StylePrimitive{ - Color: stringPtr(dark.Red().Hex), - }, - GenericEmph: ansi.StylePrimitive{ - Color: stringPtr(dark.Yellow().Hex), + BlockQuote: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownBlockQuote())), Italic: boolPtr(true), + Prefix: "┃ ", }, - GenericInserted: ansi.StylePrimitive{ - Color: stringPtr(dark.Green().Hex), - }, - GenericStrong: ansi.StylePrimitive{ - Color: stringPtr(dark.Peach().Hex), - Bold: boolPtr(true), - }, - GenericSubheading: ansi.StylePrimitive{ - Color: stringPtr(dark.Mauve().Hex), + Indent: uintPtr(1), + IndentToken: stringPtr(BaseStyle().Render(" ")), + }, + List: ansi.StyleList{ + LevelIndent: defaultMargin, + StyleBlock: ansi.StyleBlock{ + IndentToken: stringPtr(BaseStyle().Render(" ")), + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownText())), + }, }, }, - }, - Table: ansi.StyleTable{ - StyleBlock: ansi.StyleBlock{ + Heading: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "\n", BlockSuffix: "\n", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, }, - CenterSeparator: stringPtr("┼"), - ColumnSeparator: stringPtr("│"), - RowSeparator: stringPtr("─"), - }, - DefinitionDescription: ansi.StylePrimitive{ - BlockPrefix: "\n ❯ ", - Color: stringPtr(dark.Sapphire().Hex), - }, -} - -var catppuccinLight = ansi.StyleConfig{ - Document: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "\n", - BlockSuffix: "\n", - Color: stringPtr(light.Text().Hex), - }, - Margin: uintPtr(defaultMargin), - }, - BlockQuote: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(light.Yellow().Hex), - Italic: boolPtr(true), - Prefix: "┃ ", - }, - Indent: uintPtr(1), - Margin: uintPtr(defaultMargin), - }, - List: ansi.StyleList{ - LevelIndent: defaultMargin, - StyleBlock: ansi.StyleBlock{ + H1: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(light.Text().Hex), + Prefix: "# ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, }, - }, - Heading: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockSuffix: "\n", - Color: stringPtr(light.Mauve().Hex), - Bold: boolPtr(true), - }, - }, - H1: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "# ", - Color: stringPtr(light.Lavender().Hex), - Bold: boolPtr(true), - BlockPrefix: "\n", - }, - }, - H2: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "## ", - Color: stringPtr(light.Mauve().Hex), - Bold: boolPtr(true), - }, - }, - H3: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "### ", - Color: stringPtr(light.Pink().Hex), - Bold: boolPtr(true), - }, - }, - H4: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "#### ", - Color: stringPtr(light.Flamingo().Hex), - Bold: boolPtr(true), - }, - }, - H5: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "##### ", - Color: stringPtr(light.Rosewater().Hex), - Bold: boolPtr(true), - }, - }, - H6: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "###### ", - Color: stringPtr(light.Rosewater().Hex), - Bold: boolPtr(true), - }, - }, - Strikethrough: ansi.StylePrimitive{ - CrossedOut: boolPtr(true), - Color: stringPtr(light.Overlay1().Hex), - }, - Emph: ansi.StylePrimitive{ - Color: stringPtr(light.Yellow().Hex), - Italic: boolPtr(true), - }, - Strong: ansi.StylePrimitive{ - Bold: boolPtr(true), - Color: stringPtr(light.Peach().Hex), - }, - HorizontalRule: ansi.StylePrimitive{ - Color: stringPtr(light.Overlay0().Hex), - Format: "\n─────────────────────────────────────────\n", - }, - Item: ansi.StylePrimitive{ - BlockPrefix: "• ", - Color: stringPtr(light.Blue().Hex), - }, - Enumeration: ansi.StylePrimitive{ - BlockPrefix: ". ", - Color: stringPtr(light.Sky().Hex), - }, - Task: ansi.StyleTask{ - StylePrimitive: ansi.StylePrimitive{}, - Ticked: "[✓] ", - Unticked: "[ ] ", - }, - Link: ansi.StylePrimitive{ - Color: stringPtr(light.Sky().Hex), - Underline: boolPtr(true), - }, - LinkText: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - Bold: boolPtr(true), - }, - Image: ansi.StylePrimitive{ - Color: stringPtr(light.Sapphire().Hex), - Underline: boolPtr(true), - Format: "🖼 {{.text}}", - }, - ImageText: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - Format: "{{.text}}", - }, - Code: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(light.Green().Hex), - Prefix: " ", - Suffix: " ", - }, - }, - CodeBlock: ansi.StyleCodeBlock{ - StyleBlock: ansi.StyleBlock{ + H2: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Prefix: " ", - Color: stringPtr(light.Text().Hex), + Prefix: "## ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, - - Margin: uintPtr(defaultMargin), }, - Chroma: &ansi.Chroma{ - Text: ansi.StylePrimitive{ - Color: stringPtr(light.Text().Hex), - }, - Error: ansi.StylePrimitive{ - Color: stringPtr(light.Text().Hex), - }, - Comment: ansi.StylePrimitive{ - Color: stringPtr(light.Overlay1().Hex), - }, - CommentPreproc: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - Keyword: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - KeywordReserved: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - KeywordNamespace: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - KeywordType: ansi.StylePrimitive{ - Color: stringPtr(light.Sky().Hex), - }, - Operator: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - Punctuation: ansi.StylePrimitive{ - Color: stringPtr(light.Text().Hex), - }, - Name: ansi.StylePrimitive{ - Color: stringPtr(light.Sky().Hex), - }, - NameBuiltin: ansi.StylePrimitive{ - Color: stringPtr(light.Sky().Hex), - }, - NameTag: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - NameAttribute: ansi.StylePrimitive{ - Color: stringPtr(light.Green().Hex), - }, - NameClass: ansi.StylePrimitive{ - Color: stringPtr(light.Sky().Hex), - }, - NameConstant: ansi.StylePrimitive{ - Color: stringPtr(light.Mauve().Hex), - }, - NameDecorator: ansi.StylePrimitive{ - Color: stringPtr(light.Green().Hex), - }, - NameFunction: ansi.StylePrimitive{ - Color: stringPtr(light.Green().Hex), - }, - LiteralNumber: ansi.StylePrimitive{ - Color: stringPtr(light.Teal().Hex), - }, - LiteralString: ansi.StylePrimitive{ - Color: stringPtr(light.Yellow().Hex), - }, - LiteralStringEscape: ansi.StylePrimitive{ - Color: stringPtr(light.Pink().Hex), - }, - GenericDeleted: ansi.StylePrimitive{ - Color: stringPtr(light.Red().Hex), - }, - GenericEmph: ansi.StylePrimitive{ - Color: stringPtr(light.Yellow().Hex), - Italic: boolPtr(true), - }, - GenericInserted: ansi.StylePrimitive{ - Color: stringPtr(light.Green().Hex), - }, - GenericStrong: ansi.StylePrimitive{ - Color: stringPtr(light.Peach().Hex), - Bold: boolPtr(true), - }, - GenericSubheading: ansi.StylePrimitive{ - Color: stringPtr(light.Mauve().Hex), - }, - }, - }, - Table: ansi.StyleTable{ - StyleBlock: ansi.StyleBlock{ + H3: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "\n", - BlockSuffix: "\n", + Prefix: "### ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, }, - CenterSeparator: stringPtr("┼"), - ColumnSeparator: stringPtr("│"), - RowSeparator: stringPtr("─"), - }, - DefinitionDescription: ansi.StylePrimitive{ - BlockPrefix: "\n ❯ ", - Color: stringPtr(light.Sapphire().Hex), - }, -} - -func MarkdownTheme(focused bool) ansi.StyleConfig { - if !focused { - return ASCIIStyleConfig - } else { - return DraculaStyleConfig - } -} - -const ( - defaultListIndent = 2 - defaultListLevelIndent = 4 -) - -var ASCIIStyleConfig = ansi.StyleConfig{ - Document: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Color: stringPtr(ForgroundDim.Dark), - }, - Indent: uintPtr(1), - IndentToken: stringPtr(BaseStyle.Render(" ")), - }, - BlockQuote: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - Indent: uintPtr(1), - IndentToken: stringPtr("| "), - }, - Paragraph: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - }, - List: ansi.StyleList{ - StyleBlock: ansi.StyleBlock{ - IndentToken: stringPtr(BaseStyle.Render(" ")), + H4: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Prefix: "#### ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, }, - LevelIndent: defaultListLevelIndent, - }, - Heading: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - BlockSuffix: "\n", - }, - }, - H1: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "# ", - }, - }, - H2: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "## ", - }, - }, - H3: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "### ", - }, - }, - H4: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "#### ", - }, - }, - H5: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "##### ", - }, - }, - H6: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Prefix: "###### ", - }, - }, - Strikethrough: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - BlockPrefix: "~~", - BlockSuffix: "~~", - }, - Emph: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - BlockPrefix: "*", - BlockSuffix: "*", - }, - Strong: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - BlockPrefix: "**", - BlockSuffix: "**", - }, - HorizontalRule: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Format: "\n--------\n", - }, - Item: ansi.StylePrimitive{ - BlockPrefix: "• ", - BackgroundColor: stringPtr(Background.Dark), - }, - Enumeration: ansi.StylePrimitive{ - BlockPrefix: ". ", - BackgroundColor: stringPtr(Background.Dark), - }, - Task: ansi.StyleTask{ - Ticked: "[x] ", - Unticked: "[ ] ", - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - }, - ImageText: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - Format: "Image: {{.text}} →", - }, - Code: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockPrefix: "`", - BlockSuffix: "`", - BackgroundColor: stringPtr(Background.Dark), - }, - }, - CodeBlock: ansi.StyleCodeBlock{ - StyleBlock: ansi.StyleBlock{ + H5: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Prefix: "##### ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, - Margin: uintPtr(defaultMargin), }, - }, - Table: ansi.StyleTable{ - StyleBlock: ansi.StyleBlock{ + H6: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Prefix: "###### ", + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + Bold: boolPtr(true), }, - IndentToken: stringPtr(BaseStyle.Render(" ")), - }, - CenterSeparator: stringPtr("|"), - ColumnSeparator: stringPtr("|"), - RowSeparator: stringPtr("-"), - }, - DefinitionDescription: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - BlockPrefix: "\n* ", - }, -} - -var DraculaStyleConfig = ansi.StyleConfig{ - Document: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(Forground.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, - Indent: uintPtr(defaultMargin), - IndentToken: stringPtr(BaseStyle.Render(" ")), - }, - BlockQuote: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr("#f1fa8c"), - Italic: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), }, - Indent: uintPtr(defaultMargin), - IndentToken: stringPtr(BaseStyle.Render(" ")), - }, - Paragraph: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Strikethrough: ansi.StylePrimitive{ + CrossedOut: boolPtr(true), + Color: stringPtr(adaptiveColorToString(t.TextMuted())), }, - }, - List: ansi.StyleList{ - LevelIndent: defaultMargin, - StyleBlock: ansi.StyleBlock{ - IndentToken: stringPtr(BaseStyle.Render(" ")), - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(Forground.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, + Emph: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())), + Italic: boolPtr(true), }, - }, - Heading: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - BlockSuffix: "\n", - Color: stringPtr(PrimaryColor.Dark), - Bold: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), + Strong: ansi.StylePrimitive{ + Bold: boolPtr(true), + Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())), }, - }, - H1: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "# ", - BackgroundColor: stringPtr(Background.Dark), + HorizontalRule: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())), + Format: "\n─────────────────────────────────────────\n", }, - }, - H2: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "## ", - BackgroundColor: stringPtr(Background.Dark), + Item: ansi.StylePrimitive{ + BlockPrefix: "• ", + Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())), }, - }, - H3: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "### ", - BackgroundColor: stringPtr(Background.Dark), + Enumeration: ansi.StylePrimitive{ + BlockPrefix: ". ", + Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())), }, - }, - H4: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "#### ", - BackgroundColor: stringPtr(Background.Dark), + Task: ansi.StyleTask{ + StylePrimitive: ansi.StylePrimitive{}, + Ticked: "[✓] ", + Unticked: "[ ] ", }, - }, - H5: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "##### ", - BackgroundColor: stringPtr(Background.Dark), + Link: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownLink())), + Underline: boolPtr(true), }, - }, - H6: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Prefix: "###### ", - BackgroundColor: stringPtr(Background.Dark), + LinkText: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())), + Bold: boolPtr(true), }, - }, - Strikethrough: ansi.StylePrimitive{ - CrossedOut: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - Emph: ansi.StylePrimitive{ - Color: stringPtr("#f1fa8c"), - Italic: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - Strong: ansi.StylePrimitive{ - Bold: boolPtr(true), - Color: stringPtr(Blue.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, - HorizontalRule: ansi.StylePrimitive{ - Color: stringPtr("#6272A4"), - Format: "\n--------\n", - BackgroundColor: stringPtr(Background.Dark), - }, - Item: ansi.StylePrimitive{ - BlockPrefix: "• ", - BackgroundColor: stringPtr(Background.Dark), - }, - Enumeration: ansi.StylePrimitive{ - BlockPrefix: ". ", - Color: stringPtr("#8be9fd"), - BackgroundColor: stringPtr(Background.Dark), - }, - Task: ansi.StyleTask{ - StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Image: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownImage())), + Underline: boolPtr(true), + Format: "🖼 {{.text}}", }, - Ticked: "[✓] ", - Unticked: "[ ] ", - }, - Link: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - Underline: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - LinkText: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - Image: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - Underline: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - ImageText: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - Format: "Image: {{.text}} →", - BackgroundColor: stringPtr(Background.Dark), - }, - Code: ansi.StyleBlock{ - StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr("#50fa7b"), - BackgroundColor: stringPtr(Background.Dark), + ImageText: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())), + Format: "{{.text}}", }, - }, - Text: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - DefinitionList: ansi.StyleBlock{}, - CodeBlock: ansi.StyleCodeBlock{ - StyleBlock: ansi.StyleBlock{ + Code: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Color: stringPtr(Blue.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, - Margin: uintPtr(defaultMargin), - }, - Chroma: &ansi.Chroma{ - NameOther: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - Literal: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - NameException: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - LiteralDate: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - Text: ansi.StylePrimitive{ - Color: stringPtr(Forground.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, - Error: ansi.StylePrimitive{ - Color: stringPtr("#f8f8f2"), - BackgroundColor: stringPtr("#ff5555"), - }, - Comment: ansi.StylePrimitive{ - Color: stringPtr("#6272A4"), - BackgroundColor: stringPtr(Background.Dark), - }, - CommentPreproc: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - Keyword: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - KeywordReserved: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - KeywordNamespace: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - KeywordType: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - BackgroundColor: stringPtr(Background.Dark), - }, - Operator: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - Punctuation: ansi.StylePrimitive{ - Color: stringPtr(Forground.Dark), - BackgroundColor: stringPtr(Background.Dark), - }, - Name: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameBuiltin: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameTag: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameAttribute: ansi.StylePrimitive{ - Color: stringPtr("#50fa7b"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameClass: ansi.StylePrimitive{ - Color: stringPtr("#8be9fd"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameConstant: ansi.StylePrimitive{ - Color: stringPtr("#bd93f9"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameDecorator: ansi.StylePrimitive{ - Color: stringPtr("#50fa7b"), - BackgroundColor: stringPtr(Background.Dark), - }, - NameFunction: ansi.StylePrimitive{ - Color: stringPtr("#50fa7b"), - BackgroundColor: stringPtr(Background.Dark), - }, - LiteralNumber: ansi.StylePrimitive{ - Color: stringPtr("#6EEFC0"), - BackgroundColor: stringPtr(Background.Dark), - }, - LiteralString: ansi.StylePrimitive{ - Color: stringPtr("#f1fa8c"), - BackgroundColor: stringPtr(Background.Dark), - }, - LiteralStringEscape: ansi.StylePrimitive{ - Color: stringPtr("#ff79c6"), - BackgroundColor: stringPtr(Background.Dark), - }, - GenericDeleted: ansi.StylePrimitive{ - Color: stringPtr("#ff5555"), - BackgroundColor: stringPtr(Background.Dark), - }, - GenericEmph: ansi.StylePrimitive{ - Color: stringPtr("#f1fa8c"), - Italic: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - GenericInserted: ansi.StylePrimitive{ - Color: stringPtr("#50fa7b"), - BackgroundColor: stringPtr(Background.Dark), - }, - GenericStrong: ansi.StylePrimitive{ - Color: stringPtr("#ffb86c"), - Bold: boolPtr(true), - BackgroundColor: stringPtr(Background.Dark), - }, - GenericSubheading: ansi.StylePrimitive{ - Color: stringPtr("#bd93f9"), - BackgroundColor: stringPtr(Background.Dark), - }, - Background: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), - }, - }, - }, - Table: ansi.StyleTable{ - StyleBlock: ansi.StyleBlock{ + Color: stringPtr(adaptiveColorToString(t.MarkdownCode())), + Prefix: "", + Suffix: "", + }, + }, + CodeBlock: ansi.StyleCodeBlock{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Color: stringPtr(adaptiveColorToString(t.MarkdownCodeBlock())), + }, + Margin: uintPtr(defaultMargin), + }, + Chroma: &ansi.Chroma{ + Text: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownText())), + }, + Error: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.Error())), + }, + Comment: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxComment())), + }, + CommentPreproc: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + Keyword: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + KeywordReserved: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + KeywordNamespace: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + KeywordType: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxType())), + }, + Operator: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())), + }, + Punctuation: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())), + }, + Name: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())), + }, + NameBuiltin: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())), + }, + NameTag: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + NameAttribute: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())), + }, + NameClass: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxType())), + }, + NameConstant: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())), + }, + NameDecorator: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())), + }, + NameFunction: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())), + }, + LiteralNumber: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())), + }, + LiteralString: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxString())), + }, + LiteralStringEscape: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())), + }, + GenericDeleted: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.DiffRemoved())), + }, + GenericEmph: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())), + Italic: boolPtr(true), + }, + GenericInserted: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.DiffAdded())), + }, + GenericStrong: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())), + Bold: boolPtr(true), + }, + GenericSubheading: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())), + }, + }, + }, + Table: ansi.StyleTable{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "\n", + BlockSuffix: "\n", + }, + }, + CenterSeparator: stringPtr("┼"), + ColumnSeparator: stringPtr("│"), + RowSeparator: stringPtr("─"), + }, + DefinitionDescription: ansi.StylePrimitive{ + BlockPrefix: "\n ❯ ", + Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())), + }, + Text: ansi.StylePrimitive{ + Color: stringPtr(adaptiveColorToString(t.MarkdownText())), + }, + Paragraph: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - BackgroundColor: stringPtr(Background.Dark), + Color: stringPtr(adaptiveColorToString(t.MarkdownText())), }, - IndentToken: stringPtr(BaseStyle.Render(" ")), }, - }, - DefinitionDescription: ansi.StylePrimitive{ - BlockPrefix: "\n* ", - BackgroundColor: stringPtr(Background.Dark), - }, + } +} + +// adaptiveColorToString converts a lipgloss.AdaptiveColor to the appropriate +// hex color string based on the current terminal background +func adaptiveColorToString(color lipgloss.AdaptiveColor) string { + if lipgloss.HasDarkBackground() { + return color.Dark + } + return color.Light } diff --git a/internal/tui/styles/styles.go b/internal/tui/styles/styles.go index 476339b57a72157de2dd2035fe89a8b11fe56860..1d6cf80d5236f4bd2b39cade0522bafa2a18a98a 100644 --- a/internal/tui/styles/styles.go +++ b/internal/tui/styles/styles.go @@ -1,177 +1,152 @@ package styles import ( - catppuccin "github.com/catppuccin/go" "github.com/charmbracelet/lipgloss" + "github.com/opencode-ai/opencode/internal/tui/theme" ) -var ( - light = catppuccin.Latte - dark = catppuccin.Mocha -) - -// NEW STYLES -var ( - Background = lipgloss.AdaptiveColor{ - Dark: "#212121", - Light: "#212121", - } - BackgroundDim = lipgloss.AdaptiveColor{ - Dark: "#2c2c2c", - Light: "#2c2c2c", - } - BackgroundDarker = lipgloss.AdaptiveColor{ - Dark: "#181818", - Light: "#181818", - } - BorderColor = lipgloss.AdaptiveColor{ - Dark: "#4b4c5c", - Light: "#4b4c5c", - } - - Forground = lipgloss.AdaptiveColor{ - Dark: "#d3d3d3", - Light: "#d3d3d3", - } - - ForgroundMid = lipgloss.AdaptiveColor{ - Dark: "#a0a0a0", - Light: "#a0a0a0", - } - - ForgroundDim = lipgloss.AdaptiveColor{ - Dark: "#737373", - Light: "#737373", - } - - BaseStyle = lipgloss.NewStyle(). - Background(Background). - Foreground(Forground) - - PrimaryColor = lipgloss.AdaptiveColor{ - Dark: "#fab283", - Light: "#fab283", - } -) +// Style generation functions that use the current theme + +// BaseStyle returns the base style with background and foreground colors +func BaseStyle() lipgloss.Style { + t := theme.CurrentTheme() + return lipgloss.NewStyle(). + Background(t.Background()). + Foreground(t.Text()) +} + +// Regular returns a basic unstyled lipgloss.Style +func Regular() lipgloss.Style { + return lipgloss.NewStyle() +} + +// Bold returns a bold style +func Bold() lipgloss.Style { + return Regular().Bold(true) +} + +// Padded returns a style with horizontal padding +func Padded() lipgloss.Style { + return Regular().Padding(0, 1) +} + +// Border returns a style with a normal border +func Border() lipgloss.Style { + t := theme.CurrentTheme() + return Regular(). + Border(lipgloss.NormalBorder()). + BorderForeground(t.BorderNormal()) +} + +// ThickBorder returns a style with a thick border +func ThickBorder() lipgloss.Style { + t := theme.CurrentTheme() + return Regular(). + Border(lipgloss.ThickBorder()). + BorderForeground(t.BorderNormal()) +} + +// DoubleBorder returns a style with a double border +func DoubleBorder() lipgloss.Style { + t := theme.CurrentTheme() + return Regular(). + Border(lipgloss.DoubleBorder()). + BorderForeground(t.BorderNormal()) +} + +// FocusedBorder returns a style with a border using the focused border color +func FocusedBorder() lipgloss.Style { + t := theme.CurrentTheme() + return Regular(). + Border(lipgloss.NormalBorder()). + BorderForeground(t.BorderFocused()) +} + +// DimBorder returns a style with a border using the dim border color +func DimBorder() lipgloss.Style { + t := theme.CurrentTheme() + return Regular(). + Border(lipgloss.NormalBorder()). + BorderForeground(t.BorderDim()) +} + +// PrimaryColor returns the primary color from the current theme +func PrimaryColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Primary() +} + +// SecondaryColor returns the secondary color from the current theme +func SecondaryColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Secondary() +} + +// AccentColor returns the accent color from the current theme +func AccentColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Accent() +} + +// ErrorColor returns the error color from the current theme +func ErrorColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Error() +} + +// WarningColor returns the warning color from the current theme +func WarningColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Warning() +} + +// SuccessColor returns the success color from the current theme +func SuccessColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Success() +} + +// InfoColor returns the info color from the current theme +func InfoColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Info() +} + +// TextColor returns the text color from the current theme +func TextColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Text() +} + +// TextMutedColor returns the muted text color from the current theme +func TextMutedColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().TextMuted() +} + +// TextEmphasizedColor returns the emphasized text color from the current theme +func TextEmphasizedColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().TextEmphasized() +} + +// BackgroundColor returns the background color from the current theme +func BackgroundColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().Background() +} + +// BackgroundSecondaryColor returns the secondary background color from the current theme +func BackgroundSecondaryColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().BackgroundSecondary() +} + +// BackgroundDarkerColor returns the darker background color from the current theme +func BackgroundDarkerColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().BackgroundDarker() +} + +// BorderNormalColor returns the normal border color from the current theme +func BorderNormalColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().BorderNormal() +} + +// BorderFocusedColor returns the focused border color from the current theme +func BorderFocusedColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().BorderFocused() +} + +// BorderDimColor returns the dim border color from the current theme +func BorderDimColor() lipgloss.AdaptiveColor { + return theme.CurrentTheme().BorderDim() +} -var ( - Regular = lipgloss.NewStyle() - Bold = Regular.Bold(true) - Padded = Regular.Padding(0, 1) - - Border = Regular.Border(lipgloss.NormalBorder()) - ThickBorder = Regular.Border(lipgloss.ThickBorder()) - DoubleBorder = Regular.Border(lipgloss.DoubleBorder()) - - // Colors - White = lipgloss.Color("#ffffff") - Surface0 = lipgloss.AdaptiveColor{ - Dark: dark.Surface0().Hex, - Light: light.Surface0().Hex, - } - - Overlay0 = lipgloss.AdaptiveColor{ - Dark: dark.Overlay0().Hex, - Light: light.Overlay0().Hex, - } - - Ovelay1 = lipgloss.AdaptiveColor{ - Dark: dark.Overlay1().Hex, - Light: light.Overlay1().Hex, - } - - Text = lipgloss.AdaptiveColor{ - Dark: dark.Text().Hex, - Light: light.Text().Hex, - } - - SubText0 = lipgloss.AdaptiveColor{ - Dark: dark.Subtext0().Hex, - Light: light.Subtext0().Hex, - } - - SubText1 = lipgloss.AdaptiveColor{ - Dark: dark.Subtext1().Hex, - Light: light.Subtext1().Hex, - } - - LightGrey = lipgloss.AdaptiveColor{ - Dark: dark.Surface0().Hex, - Light: light.Surface0().Hex, - } - Grey = lipgloss.AdaptiveColor{ - Dark: dark.Surface1().Hex, - Light: light.Surface1().Hex, - } - - DarkGrey = lipgloss.AdaptiveColor{ - Dark: dark.Surface2().Hex, - Light: light.Surface2().Hex, - } - - Base = lipgloss.AdaptiveColor{ - Dark: dark.Base().Hex, - Light: light.Base().Hex, - } - - Crust = lipgloss.AdaptiveColor{ - Dark: dark.Crust().Hex, - Light: light.Crust().Hex, - } - - Blue = lipgloss.AdaptiveColor{ - Dark: dark.Blue().Hex, - Light: light.Blue().Hex, - } - - Red = lipgloss.AdaptiveColor{ - Dark: dark.Red().Hex, - Light: light.Red().Hex, - } - - Green = lipgloss.AdaptiveColor{ - Dark: dark.Green().Hex, - Light: light.Green().Hex, - } - - Mauve = lipgloss.AdaptiveColor{ - Dark: dark.Mauve().Hex, - Light: light.Mauve().Hex, - } - - Teal = lipgloss.AdaptiveColor{ - Dark: dark.Teal().Hex, - Light: light.Teal().Hex, - } - - Rosewater = lipgloss.AdaptiveColor{ - Dark: dark.Rosewater().Hex, - Light: light.Rosewater().Hex, - } - - Flamingo = lipgloss.AdaptiveColor{ - Dark: dark.Flamingo().Hex, - Light: light.Flamingo().Hex, - } - - Lavender = lipgloss.AdaptiveColor{ - Dark: dark.Lavender().Hex, - Light: light.Lavender().Hex, - } - - Peach = lipgloss.AdaptiveColor{ - Dark: dark.Peach().Hex, - Light: light.Peach().Hex, - } - - Yellow = lipgloss.AdaptiveColor{ - Dark: dark.Yellow().Hex, - Light: light.Yellow().Hex, - } - - Primary = Blue - Secondary = Mauve - - Warning = Peach - Error = Red -) diff --git a/internal/tui/theme/catppuccin.go b/internal/tui/theme/catppuccin.go new file mode 100644 index 0000000000000000000000000000000000000000..a843100ab21c0b3b5b035b67a6322ef2ea5239ef --- /dev/null +++ b/internal/tui/theme/catppuccin.go @@ -0,0 +1,248 @@ +package theme + +import ( + catppuccin "github.com/catppuccin/go" + "github.com/charmbracelet/lipgloss" +) + +// CatppuccinTheme implements the Theme interface with Catppuccin colors. +// It provides both dark (Mocha) and light (Latte) variants. +type CatppuccinTheme struct { + BaseTheme +} + +// NewCatppuccinTheme creates a new instance of the Catppuccin theme. +func NewCatppuccinTheme() *CatppuccinTheme { + // Get the Catppuccin palettes + mocha := catppuccin.Mocha + latte := catppuccin.Latte + + theme := &CatppuccinTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: mocha.Blue().Hex, + Light: latte.Blue().Hex, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: mocha.Mauve().Hex, + Light: latte.Mauve().Hex, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: mocha.Peach().Hex, + Light: latte.Peach().Hex, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: mocha.Red().Hex, + Light: latte.Red().Hex, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: mocha.Peach().Hex, + Light: latte.Peach().Hex, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: mocha.Green().Hex, + Light: latte.Green().Hex, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: mocha.Blue().Hex, + Light: latte.Blue().Hex, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: mocha.Text().Hex, + Light: latte.Text().Hex, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: mocha.Subtext0().Hex, + Light: latte.Subtext0().Hex, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: mocha.Lavender().Hex, + Light: latte.Lavender().Hex, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: "#212121", // From existing styles + Light: "#EEEEEE", // Light equivalent + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: "#2c2c2c", // From existing styles + Light: "#E0E0E0", // Light equivalent + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#181818", // From existing styles + Light: "#F5F5F5", // Light equivalent + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: "#4b4c5c", // From existing styles + Light: "#BDBDBD", // Light equivalent + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: mocha.Blue().Hex, + Light: latte.Blue().Hex, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: mocha.Surface0().Hex, + Light: latte.Surface0().Hex, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: "#478247", // From existing diff.go + Light: "#2E7D32", // Light equivalent + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#7C4444", // From existing diff.go + Light: "#C62828", // Light equivalent + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", // From existing diff.go + Light: "#757575", // Light equivalent + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", // From existing diff.go + Light: "#757575", // Light equivalent + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#DAFADA", // From existing diff.go + Light: "#A5D6A7", // Light equivalent + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#FADADD", // From existing diff.go + Light: "#EF9A9A", // Light equivalent + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#303A30", // From existing diff.go + Light: "#E8F5E9", // Light equivalent + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3A3030", // From existing diff.go + Light: "#FFEBEE", // Light equivalent + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: "#212121", // From existing diff.go + Light: "#F5F5F5", // Light equivalent + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: "#888888", // From existing diff.go + Light: "#9E9E9E", // Light equivalent + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#293229", // From existing diff.go + Light: "#C8E6C9", // Light equivalent + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#332929", // From existing diff.go + Light: "#FFCDD2", // Light equivalent + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: mocha.Text().Hex, + Light: latte.Text().Hex, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: mocha.Mauve().Hex, + Light: latte.Mauve().Hex, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: mocha.Sky().Hex, + Light: latte.Sky().Hex, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: mocha.Pink().Hex, + Light: latte.Pink().Hex, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: mocha.Green().Hex, + Light: latte.Green().Hex, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: mocha.Yellow().Hex, + Light: latte.Yellow().Hex, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: mocha.Yellow().Hex, + Light: latte.Yellow().Hex, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: mocha.Peach().Hex, + Light: latte.Peach().Hex, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: mocha.Overlay0().Hex, + Light: latte.Overlay0().Hex, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: mocha.Blue().Hex, + Light: latte.Blue().Hex, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: mocha.Sky().Hex, + Light: latte.Sky().Hex, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: mocha.Sapphire().Hex, + Light: latte.Sapphire().Hex, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: mocha.Pink().Hex, + Light: latte.Pink().Hex, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: mocha.Text().Hex, + Light: latte.Text().Hex, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: mocha.Overlay1().Hex, + Light: latte.Overlay1().Hex, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: mocha.Pink().Hex, + Light: latte.Pink().Hex, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: mocha.Green().Hex, + Light: latte.Green().Hex, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: mocha.Sky().Hex, + Light: latte.Sky().Hex, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: mocha.Yellow().Hex, + Light: latte.Yellow().Hex, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: mocha.Teal().Hex, + Light: latte.Teal().Hex, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: mocha.Sky().Hex, + Light: latte.Sky().Hex, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: mocha.Pink().Hex, + Light: latte.Pink().Hex, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: mocha.Text().Hex, + Light: latte.Text().Hex, + } + + return theme +} + +func init() { + // Register the Catppuccin theme with the theme manager + RegisterTheme("catppuccin", NewCatppuccinTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/dracula.go b/internal/tui/theme/dracula.go new file mode 100644 index 0000000000000000000000000000000000000000..e625206ae5e0470081244fa306443bbdd81a0b93 --- /dev/null +++ b/internal/tui/theme/dracula.go @@ -0,0 +1,274 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// DraculaTheme implements the Theme interface with Dracula colors. +// It provides both dark and light variants, though Dracula is primarily a dark theme. +type DraculaTheme struct { + BaseTheme +} + +// NewDraculaTheme creates a new instance of the Dracula theme. +func NewDraculaTheme() *DraculaTheme { + // Dracula color palette + // Official colors from https://draculatheme.com/ + darkBackground := "#282a36" + darkCurrentLine := "#44475a" + darkSelection := "#44475a" + darkForeground := "#f8f8f2" + darkComment := "#6272a4" + darkCyan := "#8be9fd" + darkGreen := "#50fa7b" + darkOrange := "#ffb86c" + darkPink := "#ff79c6" + darkPurple := "#bd93f9" + darkRed := "#ff5555" + darkYellow := "#f1fa8c" + darkBorder := "#44475a" + + // Light mode approximation (Dracula is primarily a dark theme) + lightBackground := "#f8f8f2" + lightCurrentLine := "#e6e6e6" + lightSelection := "#d8d8d8" + lightForeground := "#282a36" + lightComment := "#6272a4" + lightCyan := "#0097a7" + lightGreen := "#388e3c" + lightOrange := "#f57c00" + lightPink := "#d81b60" + lightPurple := "#7e57c2" + lightRed := "#e53935" + lightYellow := "#fbc02d" + lightBorder := "#d8d8d8" + + theme := &DraculaTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkPink, + Light: lightPink, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#21222c", // Slightly darker than background + Light: "#ffffff", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#50fa7b", + Light: "#a5d6a7", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#ff5555", + Light: "#ef9a9a", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#2c3b2c", + Light: "#e8f5e9", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3b2c2c", + Light: "#ffebee", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#253025", + Light: "#c8e6c9", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#302525", + Light: "#ffcdd2", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkPink, + Light: lightPink, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkPink, + Light: lightPink, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkPink, + Light: lightPink, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the Dracula theme with the theme manager + RegisterTheme("dracula", NewDraculaTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/flexoki.go b/internal/tui/theme/flexoki.go new file mode 100644 index 0000000000000000000000000000000000000000..49d94beb15656775d7f79679391aa6589fb18473 --- /dev/null +++ b/internal/tui/theme/flexoki.go @@ -0,0 +1,282 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// Flexoki color palette constants +const ( + // Base colors + flexokiPaper = "#FFFCF0" // Paper (lightest) + flexokiBase50 = "#F2F0E5" // bg-2 (light) + flexokiBase100 = "#E6E4D9" // ui (light) + flexokiBase150 = "#DAD8CE" // ui-2 (light) + flexokiBase200 = "#CECDC3" // ui-3 (light) + flexokiBase300 = "#B7B5AC" // tx-3 (light) + flexokiBase500 = "#878580" // tx-2 (light) + flexokiBase600 = "#6F6E69" // tx (light) + flexokiBase700 = "#575653" // tx-3 (dark) + flexokiBase800 = "#403E3C" // ui-3 (dark) + flexokiBase850 = "#343331" // ui-2 (dark) + flexokiBase900 = "#282726" // ui (dark) + flexokiBase950 = "#1C1B1A" // bg-2 (dark) + flexokiBlack = "#100F0F" // bg (darkest) + + // Accent colors - Light theme (600) + flexokiRed600 = "#AF3029" + flexokiOrange600 = "#BC5215" + flexokiYellow600 = "#AD8301" + flexokiGreen600 = "#66800B" + flexokiCyan600 = "#24837B" + flexokiBlue600 = "#205EA6" + flexokiPurple600 = "#5E409D" + flexokiMagenta600 = "#A02F6F" + + // Accent colors - Dark theme (400) + flexokiRed400 = "#D14D41" + flexokiOrange400 = "#DA702C" + flexokiYellow400 = "#D0A215" + flexokiGreen400 = "#879A39" + flexokiCyan400 = "#3AA99F" + flexokiBlue400 = "#4385BE" + flexokiPurple400 = "#8B7EC8" + flexokiMagenta400 = "#CE5D97" +) + +// FlexokiTheme implements the Theme interface with Flexoki colors. +// It provides both dark and light variants. +type FlexokiTheme struct { + BaseTheme +} + +// NewFlexokiTheme creates a new instance of the Flexoki theme. +func NewFlexokiTheme() *FlexokiTheme { + theme := &FlexokiTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlue400, + Light: flexokiBlue600, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: flexokiPurple400, + Light: flexokiPurple600, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: flexokiOrange400, + Light: flexokiOrange600, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: flexokiRed400, + Light: flexokiRed600, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: flexokiYellow400, + Light: flexokiYellow600, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: flexokiGreen400, + Light: flexokiGreen600, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: flexokiCyan400, + Light: flexokiCyan600, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase300, + Light: flexokiBase600, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase700, + Light: flexokiBase500, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: flexokiYellow400, + Light: flexokiYellow600, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlack, + Light: flexokiPaper, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase950, + Light: flexokiBase50, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase900, + Light: flexokiBase100, + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase900, + Light: flexokiBase100, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlue400, + Light: flexokiBlue600, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase850, + Light: flexokiBase150, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: flexokiGreen400, + Light: flexokiGreen600, + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: flexokiRed400, + Light: flexokiRed600, + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase700, + Light: flexokiBase500, + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase700, + Light: flexokiBase500, + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: flexokiGreen400, + Light: flexokiGreen600, + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: flexokiRed400, + Light: flexokiRed600, + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#1D2419", // Darker green background + Light: "#EFF2E2", // Light green background + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#241919", // Darker red background + Light: "#F2E2E2", // Light red background + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlack, + Light: flexokiPaper, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase700, + Light: flexokiBase500, + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#1A2017", // Slightly darker green + Light: "#E5EBD9", // Light green + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#201717", // Slightly darker red + Light: "#EBD9D9", // Light red + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase300, + Light: flexokiBase600, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: flexokiYellow400, + Light: flexokiYellow600, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: flexokiCyan400, + Light: flexokiCyan600, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: flexokiMagenta400, + Light: flexokiMagenta600, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: flexokiGreen400, + Light: flexokiGreen600, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: flexokiCyan400, + Light: flexokiCyan600, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: flexokiYellow400, + Light: flexokiYellow600, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: flexokiOrange400, + Light: flexokiOrange600, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase800, + Light: flexokiBase200, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlue400, + Light: flexokiBlue600, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlue400, + Light: flexokiBlue600, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: flexokiPurple400, + Light: flexokiPurple600, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: flexokiMagenta400, + Light: flexokiMagenta600, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase300, + Light: flexokiBase600, + } + + // Syntax highlighting colors (based on Flexoki's mappings) + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase700, // tx-3 + Light: flexokiBase300, // tx-3 + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: flexokiGreen400, // gr + Light: flexokiGreen600, // gr + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: flexokiOrange400, // or + Light: flexokiOrange600, // or + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: flexokiBlue400, // bl + Light: flexokiBlue600, // bl + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: flexokiCyan400, // cy + Light: flexokiCyan600, // cy + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: flexokiPurple400, // pu + Light: flexokiPurple600, // pu + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: flexokiYellow400, // ye + Light: flexokiYellow600, // ye + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase500, // tx-2 + Light: flexokiBase500, // tx-2 + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: flexokiBase500, // tx-2 + Light: flexokiBase500, // tx-2 + } + + return theme +} + +func init() { + // Register the Flexoki theme with the theme manager + RegisterTheme("flexoki", NewFlexokiTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/gruvbox.go b/internal/tui/theme/gruvbox.go new file mode 100644 index 0000000000000000000000000000000000000000..ed544b84de80446bf0f90be40adbc35cd0ba0689 --- /dev/null +++ b/internal/tui/theme/gruvbox.go @@ -0,0 +1,302 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// Gruvbox color palette constants +const ( + // Dark theme colors + gruvboxDarkBg0 = "#282828" + gruvboxDarkBg0Soft = "#32302f" + gruvboxDarkBg1 = "#3c3836" + gruvboxDarkBg2 = "#504945" + gruvboxDarkBg3 = "#665c54" + gruvboxDarkBg4 = "#7c6f64" + gruvboxDarkFg0 = "#fbf1c7" + gruvboxDarkFg1 = "#ebdbb2" + gruvboxDarkFg2 = "#d5c4a1" + gruvboxDarkFg3 = "#bdae93" + gruvboxDarkFg4 = "#a89984" + gruvboxDarkGray = "#928374" + gruvboxDarkRed = "#cc241d" + gruvboxDarkRedBright = "#fb4934" + gruvboxDarkGreen = "#98971a" + gruvboxDarkGreenBright = "#b8bb26" + gruvboxDarkYellow = "#d79921" + gruvboxDarkYellowBright = "#fabd2f" + gruvboxDarkBlue = "#458588" + gruvboxDarkBlueBright = "#83a598" + gruvboxDarkPurple = "#b16286" + gruvboxDarkPurpleBright = "#d3869b" + gruvboxDarkAqua = "#689d6a" + gruvboxDarkAquaBright = "#8ec07c" + gruvboxDarkOrange = "#d65d0e" + gruvboxDarkOrangeBright = "#fe8019" + + // Light theme colors + gruvboxLightBg0 = "#fbf1c7" + gruvboxLightBg0Soft = "#f2e5bc" + gruvboxLightBg1 = "#ebdbb2" + gruvboxLightBg2 = "#d5c4a1" + gruvboxLightBg3 = "#bdae93" + gruvboxLightBg4 = "#a89984" + gruvboxLightFg0 = "#282828" + gruvboxLightFg1 = "#3c3836" + gruvboxLightFg2 = "#504945" + gruvboxLightFg3 = "#665c54" + gruvboxLightFg4 = "#7c6f64" + gruvboxLightGray = "#928374" + gruvboxLightRed = "#9d0006" + gruvboxLightRedBright = "#cc241d" + gruvboxLightGreen = "#79740e" + gruvboxLightGreenBright = "#98971a" + gruvboxLightYellow = "#b57614" + gruvboxLightYellowBright = "#d79921" + gruvboxLightBlue = "#076678" + gruvboxLightBlueBright = "#458588" + gruvboxLightPurple = "#8f3f71" + gruvboxLightPurpleBright = "#b16286" + gruvboxLightAqua = "#427b58" + gruvboxLightAquaBright = "#689d6a" + gruvboxLightOrange = "#af3a03" + gruvboxLightOrangeBright = "#d65d0e" +) + +// GruvboxTheme implements the Theme interface with Gruvbox colors. +// It provides both dark and light variants. +type GruvboxTheme struct { + BaseTheme +} + +// NewGruvboxTheme creates a new instance of the Gruvbox theme. +func NewGruvboxTheme() *GruvboxTheme { + theme := &GruvboxTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkPurpleBright, + Light: gruvboxLightPurpleBright, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkOrangeBright, + Light: gruvboxLightOrangeBright, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkRedBright, + Light: gruvboxLightRedBright, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellowBright, + Light: gruvboxLightYellowBright, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGreenBright, + Light: gruvboxLightGreenBright, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg1, + Light: gruvboxLightFg1, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg4, + Light: gruvboxLightFg4, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellowBright, + Light: gruvboxLightYellowBright, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg0, + Light: gruvboxLightBg0, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg1, + Light: gruvboxLightBg1, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg0Soft, + Light: gruvboxLightBg0Soft, + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg2, + Light: gruvboxLightBg2, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg1, + Light: gruvboxLightBg1, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGreenBright, + Light: gruvboxLightGreenBright, + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkRedBright, + Light: gruvboxLightRedBright, + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg4, + Light: gruvboxLightFg4, + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg3, + Light: gruvboxLightFg3, + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGreenBright, + Light: gruvboxLightGreenBright, + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkRedBright, + Light: gruvboxLightRedBright, + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3C4C3C", // Darker green background + Light: "#E8F5E9", // Light green background + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#4C3C3C", // Darker red background + Light: "#FFEBEE", // Light red background + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg0, + Light: gruvboxLightBg0, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg4, + Light: gruvboxLightFg4, + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#32432F", // Slightly darker green + Light: "#C8E6C9", // Light green + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#43322F", // Slightly darker red + Light: "#FFCDD2", // Light red + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg1, + Light: gruvboxLightFg1, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellowBright, + Light: gruvboxLightYellowBright, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkAquaBright, + Light: gruvboxLightAquaBright, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGreenBright, + Light: gruvboxLightGreenBright, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkAquaBright, + Light: gruvboxLightAquaBright, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellowBright, + Light: gruvboxLightYellowBright, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkOrangeBright, + Light: gruvboxLightOrangeBright, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBg3, + Light: gruvboxLightBg3, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkPurpleBright, + Light: gruvboxLightPurpleBright, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkAquaBright, + Light: gruvboxLightAquaBright, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg1, + Light: gruvboxLightFg1, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGray, + Light: gruvboxLightGray, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkRedBright, + Light: gruvboxLightRedBright, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkGreenBright, + Light: gruvboxLightGreenBright, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkBlueBright, + Light: gruvboxLightBlueBright, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellowBright, + Light: gruvboxLightYellowBright, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkPurpleBright, + Light: gruvboxLightPurpleBright, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkYellow, + Light: gruvboxLightYellow, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkAquaBright, + Light: gruvboxLightAquaBright, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: gruvboxDarkFg1, + Light: gruvboxLightFg1, + } + + return theme +} + +func init() { + // Register the Gruvbox theme with the theme manager + RegisterTheme("gruvbox", NewGruvboxTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/manager.go b/internal/tui/theme/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..a81ba45c12db73823f41fa416778895046dd5ec7 --- /dev/null +++ b/internal/tui/theme/manager.go @@ -0,0 +1,118 @@ +package theme + +import ( + "fmt" + "slices" + "strings" + "sync" + + "github.com/alecthomas/chroma/v2/styles" + "github.com/opencode-ai/opencode/internal/config" + "github.com/opencode-ai/opencode/internal/logging" +) + +// 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 +} + +// Global instance of the theme manager +var globalManager = &Manager{ + themes: make(map[string]Theme), + currentName: "", +} + +// RegisterTheme adds a new theme to the registry. +// If this is the first theme registered, it becomes the default. +func RegisterTheme(name string, theme Theme) { + globalManager.mu.Lock() + defer globalManager.mu.Unlock() + + globalManager.themes[name] = theme + + // If this is the first theme, make it the default + if globalManager.currentName == "" { + globalManager.currentName = name + } +} + +// SetTheme changes the active theme to the one with the specified name. +// Returns an error if the theme doesn't exist. +func SetTheme(name string) error { + globalManager.mu.Lock() + defer globalManager.mu.Unlock() + + delete(styles.Registry, "charm") + if _, exists := globalManager.themes[name]; !exists { + return fmt.Errorf("theme '%s' not found", name) + } + + globalManager.currentName = name + + // Update the config file using viper + if err := updateConfigTheme(name); err != nil { + // Log the error but don't fail the theme change + logging.Warn("Warning: Failed to update config file with new theme", "err", err) + } + + return nil +} + +// CurrentTheme returns the currently active theme. +// If no theme is set, it returns nil. +func CurrentTheme() Theme { + globalManager.mu.RLock() + defer globalManager.mu.RUnlock() + + if globalManager.currentName == "" { + return nil + } + + return globalManager.themes[globalManager.currentName] +} + +// CurrentThemeName returns the name of the currently active theme. +func CurrentThemeName() string { + globalManager.mu.RLock() + defer globalManager.mu.RUnlock() + + return globalManager.currentName +} + +// AvailableThemes returns a list of all registered theme names. +func AvailableThemes() []string { + globalManager.mu.RLock() + defer globalManager.mu.RUnlock() + + names := make([]string, 0, len(globalManager.themes)) + for name := range globalManager.themes { + names = append(names, name) + } + slices.SortFunc(names, func(a, b string) int { + if a == "opencode" { + return -1 + } else if b == "opencode" { + return 1 + } + return strings.Compare(a, b) + }) + return names +} + +// GetTheme returns a specific theme by name. +// Returns nil if the theme doesn't exist. +func GetTheme(name string) Theme { + globalManager.mu.RLock() + defer globalManager.mu.RUnlock() + + return globalManager.themes[name] +} + +// updateConfigTheme updates the theme setting in the configuration file +func updateConfigTheme(themeName string) error { + // Use the config package to update the theme + return config.UpdateTheme(themeName) +} diff --git a/internal/tui/theme/monokai.go b/internal/tui/theme/monokai.go new file mode 100644 index 0000000000000000000000000000000000000000..4695fefa998f0e038442b1bea3074f5a8a808a0e --- /dev/null +++ b/internal/tui/theme/monokai.go @@ -0,0 +1,273 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// MonokaiProTheme implements the Theme interface with Monokai Pro colors. +// It provides both dark and light variants. +type MonokaiProTheme struct { + BaseTheme +} + +// NewMonokaiProTheme creates a new instance of the Monokai Pro theme. +func NewMonokaiProTheme() *MonokaiProTheme { + // Monokai Pro color palette (dark mode) + darkBackground := "#2d2a2e" + darkCurrentLine := "#403e41" + darkSelection := "#5b595c" + darkForeground := "#fcfcfa" + darkComment := "#727072" + darkRed := "#ff6188" + darkOrange := "#fc9867" + darkYellow := "#ffd866" + darkGreen := "#a9dc76" + darkCyan := "#78dce8" + darkBlue := "#ab9df2" + darkPurple := "#ab9df2" + darkBorder := "#403e41" + + // Light mode colors (adapted from dark) + lightBackground := "#fafafa" + lightCurrentLine := "#f0f0f0" + lightSelection := "#e5e5e6" + lightForeground := "#2d2a2e" + lightComment := "#939293" + lightRed := "#f92672" + lightOrange := "#fd971f" + lightYellow := "#e6db74" + lightGreen := "#9bca65" + lightCyan := "#66d9ef" + lightBlue := "#7e75db" + lightPurple := "#ae81ff" + lightBorder := "#d3d3d3" + + theme := &MonokaiProTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#221f22", // Slightly darker than background + Light: "#ffffff", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: "#a9dc76", + Light: "#9bca65", + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#ff6188", + Light: "#f92672", + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#c2e7a9", + Light: "#c5e0b4", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#ff8ca6", + Light: "#ffb3c8", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3a4a35", + Light: "#e8f5e9", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#4a3439", + Light: "#ffebee", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: "#888888", + Light: "#9e9e9e", + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#2d3a28", + Light: "#c8e6c9", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#3d2a2e", + Light: "#ffcdd2", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the Monokai Pro theme with the theme manager + RegisterTheme("monokai", NewMonokaiProTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/onedark.go b/internal/tui/theme/onedark.go new file mode 100644 index 0000000000000000000000000000000000000000..2b4dee50dccdd9639e0245c3d4eaaabb798b0cfe --- /dev/null +++ b/internal/tui/theme/onedark.go @@ -0,0 +1,274 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// OneDarkTheme implements the Theme interface with Atom's One Dark colors. +// It provides both dark and light variants. +type OneDarkTheme struct { + BaseTheme +} + +// NewOneDarkTheme creates a new instance of the One Dark theme. +func NewOneDarkTheme() *OneDarkTheme { + // One Dark color palette + // Dark mode colors from Atom One Dark + darkBackground := "#282c34" + darkCurrentLine := "#2c313c" + darkSelection := "#3e4451" + darkForeground := "#abb2bf" + darkComment := "#5c6370" + darkRed := "#e06c75" + darkOrange := "#d19a66" + darkYellow := "#e5c07b" + darkGreen := "#98c379" + darkCyan := "#56b6c2" + darkBlue := "#61afef" + darkPurple := "#c678dd" + darkBorder := "#3b4048" + + // Light mode colors from Atom One Light + lightBackground := "#fafafa" + lightCurrentLine := "#f0f0f0" + lightSelection := "#e5e5e6" + lightForeground := "#383a42" + lightComment := "#a0a1a7" + lightRed := "#e45649" + lightOrange := "#da8548" + lightYellow := "#c18401" + lightGreen := "#50a14f" + lightCyan := "#0184bc" + lightBlue := "#4078f2" + lightPurple := "#a626a4" + lightBorder := "#d3d3d3" + + theme := &OneDarkTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#21252b", // Slightly darker than background + Light: "#ffffff", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: "#478247", + Light: "#2E7D32", + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#7C4444", + Light: "#C62828", + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#DAFADA", + Light: "#A5D6A7", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#FADADD", + Light: "#EF9A9A", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#303A30", + Light: "#E8F5E9", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3A3030", + Light: "#FFEBEE", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: "#888888", + Light: "#9E9E9E", + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#293229", + Light: "#C8E6C9", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#332929", + Light: "#FFCDD2", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the One Dark theme with the theme manager + RegisterTheme("onedark", NewOneDarkTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/opencode.go b/internal/tui/theme/opencode.go new file mode 100644 index 0000000000000000000000000000000000000000..efec8615437eef1516582b2492833cfa16ffe4d8 --- /dev/null +++ b/internal/tui/theme/opencode.go @@ -0,0 +1,277 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// OpenCodeTheme implements the Theme interface with OpenCode brand colors. +// It provides both dark and light variants. +type OpenCodeTheme struct { + BaseTheme +} + +// NewOpenCodeTheme creates a new instance of the OpenCode theme. +func NewOpenCodeTheme() *OpenCodeTheme { + // OpenCode color palette + // Dark mode colors + darkBackground := "#212121" + darkCurrentLine := "#252525" + darkSelection := "#303030" + darkForeground := "#e0e0e0" + darkComment := "#6a6a6a" + darkPrimary := "#fab283" // Primary orange/gold + darkSecondary := "#5c9cf5" // Secondary blue + darkAccent := "#9d7cd8" // Accent purple + darkRed := "#e06c75" // Error red + darkOrange := "#f5a742" // Warning orange + darkGreen := "#7fd88f" // Success green + darkCyan := "#56b6c2" // Info cyan + darkYellow := "#e5c07b" // Emphasized text + darkBorder := "#4b4c5c" // Border color + + // Light mode colors + lightBackground := "#f8f8f8" + lightCurrentLine := "#f0f0f0" + lightSelection := "#e5e5e6" + lightForeground := "#2a2a2a" + lightComment := "#8a8a8a" + lightPrimary := "#3b7dd8" // Primary blue + lightSecondary := "#7b5bb6" // Secondary purple + lightAccent := "#d68c27" // Accent orange/gold + lightRed := "#d1383d" // Error red + lightOrange := "#d68c27" // Warning orange + lightGreen := "#3d9a57" // Success green + lightCyan := "#318795" // Info cyan + lightYellow := "#b0851f" // Emphasized text + lightBorder := "#d3d3d3" // Border color + + theme := &OpenCodeTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkSecondary, + Light: lightSecondary, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkAccent, + Light: lightAccent, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#121212", // Slightly darker than background + Light: "#ffffff", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: "#478247", + Light: "#2E7D32", + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#7C4444", + Light: "#C62828", + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: "#a0a0a0", + Light: "#757575", + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#DAFADA", + Light: "#A5D6A7", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#FADADD", + Light: "#EF9A9A", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#303A30", + Light: "#E8F5E9", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#3A3030", + Light: "#FFEBEE", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: "#888888", + Light: "#9E9E9E", + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#293229", + Light: "#C8E6C9", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#332929", + Light: "#FFCDD2", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkSecondary, + Light: lightSecondary, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkAccent, + Light: lightAccent, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkSecondary, + Light: lightSecondary, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkPrimary, + Light: lightPrimary, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkAccent, + Light: lightAccent, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the OpenCode theme with the theme manager + RegisterTheme("opencode", NewOpenCodeTheme()) +} + diff --git a/internal/tui/theme/theme.go b/internal/tui/theme/theme.go new file mode 100644 index 0000000000000000000000000000000000000000..4ee14a07f8f2247fd7129bae8fd373d4531adf98 --- /dev/null +++ b/internal/tui/theme/theme.go @@ -0,0 +1,208 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// Theme defines the interface for all UI themes in the application. +// All colors must be defined as lipgloss.AdaptiveColor to support +// both light and dark terminal backgrounds. +type Theme interface { + // Base colors + Primary() lipgloss.AdaptiveColor + Secondary() lipgloss.AdaptiveColor + Accent() lipgloss.AdaptiveColor + + // Status colors + Error() lipgloss.AdaptiveColor + Warning() lipgloss.AdaptiveColor + Success() lipgloss.AdaptiveColor + Info() lipgloss.AdaptiveColor + + // Text colors + Text() lipgloss.AdaptiveColor + TextMuted() lipgloss.AdaptiveColor + TextEmphasized() lipgloss.AdaptiveColor + + // Background colors + Background() lipgloss.AdaptiveColor + BackgroundSecondary() lipgloss.AdaptiveColor + BackgroundDarker() lipgloss.AdaptiveColor + + // Border colors + BorderNormal() lipgloss.AdaptiveColor + BorderFocused() lipgloss.AdaptiveColor + BorderDim() lipgloss.AdaptiveColor + + // Diff view colors + DiffAdded() lipgloss.AdaptiveColor + DiffRemoved() lipgloss.AdaptiveColor + DiffContext() lipgloss.AdaptiveColor + DiffHunkHeader() lipgloss.AdaptiveColor + DiffHighlightAdded() lipgloss.AdaptiveColor + DiffHighlightRemoved() lipgloss.AdaptiveColor + DiffAddedBg() lipgloss.AdaptiveColor + DiffRemovedBg() lipgloss.AdaptiveColor + DiffContextBg() lipgloss.AdaptiveColor + DiffLineNumber() lipgloss.AdaptiveColor + DiffAddedLineNumberBg() lipgloss.AdaptiveColor + DiffRemovedLineNumberBg() lipgloss.AdaptiveColor + + // Markdown colors + MarkdownText() lipgloss.AdaptiveColor + MarkdownHeading() lipgloss.AdaptiveColor + MarkdownLink() lipgloss.AdaptiveColor + MarkdownLinkText() lipgloss.AdaptiveColor + MarkdownCode() lipgloss.AdaptiveColor + MarkdownBlockQuote() lipgloss.AdaptiveColor + MarkdownEmph() lipgloss.AdaptiveColor + MarkdownStrong() lipgloss.AdaptiveColor + MarkdownHorizontalRule() lipgloss.AdaptiveColor + MarkdownListItem() lipgloss.AdaptiveColor + MarkdownListEnumeration() lipgloss.AdaptiveColor + MarkdownImage() lipgloss.AdaptiveColor + MarkdownImageText() lipgloss.AdaptiveColor + MarkdownCodeBlock() lipgloss.AdaptiveColor + + // Syntax highlighting colors + SyntaxComment() lipgloss.AdaptiveColor + SyntaxKeyword() lipgloss.AdaptiveColor + SyntaxFunction() lipgloss.AdaptiveColor + SyntaxVariable() lipgloss.AdaptiveColor + SyntaxString() lipgloss.AdaptiveColor + SyntaxNumber() lipgloss.AdaptiveColor + SyntaxType() lipgloss.AdaptiveColor + SyntaxOperator() lipgloss.AdaptiveColor + SyntaxPunctuation() lipgloss.AdaptiveColor +} + +// BaseTheme provides a default implementation of the Theme interface +// that can be embedded in concrete theme implementations. +type BaseTheme struct { + // Base colors + PrimaryColor lipgloss.AdaptiveColor + SecondaryColor lipgloss.AdaptiveColor + AccentColor lipgloss.AdaptiveColor + + // Status colors + ErrorColor lipgloss.AdaptiveColor + WarningColor lipgloss.AdaptiveColor + SuccessColor lipgloss.AdaptiveColor + InfoColor lipgloss.AdaptiveColor + + // Text colors + TextColor lipgloss.AdaptiveColor + TextMutedColor lipgloss.AdaptiveColor + TextEmphasizedColor lipgloss.AdaptiveColor + + // Background colors + BackgroundColor lipgloss.AdaptiveColor + BackgroundSecondaryColor lipgloss.AdaptiveColor + BackgroundDarkerColor lipgloss.AdaptiveColor + + // Border colors + BorderNormalColor lipgloss.AdaptiveColor + BorderFocusedColor lipgloss.AdaptiveColor + BorderDimColor lipgloss.AdaptiveColor + + // Diff view colors + DiffAddedColor lipgloss.AdaptiveColor + DiffRemovedColor lipgloss.AdaptiveColor + DiffContextColor lipgloss.AdaptiveColor + DiffHunkHeaderColor lipgloss.AdaptiveColor + DiffHighlightAddedColor lipgloss.AdaptiveColor + DiffHighlightRemovedColor lipgloss.AdaptiveColor + DiffAddedBgColor lipgloss.AdaptiveColor + DiffRemovedBgColor lipgloss.AdaptiveColor + DiffContextBgColor lipgloss.AdaptiveColor + DiffLineNumberColor lipgloss.AdaptiveColor + DiffAddedLineNumberBgColor lipgloss.AdaptiveColor + DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor + + // Markdown colors + MarkdownTextColor lipgloss.AdaptiveColor + MarkdownHeadingColor lipgloss.AdaptiveColor + MarkdownLinkColor lipgloss.AdaptiveColor + MarkdownLinkTextColor lipgloss.AdaptiveColor + MarkdownCodeColor lipgloss.AdaptiveColor + MarkdownBlockQuoteColor lipgloss.AdaptiveColor + MarkdownEmphColor lipgloss.AdaptiveColor + MarkdownStrongColor lipgloss.AdaptiveColor + MarkdownHorizontalRuleColor lipgloss.AdaptiveColor + MarkdownListItemColor lipgloss.AdaptiveColor + MarkdownListEnumerationColor lipgloss.AdaptiveColor + MarkdownImageColor lipgloss.AdaptiveColor + MarkdownImageTextColor lipgloss.AdaptiveColor + MarkdownCodeBlockColor lipgloss.AdaptiveColor + + // Syntax highlighting colors + SyntaxCommentColor lipgloss.AdaptiveColor + SyntaxKeywordColor lipgloss.AdaptiveColor + SyntaxFunctionColor lipgloss.AdaptiveColor + SyntaxVariableColor lipgloss.AdaptiveColor + SyntaxStringColor lipgloss.AdaptiveColor + SyntaxNumberColor lipgloss.AdaptiveColor + SyntaxTypeColor lipgloss.AdaptiveColor + SyntaxOperatorColor lipgloss.AdaptiveColor + SyntaxPunctuationColor lipgloss.AdaptiveColor +} + +// Implement the Theme interface for BaseTheme +func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor } +func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor } +func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor } + +func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor } +func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor } +func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor } +func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor } + +func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor } +func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor } +func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor } + +func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor } +func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor } +func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor } + +func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor } +func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor } +func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor } + +func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor } +func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor } +func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor } +func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor } +func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor } +func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor } +func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor } +func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor } +func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor } +func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor } +func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffAddedLineNumberBgColor } +func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffRemovedLineNumberBgColor } + +func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor } +func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor } +func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor } +func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor } +func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor } +func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor } +func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor } +func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor } +func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { return t.MarkdownHorizontalRuleColor } +func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor } +func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { return t.MarkdownListEnumerationColor } +func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor } +func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor } +func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor } + +func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor } +func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor } +func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor } +func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor } +func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor } +func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor } +func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor } +func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor } +func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor } \ No newline at end of file diff --git a/internal/tui/theme/theme_test.go b/internal/tui/theme/theme_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5ec810e3377ebfeb1a1ef6d0b399e6baefd0e403 --- /dev/null +++ b/internal/tui/theme/theme_test.go @@ -0,0 +1,89 @@ +package theme + +import ( + "testing" +) + +func TestThemeRegistration(t *testing.T) { + // Get list of available themes + availableThemes := AvailableThemes() + + // Check if "catppuccin" theme is registered + catppuccinFound := false + for _, themeName := range availableThemes { + if themeName == "catppuccin" { + catppuccinFound = true + break + } + } + + if !catppuccinFound { + t.Errorf("Catppuccin theme is not registered") + } + + // Check if "gruvbox" theme is registered + gruvboxFound := false + for _, themeName := range availableThemes { + if themeName == "gruvbox" { + gruvboxFound = true + break + } + } + + if !gruvboxFound { + t.Errorf("Gruvbox theme is not registered") + } + + // Check if "monokai" theme is registered + monokaiFound := false + for _, themeName := range availableThemes { + if themeName == "monokai" { + monokaiFound = true + break + } + } + + if !monokaiFound { + t.Errorf("Monokai theme is not registered") + } + + // Try to get the themes and make sure they're not nil + catppuccin := GetTheme("catppuccin") + if catppuccin == nil { + t.Errorf("Catppuccin theme is nil") + } + + gruvbox := GetTheme("gruvbox") + if gruvbox == nil { + t.Errorf("Gruvbox theme is nil") + } + + monokai := GetTheme("monokai") + if monokai == nil { + t.Errorf("Monokai theme is nil") + } + + // Test switching theme + originalTheme := CurrentThemeName() + + err := SetTheme("gruvbox") + if err != nil { + t.Errorf("Failed to set theme to gruvbox: %v", err) + } + + if CurrentThemeName() != "gruvbox" { + t.Errorf("Theme not properly switched to gruvbox") + } + + err = SetTheme("monokai") + if err != nil { + t.Errorf("Failed to set theme to monokai: %v", err) + } + + if CurrentThemeName() != "monokai" { + t.Errorf("Theme not properly switched to monokai") + } + + // Switch back to original theme + _ = SetTheme(originalTheme) +} \ No newline at end of file diff --git a/internal/tui/theme/tokyonight.go b/internal/tui/theme/tokyonight.go new file mode 100644 index 0000000000000000000000000000000000000000..acd9dbf6c0311c9226d1c81e919df30364272b62 --- /dev/null +++ b/internal/tui/theme/tokyonight.go @@ -0,0 +1,274 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// TokyoNightTheme implements the Theme interface with Tokyo Night colors. +// It provides both dark and light variants. +type TokyoNightTheme struct { + BaseTheme +} + +// NewTokyoNightTheme creates a new instance of the Tokyo Night theme. +func NewTokyoNightTheme() *TokyoNightTheme { + // Tokyo Night color palette + // Dark mode colors + darkBackground := "#222436" + darkCurrentLine := "#1e2030" + darkSelection := "#2f334d" + darkForeground := "#c8d3f5" + darkComment := "#636da6" + darkRed := "#ff757f" + darkOrange := "#ff966c" + darkYellow := "#ffc777" + darkGreen := "#c3e88d" + darkCyan := "#86e1fc" + darkBlue := "#82aaff" + darkPurple := "#c099ff" + darkBorder := "#3b4261" + + // Light mode colors (Tokyo Night Day) + lightBackground := "#e1e2e7" + lightCurrentLine := "#d5d6db" + lightSelection := "#c8c9ce" + lightForeground := "#3760bf" + lightComment := "#848cb5" + lightRed := "#f52a65" + lightOrange := "#b15c00" + lightYellow := "#8c6c3e" + lightGreen := "#587539" + lightCyan := "#007197" + lightBlue := "#2e7de9" + lightPurple := "#9854f1" + lightBorder := "#a8aecb" + + theme := &TokyoNightTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#191B29", // Darker background from palette + Light: "#f0f0f5", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: "#4fd6be", // teal from palette + Light: "#1e725c", + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#c53b53", // red1 from palette + Light: "#c53b53", + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: "#828bb8", // fg_dark from palette + Light: "#7086b5", + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: "#828bb8", // fg_dark from palette + Light: "#7086b5", + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#b8db87", // git.add from palette + Light: "#4db380", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#e26a75", // git.delete from palette + Light: "#f52a65", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#20303b", + Light: "#d5e5d5", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#37222c", + Light: "#f7d8db", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: "#545c7e", // dark3 from palette + Light: "#848cb5", + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#1b2b34", + Light: "#c5d5c5", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#2d1f26", + Light: "#e7c8cb", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the Tokyo Night theme with the theme manager + RegisterTheme("tokyonight", NewTokyoNightTheme()) +} \ No newline at end of file diff --git a/internal/tui/theme/tron.go b/internal/tui/theme/tron.go new file mode 100644 index 0000000000000000000000000000000000000000..5f1bdfb0d5aa1594c82bfb0e22f506a9b53e171a --- /dev/null +++ b/internal/tui/theme/tron.go @@ -0,0 +1,276 @@ +package theme + +import ( + "github.com/charmbracelet/lipgloss" +) + +// TronTheme implements the Theme interface with Tron-inspired colors. +// It provides both dark and light variants, though Tron is primarily a dark theme. +type TronTheme struct { + BaseTheme +} + +// NewTronTheme creates a new instance of the Tron theme. +func NewTronTheme() *TronTheme { + // Tron color palette + // Inspired by the Tron movie's neon aesthetic + darkBackground := "#0c141f" + darkCurrentLine := "#1a2633" + darkSelection := "#1a2633" + darkForeground := "#caf0ff" + darkComment := "#4d6b87" + darkCyan := "#00d9ff" + darkBlue := "#007fff" + darkOrange := "#ff9000" + darkPink := "#ff00a0" + darkPurple := "#b73fff" + darkRed := "#ff3333" + darkYellow := "#ffcc00" + darkGreen := "#00ff8f" + darkBorder := "#1a2633" + + // Light mode approximation + lightBackground := "#f0f8ff" + lightCurrentLine := "#e0f0ff" + lightSelection := "#d0e8ff" + lightForeground := "#0c141f" + lightComment := "#4d6b87" + lightCyan := "#0097b3" + lightBlue := "#0066cc" + lightOrange := "#cc7300" + lightPink := "#cc0080" + lightPurple := "#9932cc" + lightRed := "#cc2929" + lightYellow := "#cc9900" + lightGreen := "#00cc72" + lightBorder := "#d0e8ff" + + theme := &TronTheme{} + + // Base colors + theme.PrimaryColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.AccentColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + + // Status colors + theme.ErrorColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.WarningColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SuccessColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.InfoColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + + // Text colors + theme.TextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.TextMutedColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.TextEmphasizedColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + + // Background colors + theme.BackgroundColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{ + Dark: darkCurrentLine, + Light: lightCurrentLine, + } + theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{ + Dark: "#070d14", // Slightly darker than background + Light: "#ffffff", // Slightly lighter than background + } + + // Border colors + theme.BorderNormalColor = lipgloss.AdaptiveColor{ + Dark: darkBorder, + Light: lightBorder, + } + theme.BorderFocusedColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.BorderDimColor = lipgloss.AdaptiveColor{ + Dark: darkSelection, + Light: lightSelection, + } + + // Diff view colors + theme.DiffAddedColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.DiffRemovedColor = lipgloss.AdaptiveColor{ + Dark: darkRed, + Light: lightRed, + } + theme.DiffContextColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{ + Dark: "#00ff8f", + Light: "#a5d6a7", + } + theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{ + Dark: "#ff3333", + Light: "#ef9a9a", + } + theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ + Dark: "#0a2a1a", + Light: "#e8f5e9", + } + theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ + Dark: "#2a0a0a", + Light: "#ffebee", + } + theme.DiffContextBgColor = lipgloss.AdaptiveColor{ + Dark: darkBackground, + Light: lightBackground, + } + theme.DiffLineNumberColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#082015", + Light: "#c8e6c9", + } + theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ + Dark: "#200808", + Light: "#ffcdd2", + } + + // Markdown colors + theme.MarkdownTextColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownLinkColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownEmphColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.MarkdownStrongColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.MarkdownListItemColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownImageColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + // Syntax highlighting colors + theme.SyntaxCommentColor = lipgloss.AdaptiveColor{ + Dark: darkComment, + Light: lightComment, + } + theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{ + Dark: darkCyan, + Light: lightCyan, + } + theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{ + Dark: darkGreen, + Light: lightGreen, + } + theme.SyntaxVariableColor = lipgloss.AdaptiveColor{ + Dark: darkOrange, + Light: lightOrange, + } + theme.SyntaxStringColor = lipgloss.AdaptiveColor{ + Dark: darkYellow, + Light: lightYellow, + } + theme.SyntaxNumberColor = lipgloss.AdaptiveColor{ + Dark: darkBlue, + Light: lightBlue, + } + theme.SyntaxTypeColor = lipgloss.AdaptiveColor{ + Dark: darkPurple, + Light: lightPurple, + } + theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{ + Dark: darkPink, + Light: lightPink, + } + theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{ + Dark: darkForeground, + Light: lightForeground, + } + + return theme +} + +func init() { + // Register the Tron theme with the theme manager + RegisterTheme("tron", NewTronTheme()) +} \ No newline at end of file diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 186f812c4fa876bd6698f9e77c9821fae8c0d937..79c9efe50ad2fc8430de5a1da0cd201801bd02b3 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -27,6 +27,7 @@ type keyMap struct { SwitchSession key.Binding Commands key.Binding Models key.Binding + SwitchTheme key.Binding } var keys = keyMap{ @@ -58,6 +59,11 @@ var keys = keyMap{ key.WithKeys("ctrl+o"), key.WithHelp("ctrl+o", "model selection"), ), + + SwitchTheme: key.NewBinding( + key.WithKeys("ctrl+t"), + key.WithHelp("ctrl+t", "switch theme"), + ), } var helpEsc = key.NewBinding( @@ -105,6 +111,9 @@ type appModel struct { showInitDialog bool initDialog dialog.InitDialogCmp + + showThemeDialog bool + themeDialog dialog.ThemeDialog } func (a appModel) Init() tea.Cmd { @@ -126,6 +135,8 @@ func (a appModel) Init() tea.Cmd { cmds = append(cmds, cmd) cmd = a.initDialog.Init() cmds = append(cmds, cmd) + cmd = a.themeDialog.Init() + cmds = append(cmds, cmd) // Check if we should show the init dialog cmds = append(cmds, func() tea.Msg { @@ -255,6 +266,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showCommandDialog = false return a, nil + case dialog.CloseThemeDialogMsg: + a.showThemeDialog = false + return a, nil + + case dialog.ThemeChangedMsg: + a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg) + a.showThemeDialog = false + return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName)) + case dialog.CloseModelDialogMsg: a.showModelDialog = false return a, nil @@ -344,7 +364,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return a, nil case key.Matches(msg, keys.Commands): - if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog { + if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog { // Show commands dialog if len(a.commands) == 0 { return a, util.ReportWarn("No commands available") @@ -359,12 +379,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showModelDialog = false return a, nil } - if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog { a.showModelDialog = true return a, nil } return a, nil + case key.Matches(msg, keys.SwitchTheme): + if !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog { + // Show theme switcher dialog + a.showThemeDialog = true + // Theme list is dynamically loaded by the dialog component + return a, a.themeDialog.Init() + } + return a, nil case key.Matches(msg, logsKeyReturnKey): if a.currentPage == page.LogsPage { return a, a.moveToPage(page.ChatPage) @@ -465,6 +492,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if a.showThemeDialog { + d, themeCmd := a.themeDialog.Update(msg) + a.themeDialog = d.(dialog.ThemeDialog) + cmds = append(cmds, themeCmd) + // Only block key messages send all other messages down + if _, ok := msg.(tea.KeyMsg); ok { + return a, tea.Batch(cmds...) + } + } + s, _ := a.status.Update(msg) a.status = s.(core.StatusCmp) a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg) @@ -523,9 +560,9 @@ func (a appModel) View() string { } if !a.app.CoderAgent.IsBusy() { - a.status.SetHelpMsg("ctrl+? help") + a.status.SetHelpWidgetMsg("ctrl+? help") } else { - a.status.SetHelpMsg("? help") + a.status.SetHelpWidgetMsg("? help") } if a.showHelp { @@ -629,6 +666,21 @@ func (a appModel) View() string { ) } + if a.showThemeDialog { + overlay := a.themeDialog.View() + row := lipgloss.Height(appView) / 2 + row -= lipgloss.Height(overlay) / 2 + col := lipgloss.Width(appView) / 2 + col -= lipgloss.Width(overlay) / 2 + appView = layout.PlaceOverlay( + col, + row, + overlay, + appView, + true, + ) + } + return appView } @@ -645,6 +697,7 @@ func New(app *app.App) tea.Model { modelDialog: dialog.NewModelDialogCmp(), permissions: dialog.NewPermissionDialogCmp(), initDialog: dialog.NewInitDialogCmp(), + themeDialog: dialog.NewThemeDialogCmp(), app: app, commands: []dialog.Command{}, pages: map[page.PageID]tea.Model{ diff --git a/opencode-schema.json b/opencode-schema.json index 7d1dde213fe1368287a12fb0c6f45c62abd562b0..7c7513d11c99e28ecdd8b2573bb9d0d30ddabb1d 100644 --- a/opencode-schema.json +++ b/opencode-schema.json @@ -12,75 +12,79 @@ "model": { "description": "Model ID for the agent", "enum": [ + "gpt-4o-mini", + "o1-pro", + "azure.gpt-4o-mini", + "openrouter.gpt-4.1-mini", + "openrouter.o1-mini", "bedrock.claude-3.7-sonnet", - "claude-3-haiku", - "claude-3.7-sonnet", - "claude-3.5-haiku", - "o3", - "azure.o3", + "meta-llama/llama-4-scout-17b-16e-instruct", + "openrouter.gpt-4o-mini", + "gemini-2.0-flash", + "deepseek-r1-distill-llama-70b", + "openrouter.claude-3.7-sonnet", + "openrouter.gpt-4.5-preview", + "azure.o3-mini", + "openrouter.claude-3.5-haiku", + "azure.o1-mini", + "openrouter.o1", + "openrouter.gemini-2.5", + "llama-3.3-70b-versatile", "gpt-4.5-preview", - "azure.gpt-4.5-preview", - "o1-pro", + "openrouter.claude-3-opus", + "openrouter.claude-3.5-sonnet", "o4-mini", - "azure.o4-mini", + "gemini-2.0-flash-lite", + "azure.gpt-4.5-preview", + "openrouter.gpt-4o", + "o1", + "azure.gpt-4o", + "openrouter.gpt-4.1-nano", + "o3", "gpt-4.1", - "azure.gpt-4.1", + "azure.o1", + "claude-3-haiku", + "claude-3-opus", + "gpt-4.1-mini", + "openrouter.o4-mini", + "openrouter.gemini-2.5-flash", + "claude-3.5-haiku", "o3-mini", - "azure.o3-mini", + "azure.o3", + "gpt-4o", + "azure.gpt-4.1", + "openrouter.claude-3-haiku", "gpt-4.1-nano", "azure.gpt-4.1-nano", - "gpt-4o-mini", - "azure.gpt-4o-mini", - "o1", - "azure.o1", - "gemini-2.5-flash", + "claude-3.7-sonnet", + "gemini-2.5", + "azure.o4-mini", + "o1-mini", "qwen-qwq", "meta-llama/llama-4-maverick-17b-128e-instruct", - "claude-3-opus", - "gpt-4o", - "azure.gpt-4o", - "gemini-2.0-flash-lite", - "gemini-2.0-flash", - "deepseek-r1-distill-llama-70b", - "llama-3.3-70b-versatile", - "claude-3.5-sonnet", - "o1-mini", - "azure.o1-mini", - "gpt-4.1-mini", - "azure.gpt-4.1-mini", - "gemini-2.5", - "meta-llama/llama-4-scout-17b-16e-instruct", - "openrouter.deepseek-chat-free", - "openrouter.deepseek-r1-free", "openrouter.gpt-4.1", - "openrouter.gpt-4.1-mini", - "openrouter.gpt-4.1-nano", - "openrouter.gpt-4.5-preview", - "openrouter.gpt-4o", - "openrouter.gpt-4o-mini", - "openrouter.o1", "openrouter.o1-pro", - "openrouter.o1-mini", "openrouter.o3", - "openrouter.o3-mini", - "openrouter.o4-mini", - "openrouter.gemini-2.5-flash", - "openrouter.gemini-2.5", - "openrouter.claude-3.5-sonnet", - "openrouter.claude-3-haiku", - "openrouter.claude-3.7-sonnet", - "openrouter.claude-3.5-haiku", - "openrouter.claude-3-opus" + "claude-3.5-sonnet", + "gemini-2.5-flash", + "azure.gpt-4.1-mini", + "openrouter.o3-mini" ], "type": "string" }, "reasoningEffort": { "description": "Reasoning effort for models that support it (OpenAI, Anthropic)", - "enum": ["low", "medium", "high"], + "enum": [ + "low", + "medium", + "high" + ], "type": "string" } }, - "required": ["model"], + "required": [ + "model" + ], "type": "object" } }, @@ -98,75 +102,79 @@ "model": { "description": "Model ID for the agent", "enum": [ + "gpt-4o-mini", + "o1-pro", + "azure.gpt-4o-mini", + "openrouter.gpt-4.1-mini", + "openrouter.o1-mini", "bedrock.claude-3.7-sonnet", - "claude-3-haiku", - "claude-3.7-sonnet", - "claude-3.5-haiku", - "o3", - "azure.o3", + "meta-llama/llama-4-scout-17b-16e-instruct", + "openrouter.gpt-4o-mini", + "gemini-2.0-flash", + "deepseek-r1-distill-llama-70b", + "openrouter.claude-3.7-sonnet", + "openrouter.gpt-4.5-preview", + "azure.o3-mini", + "openrouter.claude-3.5-haiku", + "azure.o1-mini", + "openrouter.o1", + "openrouter.gemini-2.5", + "llama-3.3-70b-versatile", "gpt-4.5-preview", - "azure.gpt-4.5-preview", - "o1-pro", + "openrouter.claude-3-opus", + "openrouter.claude-3.5-sonnet", "o4-mini", - "azure.o4-mini", + "gemini-2.0-flash-lite", + "azure.gpt-4.5-preview", + "openrouter.gpt-4o", + "o1", + "azure.gpt-4o", + "openrouter.gpt-4.1-nano", + "o3", "gpt-4.1", - "azure.gpt-4.1", + "azure.o1", + "claude-3-haiku", + "claude-3-opus", + "gpt-4.1-mini", + "openrouter.o4-mini", + "openrouter.gemini-2.5-flash", + "claude-3.5-haiku", "o3-mini", - "azure.o3-mini", + "azure.o3", + "gpt-4o", + "azure.gpt-4.1", + "openrouter.claude-3-haiku", "gpt-4.1-nano", "azure.gpt-4.1-nano", - "gpt-4o-mini", - "azure.gpt-4o-mini", - "o1", - "azure.o1", - "gemini-2.5-flash", + "claude-3.7-sonnet", + "gemini-2.5", + "azure.o4-mini", + "o1-mini", "qwen-qwq", "meta-llama/llama-4-maverick-17b-128e-instruct", - "claude-3-opus", - "gpt-4o", - "azure.gpt-4o", - "gemini-2.0-flash-lite", - "gemini-2.0-flash", - "deepseek-r1-distill-llama-70b", - "llama-3.3-70b-versatile", - "claude-3.5-sonnet", - "o1-mini", - "azure.o1-mini", - "gpt-4.1-mini", - "azure.gpt-4.1-mini", - "gemini-2.5", - "meta-llama/llama-4-scout-17b-16e-instruct", - "openrouter.deepseek-chat-free", - "openrouter.deepseek-r1-free", "openrouter.gpt-4.1", - "openrouter.gpt-4.1-mini", - "openrouter.gpt-4.1-nano", - "openrouter.gpt-4.5-preview", - "openrouter.gpt-4o", - "openrouter.gpt-4o-mini", - "openrouter.o1", "openrouter.o1-pro", - "openrouter.o1-mini", "openrouter.o3", - "openrouter.o3-mini", - "openrouter.o4-mini", - "openrouter.gemini-2.5-flash", - "openrouter.gemini-2.5", - "openrouter.claude-3.5-sonnet", - "openrouter.claude-3-haiku", - "openrouter.claude-3.7-sonnet", - "openrouter.claude-3.5-haiku", - "openrouter.claude-3-opus" + "claude-3.5-sonnet", + "gemini-2.5-flash", + "azure.gpt-4.1-mini", + "openrouter.o3-mini" ], "type": "string" }, "reasoningEffort": { "description": "Reasoning effort for models that support it (OpenAI, Anthropic)", - "enum": ["low", "medium", "high"], + "enum": [ + "low", + "medium", + "high" + ], "type": "string" } }, - "required": ["model"], + "required": [ + "model" + ], "type": "object" }, "description": "Agent configurations", @@ -212,7 +220,9 @@ "type": "string" } }, - "required": ["directory"], + "required": [ + "directory" + ], "type": "object" }, "debug": { @@ -250,7 +260,9 @@ "type": "object" } }, - "required": ["command"], + "required": [ + "command" + ], "type": "object" }, "description": "Language Server Protocol configurations", @@ -288,7 +300,10 @@ "type": { "default": "stdio", "description": "Type of MCP server", - "enum": ["stdio", "sse"], + "enum": [ + "stdio", + "sse" + ], "type": "string" }, "url": { @@ -296,7 +311,9 @@ "type": "string" } }, - "required": ["command"], + "required": [ + "command" + ], "type": "object" }, "description": "Model Control Protocol server configurations", @@ -322,9 +339,9 @@ "openai", "gemini", "groq", + "openrouter", "bedrock", - "azure", - "openrouter" + "azure" ], "type": "string" } @@ -334,6 +351,28 @@ "description": "LLM provider configurations", "type": "object" }, + "tui": { + "description": "Terminal User Interface configuration", + "properties": { + "theme": { + "default": "opencode", + "description": "TUI theme name", + "enum": [ + "opencode", + "catppuccin", + "dracula", + "flexoki", + "gruvbox", + "monokai", + "onedark", + "tokyonight", + "tron" + ], + "type": "string" + } + }, + "type": "object" + }, "wd": { "description": "Working directory for the application", "type": "string"