on main help sign in
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 <kujtimii.h@gmail.com>

commit82de14371d45bf672686ca5d340c4567a56c2364 authorAdam <2363879+adamdottv@users.noreply.github.com> dateThu 1 May 2025, 05:49 (1 year ago) parent61d9dc9
42 files changed+4595−1923
cmd/schema/main.go+23 internal/app/app.go+19 internal/config/config.go+68 internal/diff/diff.go+224−308 internal/tui/components/chat/chat.go+46−26 internal/tui/components/chat/editor.go+46−16 internal/tui/components/chat/list.go+47−29 internal/tui/components/chat/message.go+94−85 internal/tui/components/chat/sidebar.go+54−18 internal/tui/components/core/status.go+64−28 internal/tui/components/dialog/commands.go+21−17 internal/tui/components/dialog/help.go+33−15 internal/tui/components/dialog/init.go+27−23 internal/tui/components/dialog/models.go+18−11 internal/tui/components/dialog/permission.go+69−53 internal/tui/components/dialog/quit.go+15−11 internal/tui/components/dialog/session.go+18−14 internal/tui/components/dialog/theme.go+198 internal/tui/components/logs/details.go+16−12 internal/tui/components/logs/table.go+7−4 internal/tui/layout/container.go+8−29 internal/tui/layout/overlay.go+7−3 internal/tui/layout/split.go+8−14 internal/tui/page/chat.go−1 internal/tui/page/logs.go+3−3 internal/tui/styles/huh.go−46 internal/tui/styles/markdown.go+222−879 internal/tui/styles/styles.go+145−170 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+57−4 opencode-schema.json+143−104
cmd/schema/main.go+23
@@ -98,6 +98,29 @@ func generateSchema() map[string]any {
9898 },
9999 }
100100
101+ schema["properties"].(map[string]any)["tui"] = map[string]any{
102+ "type": "object",
103+ "description": "Terminal User Interface configuration",
104+ "properties": map[string]any{
105+ "theme": map[string]any{
106+ "type": "string",
107+ "description": "TUI theme name",
108+ "default": "opencode",
109+ "enum": []string{
110+ "opencode",
111+ "catppuccin",
112+ "dracula",
113+ "flexoki",
114+ "gruvbox",
115+ "monokai",
116+ "onedark",
117+ "tokyonight",
118+ "tron",
119+ },
120+ },
121+ },
122+ }
123+
101124 // Add MCP servers
102125 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
103126 "type": "object",
internal/app/app.go+19
@@ -16,6 +16,7 @@ import (
1616 "github.com/opencode-ai/opencode/internal/message"
1717 "github.com/opencode-ai/opencode/internal/permission"
1818 "github.com/opencode-ai/opencode/internal/session"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
1920)
2021
2122type App struct {
@@ -49,6 +50,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
4950 LSPClients: make(map[string]*lsp.Client),
5051 }
5152
53+ // Initialize theme based on configuration
54+ app.initTheme()
55+
5256 // Initialize LSP clients in the background
5357 go app.initLSPClients(ctx)
5458
@@ -73,6 +77,21 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
7377 return app, nil
7478}
7579
80+// initTheme sets the application theme based on the configuration
81+func (app *App) initTheme() {
82+ cfg := config.Get()
83+ if cfg == nil || cfg.TUI.Theme == "" {
84+ return // Use default theme
85+ }
86+
87+ // Try to set the theme from config
88+ err := theme.SetTheme(cfg.TUI.Theme)
89+ if err != nil {
90+ logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
91+ } else {
92+ logging.Debug("Set theme from config", "theme", cfg.TUI.Theme)
93+ }
94+}
7695
7796// Shutdown performs a clean shutdown of the application
7897func (app *App) Shutdown() {
internal/config/config.go+68
@@ -2,9 +2,11 @@
22package config
33
44import (
5+ "encoding/json"
56 "fmt"
67 "log/slog"
78 "os"
9+ "path/filepath"
810 "strings"
911
1012 "github.com/opencode-ai/opencode/internal/llm/models"
@@ -65,6 +67,11 @@ type LSPConfig struct {
6567 Options any `json:"options"`
6668}
6769
70+// TUIConfig defines the configuration for the Terminal User Interface.
71+type TUIConfig struct {
72+ Theme string `json:"theme,omitempty"`
73+}
74+
6875// Config is the main configuration structure for the application.
6976type Config struct {
7077 Data Data `json:"data"`
@@ -76,6 +83,7 @@ type Config struct {
7683 Debug bool `json:"debug,omitempty"`
7784 DebugLSP bool `json:"debugLSP,omitempty"`
7885 ContextPaths []string `json:"contextPaths,omitempty"`
86+ TUI TUIConfig `json:"tui"`
7987}
8088
8189// Application constants
@@ -203,6 +211,7 @@ func configureViper() {
203211func setDefaults(debug bool) {
204212 viper.SetDefault("data.directory", defaultDataDirectory)
205213 viper.SetDefault("contextPaths", defaultContextPaths)
214+ viper.SetDefault("tui.theme", "opencode")
206215
207216 if debug {
208217 viper.SetDefault("debug", true)
@@ -714,3 +723,62 @@ func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
714723
715724 return nil
716725}
726+
727+// UpdateTheme updates the theme in the configuration and writes it to the config file.
728+func UpdateTheme(themeName string) error {
729+ if cfg == nil {
730+ return fmt.Errorf("config not loaded")
731+ }
732+
733+ // Update the in-memory config
734+ cfg.TUI.Theme = themeName
735+
736+ // Get the config file path
737+ configFile := viper.ConfigFileUsed()
738+ var configData []byte
739+ if configFile == "" {
740+ homeDir, err := os.UserHomeDir()
741+ if err != nil {
742+ return fmt.Errorf("failed to get home directory: %w", err)
743+ }
744+ configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
745+ logging.Info("config file not found, creating new one", "path", configFile)
746+ configData = []byte(`{}`)
747+ } else {
748+ // Read the existing config file
749+ data, err := os.ReadFile(configFile)
750+ if err != nil {
751+ return fmt.Errorf("failed to read config file: %w", err)
752+ }
753+ configData = data
754+ }
755+
756+ // Parse the JSON
757+ var configMap map[string]interface{}
758+ if err := json.Unmarshal(configData, &configMap); err != nil {
759+ return fmt.Errorf("failed to parse config file: %w", err)
760+ }
761+
762+ // Update just the theme value
763+ tuiConfig, ok := configMap["tui"].(map[string]interface{})
764+ if !ok {
765+ // TUI config doesn't exist yet, create it
766+ configMap["tui"] = map[string]interface{}{"theme": themeName}
767+ } else {
768+ // Update existing TUI config
769+ tuiConfig["theme"] = themeName
770+ configMap["tui"] = tuiConfig
771+ }
772+
773+ // Write the updated config back to file
774+ updatedData, err := json.MarshalIndent(configMap, "", " ")
775+ if err != nil {
776+ return fmt.Errorf("failed to marshal config: %w", err)
777+ }
778+
779+ if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
780+ return fmt.Errorf("failed to write config file: %w", err)
781+ }
782+
783+ return nil
784+}
internal/diff/diff.go+224−308
@@ -16,6 +16,7 @@ import (
1616 "github.com/charmbracelet/lipgloss"
1717 "github.com/charmbracelet/x/ansi"
1818 "github.com/opencode-ai/opencode/internal/config"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
1920 "github.com/sergi/go-diff/diffmatchpatch"
2021)
2122
@@ -68,143 +69,6 @@ type linePair struct {
6869 right *DiffLine
6970}
7071
71−// -------------------------------------------------------------------------
72−// Style Configuration
73−// -------------------------------------------------------------------------
74−
75−// StyleConfig defines styling for diff rendering
76−type StyleConfig struct {
77− ShowHeader bool
78− ShowHunkHeader bool
79− FileNameFg lipgloss.Color
80− // Background colors
81− RemovedLineBg lipgloss.Color
82− AddedLineBg lipgloss.Color
83− ContextLineBg lipgloss.Color
84− HunkLineBg lipgloss.Color
85− RemovedLineNumberBg lipgloss.Color
86− AddedLineNamerBg lipgloss.Color
87−
88− // Foreground colors
89− HunkLineFg lipgloss.Color
90− RemovedFg lipgloss.Color
91− AddedFg lipgloss.Color
92− LineNumberFg lipgloss.Color
93− RemovedHighlightFg lipgloss.Color
94− AddedHighlightFg lipgloss.Color
95−
96− // Highlight settings
97− HighlightStyle string
98− RemovedHighlightBg lipgloss.Color
99− AddedHighlightBg lipgloss.Color
100−}
101−
102−// StyleOption is a function that modifies a StyleConfig
103−type StyleOption func(*StyleConfig)
104−
105−// NewStyleConfig creates a StyleConfig with default values
106−func NewStyleConfig(opts ...StyleOption) StyleConfig {
107− // Default color scheme
108− config := StyleConfig{
109− ShowHeader: true,
110− ShowHunkHeader: true,
111− FileNameFg: lipgloss.Color("#a0a0a0"),
112− RemovedLineBg: lipgloss.Color("#3A3030"),
113− AddedLineBg: lipgloss.Color("#303A30"),
114− ContextLineBg: lipgloss.Color("#212121"),
115− HunkLineBg: lipgloss.Color("#212121"),
116− HunkLineFg: lipgloss.Color("#a0a0a0"),
117− RemovedFg: lipgloss.Color("#7C4444"),
118− AddedFg: lipgloss.Color("#478247"),
119− LineNumberFg: lipgloss.Color("#888888"),
120− HighlightStyle: "dracula",
121− RemovedHighlightBg: lipgloss.Color("#612726"),
122− AddedHighlightBg: lipgloss.Color("#256125"),
123− RemovedLineNumberBg: lipgloss.Color("#332929"),
124− AddedLineNamerBg: lipgloss.Color("#293229"),
125− RemovedHighlightFg: lipgloss.Color("#FADADD"),
126− AddedHighlightFg: lipgloss.Color("#DAFADA"),
127− }
128−
129− // Apply all provided options
130− for _, opt := range opts {
131− opt(&config)
132− }
133−
134− return config
135−}
136−
137−// Style option functions
138−func WithFileNameFg(color lipgloss.Color) StyleOption {
139− return func(s *StyleConfig) { s.FileNameFg = color }
140−}
141−
142−func WithRemovedLineBg(color lipgloss.Color) StyleOption {
143− return func(s *StyleConfig) { s.RemovedLineBg = color }
144−}
145−
146−func WithAddedLineBg(color lipgloss.Color) StyleOption {
147− return func(s *StyleConfig) { s.AddedLineBg = color }
148−}
149−
150−func WithContextLineBg(color lipgloss.Color) StyleOption {
151− return func(s *StyleConfig) { s.ContextLineBg = color }
152−}
153−
154−func WithRemovedFg(color lipgloss.Color) StyleOption {
155− return func(s *StyleConfig) { s.RemovedFg = color }
156−}
157−
158−func WithAddedFg(color lipgloss.Color) StyleOption {
159− return func(s *StyleConfig) { s.AddedFg = color }
160−}
161−
162−func WithLineNumberFg(color lipgloss.Color) StyleOption {
163− return func(s *StyleConfig) { s.LineNumberFg = color }
164−}
165−
166−func WithHighlightStyle(style string) StyleOption {
167− return func(s *StyleConfig) { s.HighlightStyle = style }
168−}
169−
170−func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
171− return func(s *StyleConfig) {
172− s.RemovedHighlightBg = bg
173− s.RemovedHighlightFg = fg
174− }
175−}
176−
177−func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
178− return func(s *StyleConfig) {
179− s.AddedHighlightBg = bg
180− s.AddedHighlightFg = fg
181− }
182−}
183−
184−func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
185− return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
186−}
187−
188−func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
189− return func(s *StyleConfig) { s.AddedLineNamerBg = color }
190−}
191−
192−func WithHunkLineBg(color lipgloss.Color) StyleOption {
193− return func(s *StyleConfig) { s.HunkLineBg = color }
194−}
195−
196−func WithHunkLineFg(color lipgloss.Color) StyleOption {
197− return func(s *StyleConfig) { s.HunkLineFg = color }
198−}
199−
200−func WithShowHeader(show bool) StyleOption {
201− return func(s *StyleConfig) { s.ShowHeader = show }
202−}
203−
204−func WithShowHunkHeader(show bool) StyleOption {
205− return func(s *StyleConfig) { s.ShowHunkHeader = show }
206−}
207−
20872// -------------------------------------------------------------------------
20973// Parse Configuration
21074// -------------------------------------------------------------------------
@@ -233,7 +97,6 @@ func WithContextSize(size int) ParseOption {
23397// SideBySideConfig configures the rendering of side-by-side diffs
23498type SideBySideConfig struct {
23599 TotalWidth int
236− Style StyleConfig
237100}
238101
239102// SideBySideOption modifies a SideBySideConfig
@@ -243,7 +106,6 @@ type SideBySideOption func(*SideBySideConfig)
243106func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
244107 config := SideBySideConfig{
245108 TotalWidth: 160, // Default width for side-by-side view
246− Style: NewStyleConfig(),
247109 }
248110
249111 for _, opt := range opts {
@@ -262,20 +124,6 @@ func WithTotalWidth(width int) SideBySideOption {
262124 }
263125}
264126
265−// WithStyle sets the styling configuration
266−func WithStyle(style StyleConfig) SideBySideOption {
267− return func(s *SideBySideConfig) {
268− s.Style = style
269− }
270−}
271−
272−// WithStyleOptions applies the specified style options
273−func WithStyleOptions(opts ...StyleOption) SideBySideOption {
274− return func(s *SideBySideConfig) {
275− s.Style = NewStyleConfig(opts...)
276− }
277−}
278−
279127// -------------------------------------------------------------------------
280128// Diff Parsing
281129// -------------------------------------------------------------------------
@@ -382,7 +230,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
382230}
383231
384232// HighlightIntralineChanges updates lines in a hunk to show character-level differences
385−func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
233+func HighlightIntralineChanges(h *Hunk) {
386234 var updated []DiffLine
387235 dmp := diffmatchpatch.New()
388236
@@ -476,6 +324,8 @@ func pairLines(lines []DiffLine) []linePair {
476324
477325// SyntaxHighlight applies syntax highlighting to text based on file extension
478326func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
327+ t := theme.CurrentTheme()
328+
479329 // Determine the language lexer to use
480330 l := lexers.Match(fileName)
481331 if l == nil {
@@ -491,93 +341,175 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
491341 if f == nil {
492342 f = formatters.Fallback
493343 }
494− theme := `
495− <style name="vscode-dark-plus">
344+
345+ // Dynamic theme based on current theme values
346+ syntaxThemeXml := fmt.Sprintf(`
347+ <style name="opencode-theme">
496348 <!-- Base colors -->
497− <entry type="Background" style="bg:#1E1E1E"/>
498− <entry type="Text" style="#D4D4D4"/>
499− <entry type="Other" style="#D4D4D4"/>
500− <entry type="Error" style="#F44747"/>
501− <!-- Keywords - using the Control flow / Special keywords color -->
502− <entry type="Keyword" style="#C586C0"/>
503− <entry type="KeywordConstant" style="#4FC1FF"/>
504− <entry type="KeywordDeclaration" style="#C586C0"/>
505− <entry type="KeywordNamespace" style="#C586C0"/>
506− <entry type="KeywordPseudo" style="#C586C0"/>
507− <entry type="KeywordReserved" style="#C586C0"/>
508− <entry type="KeywordType" style="#4EC9B0"/>
349+ <entry type="Background" style="bg:%s"/>
350+ <entry type="Text" style="%s"/>
351+ <entry type="Other" style="%s"/>
352+ <entry type="Error" style="%s"/>
353+ <!-- Keywords -->
354+ <entry type="Keyword" style="%s"/>
355+ <entry type="KeywordConstant" style="%s"/>
356+ <entry type="KeywordDeclaration" style="%s"/>
357+ <entry type="KeywordNamespace" style="%s"/>
358+ <entry type="KeywordPseudo" style="%s"/>
359+ <entry type="KeywordReserved" style="%s"/>
360+ <entry type="KeywordType" style="%s"/>
509361 <!-- Names -->
510− <entry type="Name" style="#D4D4D4"/>
511− <entry type="NameAttribute" style="#9CDCFE"/>
512− <entry type="NameBuiltin" style="#4EC9B0"/>
513− <entry type="NameBuiltinPseudo" style="#9CDCFE"/>
514− <entry type="NameClass" style="#4EC9B0"/>
515− <entry type="NameConstant" style="#4FC1FF"/>
516− <entry type="NameDecorator" style="#DCDCAA"/>
517− <entry type="NameEntity" style="#9CDCFE"/>
518− <entry type="NameException" style="#4EC9B0"/>
519− <entry type="NameFunction" style="#DCDCAA"/>
520− <entry type="NameLabel" style="#C8C8C8"/>
521− <entry type="NameNamespace" style="#4EC9B0"/>
522− <entry type="NameOther" style="#9CDCFE"/>
523− <entry type="NameTag" style="#569CD6"/>
524− <entry type="NameVariable" style="#9CDCFE"/>
525− <entry type="NameVariableClass" style="#9CDCFE"/>
526− <entry type="NameVariableGlobal" style="#9CDCFE"/>
527− <entry type="NameVariableInstance" style="#9CDCFE"/>
362+ <entry type="Name" style="%s"/>
363+ <entry type="NameAttribute" style="%s"/>
364+ <entry type="NameBuiltin" style="%s"/>
365+ <entry type="NameBuiltinPseudo" style="%s"/>
366+ <entry type="NameClass" style="%s"/>
367+ <entry type="NameConstant" style="%s"/>
368+ <entry type="NameDecorator" style="%s"/>
369+ <entry type="NameEntity" style="%s"/>
370+ <entry type="NameException" style="%s"/>
371+ <entry type="NameFunction" style="%s"/>
372+ <entry type="NameLabel" style="%s"/>
373+ <entry type="NameNamespace" style="%s"/>
374+ <entry type="NameOther" style="%s"/>
375+ <entry type="NameTag" style="%s"/>
376+ <entry type="NameVariable" style="%s"/>
377+ <entry type="NameVariableClass" style="%s"/>
378+ <entry type="NameVariableGlobal" style="%s"/>
379+ <entry type="NameVariableInstance" style="%s"/>
528380 <!-- Literals -->
529− <entry type="Literal" style="#CE9178"/>
530− <entry type="LiteralDate" style="#CE9178"/>
531− <entry type="LiteralString" style="#CE9178"/>
532− <entry type="LiteralStringBacktick" style="#CE9178"/>
533− <entry type="LiteralStringChar" style="#CE9178"/>
534− <entry type="LiteralStringDoc" style="#CE9178"/>
535− <entry type="LiteralStringDouble" style="#CE9178"/>
536− <entry type="LiteralStringEscape" style="#d7ba7d"/>
537− <entry type="LiteralStringHeredoc" style="#CE9178"/>
538− <entry type="LiteralStringInterpol" style="#CE9178"/>
539− <entry type="LiteralStringOther" style="#CE9178"/>
540− <entry type="LiteralStringRegex" style="#d16969"/>
541− <entry type="LiteralStringSingle" style="#CE9178"/>
542− <entry type="LiteralStringSymbol" style="#CE9178"/>
543− <!-- Numbers - using the numberLiteral color -->
544− <entry type="LiteralNumber" style="#b5cea8"/>
545− <entry type="LiteralNumberBin" style="#b5cea8"/>
546− <entry type="LiteralNumberFloat" style="#b5cea8"/>
547− <entry type="LiteralNumberHex" style="#b5cea8"/>
548− <entry type="LiteralNumberInteger" style="#b5cea8"/>
549− <entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
550− <entry type="LiteralNumberOct" style="#b5cea8"/>
381+ <entry type="Literal" style="%s"/>
382+ <entry type="LiteralDate" style="%s"/>
383+ <entry type="LiteralString" style="%s"/>
384+ <entry type="LiteralStringBacktick" style="%s"/>
385+ <entry type="LiteralStringChar" style="%s"/>
386+ <entry type="LiteralStringDoc" style="%s"/>
387+ <entry type="LiteralStringDouble" style="%s"/>
388+ <entry type="LiteralStringEscape" style="%s"/>
389+ <entry type="LiteralStringHeredoc" style="%s"/>
390+ <entry type="LiteralStringInterpol" style="%s"/>
391+ <entry type="LiteralStringOther" style="%s"/>
392+ <entry type="LiteralStringRegex" style="%s"/>
393+ <entry type="LiteralStringSingle" style="%s"/>
394+ <entry type="LiteralStringSymbol" style="%s"/>
395+ <!-- Numbers -->
396+ <entry type="LiteralNumber" style="%s"/>
397+ <entry type="LiteralNumberBin" style="%s"/>
398+ <entry type="LiteralNumberFloat" style="%s"/>
399+ <entry type="LiteralNumberHex" style="%s"/>
400+ <entry type="LiteralNumberInteger" style="%s"/>
401+ <entry type="LiteralNumberIntegerLong" style="%s"/>
402+ <entry type="LiteralNumberOct" style="%s"/>
551403 <!-- Operators -->
552− <entry type="Operator" style="#D4D4D4"/>
553− <entry type="OperatorWord" style="#C586C0"/>
554− <entry type="Punctuation" style="#D4D4D4"/>
555− <!-- Comments - standard VSCode Dark+ comment color -->
556− <entry type="Comment" style="#6A9955"/>
557− <entry type="CommentHashbang" style="#6A9955"/>
558− <entry type="CommentMultiline" style="#6A9955"/>
559− <entry type="CommentSingle" style="#6A9955"/>
560− <entry type="CommentSpecial" style="#6A9955"/>
561− <entry type="CommentPreproc" style="#C586C0"/>
404+ <entry type="Operator" style="%s"/>
405+ <entry type="OperatorWord" style="%s"/>
406+ <entry type="Punctuation" style="%s"/>
407+ <!-- Comments -->
408+ <entry type="Comment" style="%s"/>
409+ <entry type="CommentHashbang" style="%s"/>
410+ <entry type="CommentMultiline" style="%s"/>
411+ <entry type="CommentSingle" style="%s"/>
412+ <entry type="CommentSpecial" style="%s"/>
413+ <entry type="CommentPreproc" style="%s"/>
562414 <!-- Generic styles -->
563− <entry type="Generic" style="#D4D4D4"/>
564− <entry type="GenericDeleted" style="#F44747"/>
565− <entry type="GenericEmph" style="italic #D4D4D4"/>
566− <entry type="GenericError" style="#F44747"/>
567− <entry type="GenericHeading" style="bold #D4D4D4"/>
568− <entry type="GenericInserted" style="#b5cea8"/>
569− <entry type="GenericOutput" style="#808080"/>
570− <entry type="GenericPrompt" style="#D4D4D4"/>
571− <entry type="GenericStrong" style="bold #D4D4D4"/>
572− <entry type="GenericSubheading" style="bold #D4D4D4"/>
573− <entry type="GenericTraceback" style="#F44747"/>
415+ <entry type="Generic" style="%s"/>
416+ <entry type="GenericDeleted" style="%s"/>
417+ <entry type="GenericEmph" style="italic %s"/>
418+ <entry type="GenericError" style="%s"/>
419+ <entry type="GenericHeading" style="bold %s"/>
420+ <entry type="GenericInserted" style="%s"/>
421+ <entry type="GenericOutput" style="%s"/>
422+ <entry type="GenericPrompt" style="%s"/>
423+ <entry type="GenericStrong" style="bold %s"/>
424+ <entry type="GenericSubheading" style="bold %s"/>
425+ <entry type="GenericTraceback" style="%s"/>
574426 <entry type="GenericUnderline" style="underline"/>
575− <entry type="TextWhitespace" style="#D4D4D4"/>
427+ <entry type="TextWhitespace" style="%s"/>
576428</style>
577−`
429+`,
430+ getColor(t.Background()), // Background
431+ getColor(t.Text()), // Text
432+ getColor(t.Text()), // Other
433+ getColor(t.Error()), // Error
434+
435+ getColor(t.SyntaxKeyword()), // Keyword
436+ getColor(t.SyntaxKeyword()), // KeywordConstant
437+ getColor(t.SyntaxKeyword()), // KeywordDeclaration
438+ getColor(t.SyntaxKeyword()), // KeywordNamespace
439+ getColor(t.SyntaxKeyword()), // KeywordPseudo
440+ getColor(t.SyntaxKeyword()), // KeywordReserved
441+ getColor(t.SyntaxType()), // KeywordType
442+
443+ getColor(t.Text()), // Name
444+ getColor(t.SyntaxVariable()), // NameAttribute
445+ getColor(t.SyntaxType()), // NameBuiltin
446+ getColor(t.SyntaxVariable()), // NameBuiltinPseudo
447+ getColor(t.SyntaxType()), // NameClass
448+ getColor(t.SyntaxVariable()), // NameConstant
449+ getColor(t.SyntaxFunction()), // NameDecorator
450+ getColor(t.SyntaxVariable()), // NameEntity
451+ getColor(t.SyntaxType()), // NameException
452+ getColor(t.SyntaxFunction()), // NameFunction
453+ getColor(t.Text()), // NameLabel
454+ getColor(t.SyntaxType()), // NameNamespace
455+ getColor(t.SyntaxVariable()), // NameOther
456+ getColor(t.SyntaxKeyword()), // NameTag
457+ getColor(t.SyntaxVariable()), // NameVariable
458+ getColor(t.SyntaxVariable()), // NameVariableClass
459+ getColor(t.SyntaxVariable()), // NameVariableGlobal
460+ getColor(t.SyntaxVariable()), // NameVariableInstance
461+
462+ getColor(t.SyntaxString()), // Literal
463+ getColor(t.SyntaxString()), // LiteralDate
464+ getColor(t.SyntaxString()), // LiteralString
465+ getColor(t.SyntaxString()), // LiteralStringBacktick
466+ getColor(t.SyntaxString()), // LiteralStringChar
467+ getColor(t.SyntaxString()), // LiteralStringDoc
468+ getColor(t.SyntaxString()), // LiteralStringDouble
469+ getColor(t.SyntaxString()), // LiteralStringEscape
470+ getColor(t.SyntaxString()), // LiteralStringHeredoc
471+ getColor(t.SyntaxString()), // LiteralStringInterpol
472+ getColor(t.SyntaxString()), // LiteralStringOther
473+ getColor(t.SyntaxString()), // LiteralStringRegex
474+ getColor(t.SyntaxString()), // LiteralStringSingle
475+ getColor(t.SyntaxString()), // LiteralStringSymbol
476+
477+ getColor(t.SyntaxNumber()), // LiteralNumber
478+ getColor(t.SyntaxNumber()), // LiteralNumberBin
479+ getColor(t.SyntaxNumber()), // LiteralNumberFloat
480+ getColor(t.SyntaxNumber()), // LiteralNumberHex
481+ getColor(t.SyntaxNumber()), // LiteralNumberInteger
482+ getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
483+ getColor(t.SyntaxNumber()), // LiteralNumberOct
484+
485+ getColor(t.SyntaxOperator()), // Operator
486+ getColor(t.SyntaxKeyword()), // OperatorWord
487+ getColor(t.SyntaxPunctuation()), // Punctuation
488+
489+ getColor(t.SyntaxComment()), // Comment
490+ getColor(t.SyntaxComment()), // CommentHashbang
491+ getColor(t.SyntaxComment()), // CommentMultiline
492+ getColor(t.SyntaxComment()), // CommentSingle
493+ getColor(t.SyntaxComment()), // CommentSpecial
494+ getColor(t.SyntaxKeyword()), // CommentPreproc
495+
496+ getColor(t.Text()), // Generic
497+ getColor(t.Error()), // GenericDeleted
498+ getColor(t.Text()), // GenericEmph
499+ getColor(t.Error()), // GenericError
500+ getColor(t.Text()), // GenericHeading
501+ getColor(t.Success()), // GenericInserted
502+ getColor(t.TextMuted()), // GenericOutput
503+ getColor(t.Text()), // GenericPrompt
504+ getColor(t.Text()), // GenericStrong
505+ getColor(t.Text()), // GenericSubheading
506+ getColor(t.Error()), // GenericTraceback
507+ getColor(t.Text()), // TextWhitespace
508+ )
578509
579− r := strings.NewReader(theme)
510+ r := strings.NewReader(syntaxThemeXml)
580511 style := chroma.MustNewXMLStyle(r)
512+
581513 // Modify the style to use the provided background
582514 s, err := style.Builder().Transform(
583515 func(t chroma.StyleEntry) chroma.StyleEntry {
@@ -599,6 +531,14 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
599531 return f.Format(w, s, it)
600532}
601533
534+// getColor returns the appropriate hex color string based on terminal background
535+func getColor(adaptiveColor lipgloss.AdaptiveColor) string {
536+ if lipgloss.HasDarkBackground() {
537+ return adaptiveColor.Dark
538+ }
539+ return adaptiveColor.Light
540+}
541+
602542// highlightLine applies syntax highlighting to a single line
603543func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
604544 var buf bytes.Buffer
@@ -610,11 +550,11 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
610550}
611551
612552// createStyles generates the lipgloss styles needed for rendering diffs
613−func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
614− removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
615− addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
616− contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
617− lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
553+func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
554+ removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
555+ addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
556+ contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
557+ lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
618558
619559 return
620560}
@@ -623,9 +563,20 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
623563// Rendering Functions
624564// -------------------------------------------------------------------------
625565
566+func lipglossToHex(color lipgloss.Color) string {
567+ r, g, b, a := color.RGBA()
568+
569+ // Scale uint32 values (0-65535) to uint8 (0-255).
570+ r8 := uint8(r >> 8)
571+ g8 := uint8(g >> 8)
572+ b8 := uint8(b >> 8)
573+ a8 := uint8(a >> 8)
574+
575+ return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8)
576+}
577+
626578// applyHighlighting applies intra-line highlighting to a piece of text
627−func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
628−) string {
579+func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
629580 // Find all ANSI sequences in the content
630581 ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
631582 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
@@ -663,6 +614,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
663614 inSelection := false
664615 currentPos := 0
665616
617+ // Get the appropriate color based on terminal background
618+ bgColor := lipgloss.Color(getColor(highlightBg))
619+ fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
620+
666621 for i := 0; i < len(content); {
667622 // Check if we're at an ANSI sequence
668623 isAnsi := false
@@ -697,12 +652,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
697652 // Get the current styling
698653 currentStyle := ansiSequences[currentPos]
699654
700− // Apply background highlight
655+ // Apply foreground and background highlight
656+ sb.WriteString("\x1b[38;2;")
657+ r, g, b, _ := fgColor.RGBA()
658+ sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
701659 sb.WriteString("\x1b[48;2;")
702− r, g, b, _ := highlightBg.RGBA()
660+ r, g, b, _ = bgColor.RGBA()
703661 sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
704662 sb.WriteString(char)
705− sb.WriteString("\x1b[49m") // Reset only background
663+ // Reset foreground and background
664+ sb.WriteString("\x1b[39m")
706665
707666 // Reapply the original ANSI sequence
708667 sb.WriteString(currentStyle)
@@ -719,22 +678,24 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
719678}
720679
721680// renderLeftColumn formats the left side of a side-by-side diff
722−func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
681+func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string {
682+ t := theme.CurrentTheme()
683+
723684 if dl == nil {
724− contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
685+ contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
725686 return contextLineStyle.Width(colWidth).Render("")
726687 }
727688
728− removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
689+ removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t)
729690
730691 // Determine line style based on line type
731692 var marker string
732693 var bgStyle lipgloss.Style
733694 switch dl.Kind {
734695 case LineRemoved:
735− marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
696+ marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-")
736697 bgStyle = removedLineStyle
737− lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
698+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
738699 case LineAdded:
739700 marker = "?"
740701 bgStyle = contextLineStyle
@@ -757,7 +718,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
757718
758719 // Apply intra-line highlighting for removed lines
759720 if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
760− content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
721+ content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved())
761722 }
762723
763724 // Add a padding space for removed lines
@@ -771,28 +732,30 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
771732 ansi.Truncate(
772733 lineText,
773734 colWidth,
774− lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
735+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
775736 ),
776737 )
777738}
778739
779740// renderRightColumn formats the right side of a side-by-side diff
780−func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
741+func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
742+ t := theme.CurrentTheme()
743+
781744 if dl == nil {
782− contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
745+ contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
783746 return contextLineStyle.Width(colWidth).Render("")
784747 }
785748
786− _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
749+ _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
787750
788751 // Determine line style based on line type
789752 var marker string
790753 var bgStyle lipgloss.Style
791754 switch dl.Kind {
792755 case LineAdded:
793− marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
756+ marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+")
794757 bgStyle = addedLineStyle
795− lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
758+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
796759 case LineRemoved:
797760 marker = "?"
798761 bgStyle = contextLineStyle
@@ -815,7 +778,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
815778
816779 // Apply intra-line highlighting for added lines
817780 if dl.Kind == LineAdded && len(dl.Segments) > 0 {
818− content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
781+ content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded())
819782 }
820783
821784 // Add a padding space for added lines
@@ -829,7 +792,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
829792 ansi.Truncate(
830793 lineText,
831794 colWidth,
832− lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
795+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
833796 ),
834797 )
835798}
@@ -848,7 +811,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
848811 copy(hunkCopy.Lines, h.Lines)
849812
850813 // Highlight changes within lines
851− HighlightIntralineChanges(&hunkCopy, config.Style)
814+ HighlightIntralineChanges(&hunkCopy)
852815
853816 // Pair lines for side-by-side display
854817 pairs := pairLines(hunkCopy.Lines)
@@ -860,8 +823,8 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
860823 rightWidth := config.TotalWidth - colWidth
861824 var sb strings.Builder
862825 for _, p := range pairs {
863− leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
864− rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
826+ leftStr := renderLeftColumn(fileName, p.left, leftWidth)
827+ rightStr := renderRightColumn(fileName, p.right, rightWidth)
865828 sb.WriteString(leftStr + rightStr + "\n")
866829 }
867830
@@ -876,54 +839,7 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
876839 }
877840
878841 var sb strings.Builder
879− config := NewSideBySideConfig(opts...)
880−
881− if config.Style.ShowHeader {
882− removeIcon := lipgloss.NewStyle().
883− Background(config.Style.RemovedLineBg).
884− Foreground(config.Style.RemovedFg).
885− Render("⏹")
886− addIcon := lipgloss.NewStyle().
887− Background(config.Style.AddedLineBg).
888− Foreground(config.Style.AddedFg).
889− Render("⏹")
890−
891− fileName := lipgloss.NewStyle().
892− Background(config.Style.ContextLineBg).
893− Foreground(config.Style.FileNameFg).
894− Render(" " + diffResult.OldFile)
895− sb.WriteString(
896− lipgloss.NewStyle().
897− Background(config.Style.ContextLineBg).
898− Padding(0, 1, 0, 1).
899− Foreground(config.Style.FileNameFg).
900− BorderStyle(lipgloss.NormalBorder()).
901− BorderTop(true).
902− BorderBottom(true).
903− BorderForeground(config.Style.FileNameFg).
904− BorderBackground(config.Style.ContextLineBg).
905− Width(config.TotalWidth).
906− Render(
907− lipgloss.JoinHorizontal(lipgloss.Top,
908− removeIcon,
909− addIcon,
910− fileName,
911− ),
912− ) + "\n",
913− )
914− }
915−
916842 for _, h := range diffResult.Hunks {
917− // Render hunk header
918− if config.Style.ShowHunkHeader {
919− sb.WriteString(
920− lipgloss.NewStyle().
921− Background(config.Style.HunkLineBg).
922− Foreground(config.Style.HunkLineFg).
923− Width(config.TotalWidth).
924− Render(h.Header) + "\n",
925− )
926− }
927843 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
928844 }
929845
@@ -944,8 +860,8 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
944860 removals = 0
945861 )
946862
947− lines := strings.Split(unified, "\n")
948− for _, line := range lines {
863+ lines := strings.SplitSeq(unified, "\n")
864+ for line := range lines {
949865 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
950866 additions++
951867 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
internal/tui/components/chat/chat.go+46−26
@@ -9,6 +9,7 @@ import (
99 "github.com/opencode-ai/opencode/internal/config"
1010 "github.com/opencode-ai/opencode/internal/session"
1111 "github.com/opencode-ai/opencode/internal/tui/styles"
12+ "github.com/opencode-ai/opencode/internal/tui/theme"
1213 "github.com/opencode-ai/opencode/internal/version"
1314)
1415
@@ -22,12 +23,29 @@ type SessionClearedMsg struct{}
2223
2324type EditorFocusMsg bool
2425
26+func header(width int) string {
27+ return lipgloss.JoinVertical(
28+ lipgloss.Top,
29+ logo(width),
30+ repo(width),
31+ "",
32+ cwd(width),
33+ )
34+}
35+
2536func lspsConfigured(width int) string {
2637 cfg := config.Get()
2738 title := "LSP Configuration"
2839 title = ansi.Truncate(title, width, "…")
2940
30− lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
41+ t := theme.CurrentTheme()
42+ baseStyle := styles.BaseStyle()
43+
44+ lsps := baseStyle.
45+ Width(width).
46+ Foreground(t.Primary()).
47+ Bold(true).
48+ Render(title)
3149
3250 // Get LSP names and sort them for consistent ordering
3351 var lspNames []string
@@ -39,16 +57,19 @@ func lspsConfigured(width int) string {
3957 var lspViews []string
4058 for _, name := range lspNames {
4159 lsp := cfg.LSP[name]
42− lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
43− fmt.Sprintf("• %s", name),
44− )
60+ lspName := baseStyle.
61+ Foreground(t.Text()).
62+ Render(fmt.Sprintf("• %s", name))
63+
4564 cmd := lsp.Command
4665 cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
47− lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
48− fmt.Sprintf(" (%s)", cmd),
49− )
66+
67+ lspPath := baseStyle.
68+ Foreground(t.TextMuted()).
69+ Render(fmt.Sprintf(" (%s)", cmd))
70+
5071 lspViews = append(lspViews,
51− styles.BaseStyle.
72+ baseStyle.
5273 Width(width).
5374 Render(
5475 lipgloss.JoinHorizontal(
@@ -59,7 +80,8 @@ func lspsConfigured(width int) string {
5980 ),
6081 )
6182 }
62− return styles.BaseStyle.
83+
84+ return baseStyle.
6385 Width(width).
6486 Render(
6587 lipgloss.JoinVertical(
@@ -75,10 +97,14 @@ func lspsConfigured(width int) string {
7597
7698func logo(width int) string {
7799 logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
100+ t := theme.CurrentTheme()
101+ baseStyle := styles.BaseStyle()
78102
79− version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
103+ versionText := baseStyle.
104+ Foreground(t.TextMuted()).
105+ Render(version.Version)
80106
81− return styles.BaseStyle.
107+ return baseStyle.
82108 Bold(true).
83109 Width(width).
84110 Render(
@@ -86,34 +112,28 @@ func logo(width int) string {
86112 lipgloss.Left,
87113 logo,
88114 " ",
89− version,
115+ versionText,
90116 ),
91117 )
92118}
93119
94120func repo(width int) string {
95121 repo := "https://github.com/opencode-ai/opencode"
96− return styles.BaseStyle.
97− Foreground(styles.ForgroundDim).
122+ t := theme.CurrentTheme()
123+
124+ return styles.BaseStyle().
125+ Foreground(t.TextMuted()).
98126 Width(width).
99127 Render(repo)
100128}
101129
102130func cwd(width int) string {
103131 cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
104− return styles.BaseStyle.
105− Foreground(styles.ForgroundDim).
132+ t := theme.CurrentTheme()
133+
134+ return styles.BaseStyle().
135+ Foreground(t.TextMuted()).
106136 Width(width).
107137 Render(cwd)
108138}
109139
110−func header(width int) string {
111− header := lipgloss.JoinVertical(
112− lipgloss.Top,
113− logo(width),
114− repo(width),
115− "",
116− cwd(width),
117− )
118− return header
119−}
internal/tui/components/chat/editor.go+46−16
@@ -10,8 +10,10 @@ import (
1010 "github.com/charmbracelet/lipgloss"
1111 "github.com/opencode-ai/opencode/internal/app"
1212 "github.com/opencode-ai/opencode/internal/session"
13+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
1314 "github.com/opencode-ai/opencode/internal/tui/layout"
1415 "github.com/opencode-ai/opencode/internal/tui/styles"
16+ "github.com/opencode-ai/opencode/internal/tui/theme"
1517 "github.com/opencode-ai/opencode/internal/tui/util"
1618)
1719
@@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd {
100102func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101103 var cmd tea.Cmd
102104 switch msg := msg.(type) {
105+ case dialog.ThemeChangedMsg:
106+ m.textarea = CreateTextArea(&m.textarea)
107+ return m, nil
103108 case SessionSelectedMsg:
104109 if msg.ID != m.session.ID {
105110 m.session = msg
@@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134139}
135140
136141func (m *editorCmp) View() string {
137− style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
142+ t := theme.CurrentTheme()
143+
144+ // Style the prompt with theme colors
145+ style := lipgloss.NewStyle().
146+ Padding(0, 0, 0, 1).
147+ Bold(true).
148+ Foreground(t.Primary())
138149
139150 return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
140151}
@@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding {
155166 return bindings
156167}
157168
169+func CreateTextArea(existing *textarea.Model) textarea.Model {
170+ t := theme.CurrentTheme()
171+ bgColor := t.Background()
172+ textColor := t.Text()
173+ textMutedColor := t.TextMuted()
174+
175+ ta := textarea.New()
176+ ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
177+ ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
178+ ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
179+ ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
180+ ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
181+ ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor)
182+ ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
183+ ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
184+
185+ ta.Prompt = " "
186+ ta.ShowLineNumbers = false
187+ ta.CharLimit = -1
188+
189+ if existing != nil {
190+ ta.SetValue(existing.Value())
191+ ta.SetWidth(existing.Width())
192+ ta.SetHeight(existing.Height())
193+ }
194+
195+ ta.Focus()
196+ return ta
197+}
198+
158199func NewEditorCmp(app *app.App) tea.Model {
159− ti := textarea.New()
160− ti.Prompt = " "
161− ti.ShowLineNumbers = false
162− ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
163− ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
164− ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
165− ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
166−
167− ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
168− ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
169− ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
170− ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
171− ti.CharLimit = -1
172− ti.Focus()
200+ ta := CreateTextArea(nil)
201+
173202 return &editorCmp{
174203 app: app,
175− textarea: ti,
204+ textarea: ta,
176205 }
177206}
207+
internal/tui/components/chat/list.go+47−29
@@ -14,7 +14,9 @@ import (
1414 "github.com/opencode-ai/opencode/internal/message"
1515 "github.com/opencode-ai/opencode/internal/pubsub"
1616 "github.com/opencode-ai/opencode/internal/session"
17+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
1718 "github.com/opencode-ai/opencode/internal/tui/styles"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
1820 "github.com/opencode-ai/opencode/internal/tui/util"
1921)
2022
@@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd {
6971func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7072 var cmds []tea.Cmd
7173 switch msg := msg.(type) {
72−
74+ case dialog.ThemeChangedMsg:
75+ m.rerender()
76+ return m, nil
7377 case SessionSelectedMsg:
7478 if msg.ID != m.session.ID {
7579 cmd := m.SetSession(msg)
@@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string {
174178func (m *messagesCmp) renderView() {
175179 m.uiMessages = make([]uiMessage, 0)
176180 pos := 0
181+ baseStyle := styles.BaseStyle()
177182
178183 if m.width == 0 {
179184 return
@@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() {
225230 messages := make([]string, 0)
226231 for _, v := range m.uiMessages {
227232 messages = append(messages, v.content,
228− styles.BaseStyle.
233+ baseStyle.
229234 Width(m.width).
230− Render(
231− "",
232− ),
235+ Render(""),
233236 )
234237 }
235238 m.viewport.SetContent(
236− styles.BaseStyle.
239+ baseStyle.
237240 Width(m.width).
238241 Render(
239242 lipgloss.JoinVertical(
@@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() {
245248}
246249
247250func (m *messagesCmp) View() string {
251+ baseStyle := styles.BaseStyle()
252+
248253 if m.rendering {
249− return styles.BaseStyle.
254+ return baseStyle.
250255 Width(m.width).
251256 Render(
252257 lipgloss.JoinVertical(
@@ -258,14 +263,14 @@ func (m *messagesCmp) View() string {
258263 )
259264 }
260265 if len(m.messages) == 0 {
261− content := styles.BaseStyle.
266+ content := baseStyle.
262267 Width(m.width).
263268 Height(m.height - 1).
264269 Render(
265270 m.initialScreen(),
266271 )
267272
268− return styles.BaseStyle.
273+ return baseStyle.
269274 Width(m.width).
270275 Render(
271276 lipgloss.JoinVertical(
@@ -277,7 +282,7 @@ func (m *messagesCmp) View() string {
277282 )
278283 }
279284
280− return styles.BaseStyle.
285+ return baseStyle.
281286 Width(m.width).
282287 Render(
283288 lipgloss.JoinVertical(
@@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
328333func (m *messagesCmp) working() string {
329334 text := ""
330335 if m.IsAgentWorking() && len(m.messages) > 0 {
336+ t := theme.CurrentTheme()
337+ baseStyle := styles.BaseStyle()
338+
331339 task := "Thinking..."
332340 lastMessage := m.messages[len(m.messages)-1]
333341 if hasToolsWithoutResponse(m.messages) {
@@ -338,42 +346,49 @@ func (m *messagesCmp) working() string {
338346 task = "Generating..."
339347 }
340348 if task != "" {
341− text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render(
342− fmt.Sprintf("%s %s ", m.spinner.View(), task),
343− )
349+ text += baseStyle.
350+ Width(m.width).
351+ Foreground(t.Primary()).
352+ Bold(true).
353+ Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
344354 }
345355 }
346356 return text
347357}
348358
349359func (m *messagesCmp) help() string {
360+ t := theme.CurrentTheme()
361+ baseStyle := styles.BaseStyle()
362+
350363 text := ""
351364
352365 if m.app.CoderAgent.IsBusy() {
353366 text += lipgloss.JoinHorizontal(
354367 lipgloss.Left,
355− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
356− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
357− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
368+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
369+ baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
370+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
358371 )
359372 } else {
360373 text += lipgloss.JoinHorizontal(
361374 lipgloss.Left,
362− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
363− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"),
364− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"),
365− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"),
366− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"),
367− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"),
375+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
376+ baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
377+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
378+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
379+ baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
380+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
368381 )
369382 }
370− return styles.BaseStyle.
383+ return baseStyle.
371384 Width(m.width).
372385 Render(text)
373386}
374387
375388func (m *messagesCmp) initialScreen() string {
376− return styles.BaseStyle.Width(m.width).Render(
389+ baseStyle := styles.BaseStyle()
390+
391+ return baseStyle.Width(m.width).Render(
377392 lipgloss.JoinVertical(
378393 lipgloss.Top,
379394 header(m.width),
@@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string {
383398 )
384399}
385400
401+func (m *messagesCmp) rerender() {
402+ for _, msg := range m.messages {
403+ delete(m.cachedContent, msg.ID)
404+ }
405+ m.renderView()
406+}
407+
386408func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
387409 if m.width == width && m.height == height {
388410 return nil
@@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
391413 m.height = height
392414 m.viewport.Width = width
393415 m.viewport.Height = height - 2
394− for _, msg := range m.messages {
395− delete(m.cachedContent, msg.ID)
396− }
397− m.uiMessages = make([]uiMessage, 0)
398− m.renderView()
416+ m.rerender()
399417 return nil
400418}
401419
internal/tui/components/chat/message.go+94−85
@@ -6,10 +6,8 @@ import (
66 "fmt"
77 "path/filepath"
88 "strings"
9− "sync"
109 "time"
1110
12− "github.com/charmbracelet/glamour"
1311 "github.com/charmbracelet/lipgloss"
1412 "github.com/charmbracelet/x/ansi"
1513 "github.com/opencode-ai/opencode/internal/config"
@@ -19,6 +17,7 @@ import (
1917 "github.com/opencode-ai/opencode/internal/llm/tools"
2018 "github.com/opencode-ai/opencode/internal/message"
2119 "github.com/opencode-ai/opencode/internal/tui/styles"
20+ "github.com/opencode-ai/opencode/internal/tui/theme"
2221)
2322
2423type uiMessageType int
@@ -31,8 +30,6 @@ const (
3130 maxResultHeight = 10
3231)
3332
34−var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false))
35−
3633type uiMessage struct {
3734 ID string
3835 messageType uiMessageType
@@ -41,46 +38,37 @@ type uiMessage struct {
4138 content string
4239}
4340
44−type renderCache struct {
45− mutex sync.Mutex
46− cache map[string][]uiMessage
47−}
48−
4941func toMarkdown(content string, focused bool, width int) string {
50− r, _ := glamour.NewTermRenderer(
51− glamour.WithStyles(styles.MarkdownTheme(false)),
52− glamour.WithWordWrap(width),
53− )
54− if focused {
55− r, _ = glamour.NewTermRenderer(
56− glamour.WithStyles(styles.MarkdownTheme(true)),
57− glamour.WithWordWrap(width),
58− )
59− }
42+ r := styles.GetMarkdownRenderer(width)
6043 rendered, _ := r.Render(content)
6144 return rendered
6245}
6346
6447func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
65− style := styles.BaseStyle.
48+ t := theme.CurrentTheme()
49+
50+ style := styles.BaseStyle().
6651 Width(width - 1).
6752 BorderLeft(true).
68− Foreground(styles.ForgroundDim).
69− BorderForeground(styles.PrimaryColor).
53+ Foreground(t.TextMuted()).
54+ BorderForeground(t.Primary()).
7055 BorderStyle(lipgloss.ThickBorder())
56+
7157 if isUser {
72− style = style.
73− BorderForeground(styles.Blue)
58+ style = style.BorderForeground(t.Secondary())
7459 }
60+
61+ // Apply markdown formatting and handle background color
7562 parts := []string{
76− styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background),
63+ styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
7764 }
7865
79− // remove newline at the end
66+ // Remove newline at the end
8067 parts[0] = strings.TrimSuffix(parts[0], "\n")
8168 if len(info) > 0 {
8269 parts = append(parts, info...)
8370 }
71+
8472 rendered := style.Render(
8573 lipgloss.JoinVertical(
8674 lipgloss.Left,
@@ -121,26 +109,37 @@ func renderAssistantMessage(
121109 finishData := msg.FinishPart()
122110 info := []string{}
123111
112+ t := theme.CurrentTheme()
113+ baseStyle := styles.BaseStyle()
114+
124115 // Add finish info if available
125116 if finished {
126117 switch finishData.Reason {
127118 case message.FinishReasonEndTurn:
128− took := formatTimeDifference(msg.CreatedAt, finishData.Time)
129− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
130− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
131− ))
119+ took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
120+ info = append(info, baseStyle.
121+ Width(width-1).
122+ Foreground(t.TextMuted()).
123+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
124+ )
132125 case message.FinishReasonCanceled:
133− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
134− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"),
135− ))
126+ info = append(info, baseStyle.
127+ Width(width-1).
128+ Foreground(t.TextMuted()).
129+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
130+ )
136131 case message.FinishReasonError:
137− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
138− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"),
139− ))
132+ info = append(info, baseStyle.
133+ Width(width-1).
134+ Foreground(t.TextMuted()).
135+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
136+ )
140137 case message.FinishReasonPermissionDenied:
141− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
142− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"),
143− ))
138+ info = append(info, baseStyle.
139+ Width(width-1).
140+ Foreground(t.TextMuted()).
141+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
142+ )
144143 }
145144 }
146145 if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
@@ -414,32 +413,36 @@ func truncateHeight(content string, height int) string {
414413}
415414
416415func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
416+ t := theme.CurrentTheme()
417+ baseStyle := styles.BaseStyle()
418+
417419 if response.IsError {
418420 errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
419421 errContent = ansi.Truncate(errContent, width-1, "...")
420− return styles.BaseStyle.
422+ return baseStyle.
421423 Width(width).
422− Foreground(styles.Error).
424+ Foreground(t.Error()).
423425 Render(errContent)
424426 }
427+
425428 resultContent := truncateHeight(response.Content, maxResultHeight)
426429 switch toolCall.Name {
427430 case agent.AgentToolName:
428431 return styles.ForceReplaceBackgroundWithLipgloss(
429432 toMarkdown(resultContent, false, width),
430− styles.Background,
433+ t.Background(),
431434 )
432435 case tools.BashToolName:
433436 resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
434437 return styles.ForceReplaceBackgroundWithLipgloss(
435438 toMarkdown(resultContent, true, width),
436− styles.Background,
439+ t.Background(),
437440 )
438441 case tools.EditToolName:
439442 metadata := tools.EditResponseMetadata{}
440443 json.Unmarshal([]byte(response.Metadata), &metadata)
441444 truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
442− formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle))
445+ formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
443446 return formattedDiff
444447 case tools.FetchToolName:
445448 var params tools.FetchParams
@@ -454,16 +457,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
454457 resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
455458 return styles.ForceReplaceBackgroundWithLipgloss(
456459 toMarkdown(resultContent, true, width),
457− styles.Background,
460+ t.Background(),
458461 )
459462 case tools.GlobToolName:
460− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
463+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
461464 case tools.GrepToolName:
462− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
465+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
463466 case tools.LSToolName:
464− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
467+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
465468 case tools.SourcegraphToolName:
466− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
469+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
467470 case tools.ViewToolName:
468471 metadata := tools.ViewResponseMetadata{}
469472 json.Unmarshal([]byte(response.Metadata), &metadata)
@@ -476,7 +479,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
476479 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
477480 return styles.ForceReplaceBackgroundWithLipgloss(
478481 toMarkdown(resultContent, true, width),
479− styles.Background,
482+ t.Background(),
480483 )
481484 case tools.WriteToolName:
482485 params := tools.WriteParams{}
@@ -492,13 +495,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
492495 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
493496 return styles.ForceReplaceBackgroundWithLipgloss(
494497 toMarkdown(resultContent, true, width),
495− styles.Background,
498+ t.Background(),
496499 )
497500 default:
498501 resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
499502 return styles.ForceReplaceBackgroundWithLipgloss(
500503 toMarkdown(resultContent, true, width),
501− styles.Background,
504+ t.Background(),
502505 )
503506 }
504507}
@@ -515,39 +518,31 @@ func renderToolMessage(
515518 if nested {
516519 width = width - 3
517520 }
518− style := styles.BaseStyle.
521+
522+ t := theme.CurrentTheme()
523+ baseStyle := styles.BaseStyle()
524+
525+ style := baseStyle.
519526 Width(width - 1).
520527 BorderLeft(true).
521528 BorderStyle(lipgloss.ThickBorder()).
522529 PaddingLeft(1).
523− BorderForeground(styles.ForgroundDim)
530+ BorderForeground(t.TextMuted())
524531
525532 response := findToolResponse(toolCall.ID, allMessages)
526− toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
533+ toolNameText := baseStyle.Foreground(t.TextMuted()).
534+ Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
527535
528536 if !toolCall.Finished {
529537 // Get a brief description of what the tool is doing
530538 toolAction := getToolAction(toolCall.Name)
531539
532− // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ")
533− // truncatedInput := toolInput
534− // if len(truncatedInput) > 10 {
535− // truncatedInput = truncatedInput[len(truncatedInput)-10:]
536− // }
537− //
538− // truncatedInput = styles.BaseStyle.
539− // Italic(true).
540− // Width(width - 2 - lipgloss.Width(toolName)).
541− // Background(styles.BackgroundDim).
542− // Foreground(styles.ForgroundMid).
543− // Render(truncatedInput)
544−
545− progressText := styles.BaseStyle.
546− Width(width - 2 - lipgloss.Width(toolName)).
547− Foreground(styles.ForgroundDim).
540+ progressText := baseStyle.
541+ Width(width - 2 - lipgloss.Width(toolNameText)).
542+ Foreground(t.TextMuted()).
548543 Render(fmt.Sprintf("%s", toolAction))
549544
550− content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
545+ content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
551546 toolMsg := uiMessage{
552547 messageType: toolMessageType,
553548 position: position,
@@ -556,37 +551,39 @@ func renderToolMessage(
556551 }
557552 return toolMsg
558553 }
559− params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
554+
555+ params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
560556 responseContent := ""
561557 if response != nil {
562558 responseContent = renderToolResponse(toolCall, *response, width-2)
563559 responseContent = strings.TrimSuffix(responseContent, "\n")
564560 } else {
565− responseContent = styles.BaseStyle.
561+ responseContent = baseStyle.
566562 Italic(true).
567563 Width(width - 2).
568− Foreground(styles.ForgroundDim).
564+ Foreground(t.TextMuted()).
569565 Render("Waiting for response...")
570566 }
571567
572568 parts := []string{}
573569 if !nested {
574− params := styles.BaseStyle.
575− Width(width - 2 - lipgloss.Width(toolName)).
576− Foreground(styles.ForgroundDim).
570+ formattedParams := baseStyle.
571+ Width(width - 2 - lipgloss.Width(toolNameText)).
572+ Foreground(t.TextMuted()).
577573 Render(params)
578574
579− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params))
575+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
580576 } else {
581− prefix := styles.BaseStyle.
582− Foreground(styles.ForgroundDim).
577+ prefix := baseStyle.
578+ Foreground(t.TextMuted()).
583579 Render(" └ ")
584− params := styles.BaseStyle.
585− Width(width - 2 - lipgloss.Width(toolName)).
586− Foreground(styles.ForgroundMid).
580+ formattedParams := baseStyle.
581+ Width(width - 2 - lipgloss.Width(toolNameText)).
582+ Foreground(t.TextMuted()).
587583 Render(params)
588− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params))
584+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
589585 }
586+
590587 if toolCall.Name == agent.AgentToolName {
591588 taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
592589 toolCalls := []message.ToolCall{}
@@ -622,3 +619,15 @@ func renderToolMessage(
622619 }
623620 return toolMsg
624621}
622+
623+// Helper function to format the time difference between two Unix timestamps
624+func formatTimestampDiff(start, end int64) string {
625+ diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
626+ if diffSeconds < 1 {
627+ return fmt.Sprintf("%dms", int(diffSeconds*1000))
628+ }
629+ if diffSeconds < 60 {
630+ return fmt.Sprintf("%.1fs", diffSeconds)
631+ }
632+ return fmt.Sprintf("%.1fm", diffSeconds/60)
633+}
internal/tui/components/chat/sidebar.go+54−18
@@ -14,6 +14,7 @@ import (
1414 "github.com/opencode-ai/opencode/internal/pubsub"
1515 "github.com/opencode-ai/opencode/internal/session"
1616 "github.com/opencode-ai/opencode/internal/tui/styles"
17+ "github.com/opencode-ai/opencode/internal/tui/theme"
1718)
1819
1920type sidebarCmp struct {
@@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8182}
8283
8384func (m *sidebarCmp) View() string {
84− return styles.BaseStyle.
85+ baseStyle := styles.BaseStyle()
86+
87+ return baseStyle.
8588 Width(m.width).
8689 PaddingLeft(4).
8790 PaddingRight(2).
@@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string {
101104}
102105
103106func (m *sidebarCmp) sessionSection() string {
104− sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
105− sessionValue := styles.BaseStyle.
106− Foreground(styles.Forground).
107+ t := theme.CurrentTheme()
108+ baseStyle := styles.BaseStyle()
109+
110+ sessionKey := baseStyle.
111+ Foreground(t.Primary()).
112+ Bold(true).
113+ Render("Session")
114+
115+ sessionValue := baseStyle.
116+ Foreground(t.Text()).
107117 Width(m.width - lipgloss.Width(sessionKey)).
108118 Render(fmt.Sprintf(": %s", m.session.Title))
119+
109120 return lipgloss.JoinHorizontal(
110121 lipgloss.Left,
111122 sessionKey,
@@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string {
114125}
115126
116127func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
128+ t := theme.CurrentTheme()
129+ baseStyle := styles.BaseStyle()
130+
117131 stats := ""
118132 if additions > 0 && removals > 0 {
119− additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions))
120− removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals))
121− content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals)
122− stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content)
133+ additionsStr := baseStyle.
134+ Foreground(t.Success()).
135+ PaddingLeft(1).
136+ Render(fmt.Sprintf("+%d", additions))
137+
138+ removalsStr := baseStyle.
139+ Foreground(t.Error()).
140+ PaddingLeft(1).
141+ Render(fmt.Sprintf("-%d", removals))
142+
143+ content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
144+ stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
123145 } else if additions > 0 {
124− additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions)))
125− stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions)
146+ additionsStr := fmt.Sprintf(" %s", baseStyle.
147+ PaddingLeft(1).
148+ Foreground(t.Success()).
149+ Render(fmt.Sprintf("+%d", additions)))
150+ stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
126151 } else if removals > 0 {
127− removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals)))
128− stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals)
152+ removalsStr := fmt.Sprintf(" %s", baseStyle.
153+ PaddingLeft(1).
154+ Foreground(t.Error()).
155+ Render(fmt.Sprintf("-%d", removals)))
156+ stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
129157 }
130− filePathStr := styles.BaseStyle.Render(filePath)
131158
132− return styles.BaseStyle.
159+ filePathStr := baseStyle.Render(filePath)
160+
161+ return baseStyle.
133162 Width(m.width).
134163 Render(
135164 lipgloss.JoinHorizontal(
@@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
141170}
142171
143172func (m *sidebarCmp) modifiedFiles() string {
144− modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
173+ t := theme.CurrentTheme()
174+ baseStyle := styles.BaseStyle()
175+
176+ modifiedFiles := baseStyle.
177+ Width(m.width).
178+ Foreground(t.Primary()).
179+ Bold(true).
180+ Render("Modified Files:")
145181
146182 // If no modified files, show a placeholder message
147183 if m.modFiles == nil || len(m.modFiles) == 0 {
@@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string {
150186 if remainingWidth > 0 {
151187 message += strings.Repeat(" ", remainingWidth)
152188 }
153− return styles.BaseStyle.
189+ return baseStyle.
154190 Width(m.width).
155191 Render(
156192 lipgloss.JoinVertical(
157193 lipgloss.Top,
158194 modifiedFiles,
159− styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message),
195+ baseStyle.Foreground(t.TextMuted()).Render(message),
160196 ),
161197 )
162198 }
@@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string {
175211 fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
176212 }
177213
178− return styles.BaseStyle.
214+ return baseStyle.
179215 Width(m.width).
180216 Render(
181217 lipgloss.JoinVertical(
internal/tui/components/core/status.go+64−28
@@ -15,12 +15,13 @@ import (
1515 "github.com/opencode-ai/opencode/internal/session"
1616 "github.com/opencode-ai/opencode/internal/tui/components/chat"
1717 "github.com/opencode-ai/opencode/internal/tui/styles"
18+ "github.com/opencode-ai/opencode/internal/tui/theme"
1819 "github.com/opencode-ai/opencode/internal/tui/util"
1920)
2021
2122type StatusCmp interface {
2223 tea.Model
23− SetHelpMsg(string)
24+ SetHelpWidgetMsg(string)
2425}
2526
2627type statusCmp struct {
@@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7071 return m, nil
7172}
7273
73−var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
74+var helpWidget = ""
75+
76+// getHelpWidget returns the help widget with current theme colors
77+func getHelpWidget(helpText string) string {
78+ t := theme.CurrentTheme()
79+ if helpText == "" {
80+ helpText = "ctrl+? help"
81+ }
82+
83+ return styles.Padded().
84+ Background(t.TextMuted()).
85+ Foreground(t.BackgroundDarker()).
86+ Bold(true).
87+ Render(helpText)
88+}
7489
7590func formatTokensAndCost(tokens int64, cost float64) string {
7691 // Format tokens in human-readable format (e.g., 110K, 1.2M)
@@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string {
99114}
100115
101116func (m statusCmp) View() string {
102− status := helpWidget
117+ t := theme.CurrentTheme()
118+
119+ // Initialize the help widget
120+ status := getHelpWidget("")
121+
103122 if m.session.ID != "" {
104123 tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
105− tokensStyle := styles.Padded.
106− Background(styles.Forground).
107− Foreground(styles.BackgroundDim).
124+ tokensStyle := styles.Padded().
125+ Background(t.Text()).
126+ Foreground(t.BackgroundSecondary()).
108127 Render(tokens)
109128 status += tokensStyle
110129 }
111130
112− diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
131+ diagnostics := styles.Padded().
132+ Background(t.BackgroundDarker()).
133+ Render(m.projectDiagnostics())
134+
113135 if m.info.Msg != "" {
114− infoStyle := styles.Padded.
115− Foreground(styles.Base).
136+ infoStyle := styles.Padded().
137+ Foreground(t.Background()).
116138 Width(m.availableFooterMsgWidth(diagnostics))
139+
117140 switch m.info.Type {
118141 case util.InfoTypeInfo:
119− infoStyle = infoStyle.Background(styles.BorderColor)
142+ infoStyle = infoStyle.Background(t.Info())
120143 case util.InfoTypeWarn:
121− infoStyle = infoStyle.Background(styles.Peach)
144+ infoStyle = infoStyle.Background(t.Warning())
122145 case util.InfoTypeError:
123− infoStyle = infoStyle.Background(styles.Red)
146+ infoStyle = infoStyle.Background(t.Error())
124147 }
148+
125149 // Truncate message if it's longer than available width
126150 msg := m.info.Msg
127151 availWidth := m.availableFooterMsgWidth(diagnostics) - 10
@@ -130,9 +154,9 @@ func (m statusCmp) View() string {
130154 }
131155 status += infoStyle.Render(msg)
132156 } else {
133− status += styles.Padded.
134− Foreground(styles.Base).
135− Background(styles.BackgroundDim).
157+ status += styles.Padded().
158+ Foreground(t.Text()).
159+ Background(t.BackgroundSecondary()).
136160 Width(m.availableFooterMsgWidth(diagnostics)).
137161 Render("")
138162 }
@@ -143,6 +167,8 @@ func (m statusCmp) View() string {
143167}
144168
145169func (m *statusCmp) projectDiagnostics() string {
170+ t := theme.CurrentTheme()
171+
146172 // Check if any LSP server is still initializing
147173 initializing := false
148174 for _, client := range m.lspClients {
@@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string {
155181 // If any server is initializing, show that status
156182 if initializing {
157183 return lipgloss.NewStyle().
158− Background(styles.BackgroundDarker).
159− Foreground(styles.Peach).
184+ Background(t.BackgroundDarker()).
185+ Foreground(t.Warning()).
160186 Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
161187 }
162188
@@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string {
189215
190216 if len(errorDiagnostics) > 0 {
191217 errStr := lipgloss.NewStyle().
192− Background(styles.BackgroundDarker).
193− Foreground(styles.Error).
218+ Background(t.BackgroundDarker()).
219+ Foreground(t.Error()).
194220 Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
195221 diagnostics = append(diagnostics, errStr)
196222 }
197223 if len(warnDiagnostics) > 0 {
198224 warnStr := lipgloss.NewStyle().
199− Background(styles.BackgroundDarker).
200− Foreground(styles.Warning).
225+ Background(t.BackgroundDarker()).
226+ Foreground(t.Warning()).
201227 Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
202228 diagnostics = append(diagnostics, warnStr)
203229 }
204230 if len(hintDiagnostics) > 0 {
205231 hintStr := lipgloss.NewStyle().
206− Background(styles.BackgroundDarker).
207− Foreground(styles.Text).
232+ Background(t.BackgroundDarker()).
233+ Foreground(t.Text()).
208234 Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
209235 diagnostics = append(diagnostics, hintStr)
210236 }
211237 if len(infoDiagnostics) > 0 {
212238 infoStr := lipgloss.NewStyle().
213− Background(styles.BackgroundDarker).
214− Foreground(styles.Peach).
239+ Background(t.BackgroundDarker()).
240+ Foreground(t.Info()).
215241 Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
216242 diagnostics = append(diagnostics, infoStr)
217243 }
@@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
230256}
231257
232258func (m statusCmp) model() string {
259+ t := theme.CurrentTheme()
260+
233261 cfg := config.Get()
234262
235263 coder, ok := cfg.Agents[config.AgentCoder]
@@ -237,14 +265,22 @@ func (m statusCmp) model() string {
237265 return "Unknown"
238266 }
239267 model := models.SupportedModels[coder.Model]
240− return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
268+
269+ return styles.Padded().
270+ Background(t.Secondary()).
271+ Foreground(t.Background()).
272+ Render(model.Name)
241273}
242274
243−func (m statusCmp) SetHelpMsg(s string) {
244− helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
275+func (m statusCmp) SetHelpWidgetMsg(s string) {
276+ // Update the help widget text using the getHelpWidget function
277+ helpWidget = getHelpWidget(s)
245278}
246279
247280func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
281+ // Initialize the help widget with default text
282+ helpWidget = getHelpWidget("")
283+
248284 return &statusCmp{
249285 messageTTL: 10 * time.Second,
250286 lspClients: lspClients,
internal/tui/components/dialog/commands.go+21−17
@@ -6,6 +6,7 @@ import (
66 "github.com/charmbracelet/lipgloss"
77 "github.com/opencode-ai/opencode/internal/tui/layout"
88 "github.com/opencode-ai/opencode/internal/tui/styles"
9+ "github.com/opencode-ai/opencode/internal/tui/theme"
910 "github.com/opencode-ai/opencode/internal/tui/util"
1011)
1112
@@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112113}
113114
114115func (c *commandDialogCmp) View() string {
116+ t := theme.CurrentTheme()
117+ baseStyle := styles.BaseStyle()
118+
115119 if len(c.commands) == 0 {
116− return styles.BaseStyle.Padding(1, 2).
120+ return baseStyle.Padding(1, 2).
117121 Border(lipgloss.RoundedBorder()).
118− BorderBackground(styles.Background).
119− BorderForeground(styles.ForgroundDim).
122+ BorderBackground(t.Background()).
123+ BorderForeground(t.TextMuted()).
120124 Width(40).
121125 Render("No commands available")
122126 }
@@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string {
154158
155159 for i := startIdx; i < endIdx; i++ {
156160 cmd := c.commands[i]
157− itemStyle := styles.BaseStyle.Width(maxWidth)
158− descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim)
161+ itemStyle := baseStyle.Width(maxWidth)
162+ descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
159163
160164 if i == c.selectedIdx {
161165 itemStyle = itemStyle.
162− Background(styles.PrimaryColor).
163− Foreground(styles.Background).
166+ Background(t.Primary()).
167+ Foreground(t.Background()).
164168 Bold(true)
165169 descStyle = descStyle.
166− Background(styles.PrimaryColor).
167− Foreground(styles.Background)
170+ Background(t.Primary()).
171+ Foreground(t.Background())
168172 }
169173
170174 title := itemStyle.Padding(0, 1).Render(cmd.Title)
@@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string {
177181 }
178182 }
179183
180− title := styles.BaseStyle.
181− Foreground(styles.PrimaryColor).
184+ title := baseStyle.
185+ Foreground(t.Primary()).
182186 Bold(true).
183187 Width(maxWidth).
184188 Padding(0, 1).
@@ -187,15 +191,15 @@ func (c *commandDialogCmp) View() string {
187191 content := lipgloss.JoinVertical(
188192 lipgloss.Left,
189193 title,
190− styles.BaseStyle.Width(maxWidth).Render(""),
191− styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
192− styles.BaseStyle.Width(maxWidth).Render(""),
194+ baseStyle.Width(maxWidth).Render(""),
195+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
196+ baseStyle.Width(maxWidth).Render(""),
193197 )
194198
195− return styles.BaseStyle.Padding(1, 2).
199+ return baseStyle.Padding(1, 2).
196200 Border(lipgloss.RoundedBorder()).
197− BorderBackground(styles.Background).
198− BorderForeground(styles.ForgroundDim).
201+ BorderBackground(t.Background()).
202+ BorderForeground(t.TextMuted()).
199203 Width(lipgloss.Width(content) + 4).
200204 Render(content)
201205}
internal/tui/components/dialog/help.go+33−15
@@ -7,6 +7,7 @@ import (
77 tea "github.com/charmbracelet/bubbletea"
88 "github.com/charmbracelet/lipgloss"
99 "github.com/opencode-ai/opencode/internal/tui/styles"
10+ "github.com/opencode-ai/opencode/internal/tui/theme"
1011)
1112
1213type helpCmp struct {
@@ -53,10 +54,21 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
5354}
5455
5556func (h *helpCmp) render() string {
56− helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0)
57− helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid)
57+ t := theme.CurrentTheme()
58+ baseStyle := styles.BaseStyle()
59+
60+ helpKeyStyle := styles.Bold().
61+ Background(t.Background()).
62+ Foreground(t.Text()).
63+ Padding(0, 1, 0, 0)
64+
65+ helpDescStyle := styles.Regular().
66+ Background(t.Background()).
67+ Foreground(t.TextMuted())
68+
5869 // Compile list of bindings to render
5970 bindings := removeDuplicateBindings(h.keys)
71+
6072 // Enumerate through each group of bindings, populating a series of
6173 // pairs of columns, one for keys, one for descriptions
6274 var (
@@ -64,6 +76,7 @@ func (h *helpCmp) render() string {
6476 width int
6577 rows = 10 - 2
6678 )
79+
6780 for i := 0; i < len(bindings); i += rows {
6881 var (
6982 keys []string
@@ -73,11 +86,12 @@ func (h *helpCmp) render() string {
7386 keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
7487 descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
7588 }
89+
7690 // Render pair of columns; beyond the first pair, render a three space
7791 // left margin, in order to visually separate the pairs.
7892 var cols []string
7993 if len(pairs) > 0 {
80− cols = []string{styles.BaseStyle.Render(" ")}
94+ cols = []string{baseStyle.Render(" ")}
8195 }
8296
8397 maxDescWidth := 0
@@ -89,7 +103,7 @@ func (h *helpCmp) render() string {
89103 for i := range descs {
90104 remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
91105 if remainingWidth > 0 {
92− descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
106+ descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
93107 }
94108 }
95109 maxKeyWidth := 0
@@ -101,7 +115,7 @@ func (h *helpCmp) render() string {
101115 for i := range keys {
102116 remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
103117 if remainingWidth > 0 {
104− keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
118+ keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
105119 }
106120 }
107121
@@ -110,7 +124,7 @@ func (h *helpCmp) render() string {
110124 strings.Join(descs, "\n"),
111125 )
112126
113− pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
127+ pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
114128 // check whether it exceeds the maximum width avail (the width of the
115129 // terminal, subtracting 2 for the borders).
116130 width += lipgloss.Width(pair)
@@ -130,9 +144,9 @@ func (h *helpCmp) render() string {
130144 lipgloss.Left, // x
131145 lipgloss.Top, // y
132146 lastPair, // content
133− lipgloss.WithWhitespaceBackground(styles.Background), // background
147+ lipgloss.WithWhitespaceBackground(t.Background()),
134148 ))
135− content := styles.BaseStyle.Width(h.width).Render(
149+ content := baseStyle.Width(h.width).Render(
136150 lipgloss.JoinHorizontal(
137151 lipgloss.Top,
138152 prefix...,
@@ -140,8 +154,9 @@ func (h *helpCmp) render() string {
140154 )
141155 return content
142156 }
157+
143158 // Join pairs of columns and enclose in a border
144− content := styles.BaseStyle.Width(h.width).Render(
159+ content := baseStyle.Width(h.width).Render(
145160 lipgloss.JoinHorizontal(
146161 lipgloss.Top,
147162 pairs...,
@@ -151,22 +166,25 @@ func (h *helpCmp) render() string {
151166}
152167
153168func (h *helpCmp) View() string {
169+ t := theme.CurrentTheme()
170+ baseStyle := styles.BaseStyle()
171+
154172 content := h.render()
155− header := styles.BaseStyle.
173+ header := baseStyle.
156174 Bold(true).
157175 Width(lipgloss.Width(content)).
158− Foreground(styles.PrimaryColor).
176+ Foreground(t.Primary()).
159177 Render("Keyboard Shortcuts")
160178
161− return styles.BaseStyle.Padding(1).
179+ return baseStyle.Padding(1).
162180 Border(lipgloss.RoundedBorder()).
163− BorderForeground(styles.ForgroundDim).
181+ BorderForeground(t.TextMuted()).
164182 Width(h.width).
165− BorderBackground(styles.Background).
183+ BorderBackground(t.Background()).
166184 Render(
167185 lipgloss.JoinVertical(lipgloss.Center,
168186 header,
169− styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
187+ baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
170188 content,
171189 ),
172190 )
internal/tui/components/dialog/init.go+27−23
@@ -6,6 +6,7 @@ import (
66 "github.com/charmbracelet/lipgloss"
77
88 "github.com/opencode-ai/opencode/internal/tui/styles"
9+ "github.com/opencode-ai/opencode/internal/tui/theme"
910 "github.com/opencode-ai/opencode/internal/tui/util"
1011)
1112
@@ -92,55 +93,58 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9293
9394// View implements tea.Model.
9495func (m InitDialogCmp) View() string {
96+ t := theme.CurrentTheme()
97+ baseStyle := styles.BaseStyle()
98+
9599 // Calculate width needed for content
96100 maxWidth := 60 // Width for explanation text
97101
98− title := styles.BaseStyle.
99− Foreground(styles.PrimaryColor).
102+ title := baseStyle.
103+ Foreground(t.Primary()).
100104 Bold(true).
101105 Width(maxWidth).
102106 Padding(0, 1).
103107 Render("Initialize Project")
104108
105− explanation := styles.BaseStyle.
106− Foreground(styles.Forground).
109+ explanation := baseStyle.
110+ Foreground(t.Text()).
107111 Width(maxWidth).
108112 Padding(0, 1).
109113 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.")
110114
111− question := styles.BaseStyle.
112− Foreground(styles.Forground).
115+ question := baseStyle.
116+ Foreground(t.Text()).
113117 Width(maxWidth).
114118 Padding(1, 1).
115119 Render("Would you like to initialize this project?")
116120
117121 maxWidth = min(maxWidth, m.width-10)
118− yesStyle := styles.BaseStyle
119− noStyle := styles.BaseStyle
122+ yesStyle := baseStyle
123+ noStyle := baseStyle
120124
121125 if m.selected == 0 {
122126 yesStyle = yesStyle.
123− Background(styles.PrimaryColor).
124− Foreground(styles.Background).
127+ Background(t.Primary()).
128+ Foreground(t.Background()).
125129 Bold(true)
126130 noStyle = noStyle.
127− Background(styles.Background).
128− Foreground(styles.PrimaryColor)
131+ Background(t.Background()).
132+ Foreground(t.Primary())
129133 } else {
130134 noStyle = noStyle.
131− Background(styles.PrimaryColor).
132− Foreground(styles.Background).
135+ Background(t.Primary()).
136+ Foreground(t.Background()).
133137 Bold(true)
134138 yesStyle = yesStyle.
135− Background(styles.Background).
136− Foreground(styles.PrimaryColor)
139+ Background(t.Background()).
140+ Foreground(t.Primary())
137141 }
138142
139143 yes := yesStyle.Padding(0, 3).Render("Yes")
140144 no := noStyle.Padding(0, 3).Render("No")
141145
142− buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
143− buttons = styles.BaseStyle.
146+ buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
147+ buttons = baseStyle.
144148 Width(maxWidth).
145149 Padding(1, 0).
146150 Render(buttons)
@@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string {
148152 content := lipgloss.JoinVertical(
149153 lipgloss.Left,
150154 title,
151− styles.BaseStyle.Width(maxWidth).Render(""),
155+ baseStyle.Width(maxWidth).Render(""),
152156 explanation,
153157 question,
154158 buttons,
155− styles.BaseStyle.Width(maxWidth).Render(""),
159+ baseStyle.Width(maxWidth).Render(""),
156160 )
157161
158− return styles.BaseStyle.Padding(1, 2).
162+ return baseStyle.Padding(1, 2).
159163 Border(lipgloss.RoundedBorder()).
160− BorderBackground(styles.Background).
161− BorderForeground(styles.ForgroundDim).
164+ BorderBackground(t.Background()).
165+ BorderForeground(t.TextMuted()).
162166 Width(lipgloss.Width(content) + 4).
163167 Render(content)
164168}
internal/tui/components/dialog/models.go+18−11
@@ -12,6 +12,7 @@ import (
1212 "github.com/opencode-ai/opencode/internal/llm/models"
1313 "github.com/opencode-ai/opencode/internal/tui/layout"
1414 "github.com/opencode-ai/opencode/internal/tui/styles"
15+ "github.com/opencode-ai/opencode/internal/tui/theme"
1516 "github.com/opencode-ai/opencode/internal/tui/util"
1617)
1718
@@ -185,10 +186,13 @@ func (m *modelDialogCmp) switchProvider(offset int) {
185186}
186187
187188func (m *modelDialogCmp) View() string {
189+ t := theme.CurrentTheme()
190+ baseStyle := styles.BaseStyle()
191+
188192 // Capitalize first letter of provider name
189193 providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
190− title := styles.BaseStyle.
191− Foreground(styles.PrimaryColor).
194+ title := baseStyle.
195+ Foreground(t.Primary()).
192196 Bold(true).
193197 Width(maxDialogWidth).
194198 Padding(0, 0, 1).
@@ -199,10 +203,10 @@ func (m *modelDialogCmp) View() string {
199203 modelItems := make([]string, 0, endIdx-m.scrollOffset)
200204
201205 for i := m.scrollOffset; i < endIdx; i++ {
202− itemStyle := styles.BaseStyle.Width(maxDialogWidth)
206+ itemStyle := baseStyle.Width(maxDialogWidth)
203207 if i == m.selectedIdx {
204− itemStyle = itemStyle.Background(styles.PrimaryColor).
205− Foreground(styles.Background).Bold(true)
208+ itemStyle = itemStyle.Background(t.Primary()).
209+ Foreground(t.Background()).Bold(true)
206210 }
207211 modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
208212 }
@@ -212,14 +216,14 @@ func (m *modelDialogCmp) View() string {
212216 content := lipgloss.JoinVertical(
213217 lipgloss.Left,
214218 title,
215− styles.BaseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
219+ baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
216220 scrollIndicator,
217221 )
218222
219− return styles.BaseStyle.Padding(1, 2).
223+ return baseStyle.Padding(1, 2).
220224 Border(lipgloss.RoundedBorder()).
221− BorderBackground(styles.Background).
222− BorderForeground(styles.ForgroundDim).
225+ BorderBackground(t.Background()).
226+ BorderForeground(t.TextMuted()).
223227 Width(lipgloss.Width(content) + 4).
224228 Render(content)
225229}
@@ -249,8 +253,11 @@ func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
249253 return ""
250254 }
251255
252− return styles.BaseStyle.
253− Foreground(styles.PrimaryColor).
256+ t := theme.CurrentTheme()
257+ baseStyle := styles.BaseStyle()
258+
259+ return baseStyle.
260+ Foreground(t.Primary()).
254261 Width(maxWidth).
255262 Align(lipgloss.Right).
256263 Bold(true).
internal/tui/components/dialog/permission.go+69−53
@@ -7,13 +7,13 @@ import (
77 "github.com/charmbracelet/bubbles/key"
88 "github.com/charmbracelet/bubbles/viewport"
99 tea "github.com/charmbracelet/bubbletea"
10− "github.com/charmbracelet/glamour"
1110 "github.com/charmbracelet/lipgloss"
1211 "github.com/opencode-ai/opencode/internal/diff"
1312 "github.com/opencode-ai/opencode/internal/llm/tools"
1413 "github.com/opencode-ai/opencode/internal/permission"
1514 "github.com/opencode-ai/opencode/internal/tui/layout"
1615 "github.com/opencode-ai/opencode/internal/tui/styles"
16+ "github.com/opencode-ai/opencode/internal/tui/theme"
1717 "github.com/opencode-ai/opencode/internal/tui/util"
1818)
1919
@@ -149,25 +149,28 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
149149}
150150
151151func (p *permissionDialogCmp) renderButtons() string {
152− allowStyle := styles.BaseStyle
153− allowSessionStyle := styles.BaseStyle
154− denyStyle := styles.BaseStyle
155− spacerStyle := styles.BaseStyle.Background(styles.Background)
152+ t := theme.CurrentTheme()
153+ baseStyle := styles.BaseStyle()
154+
155+ allowStyle := baseStyle
156+ allowSessionStyle := baseStyle
157+ denyStyle := baseStyle
158+ spacerStyle := baseStyle.Background(t.Background())
156159
157160 // Style the selected button
158161 switch p.selectedOption {
159162 case 0:
160− allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
161− allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
162− denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
163+ allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
164+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
165+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
163166 case 1:
164− allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
165− allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
166− denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
167+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
168+ allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
169+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
167170 case 2:
168− allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
169− allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
170− denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
171+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
172+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
173+ denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
171174 }
172175
173176 allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
@@ -192,15 +195,18 @@ func (p *permissionDialogCmp) renderButtons() string {
192195}
193196
194197func (p *permissionDialogCmp) renderHeader() string {
195− toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
196− toolValue := styles.BaseStyle.
197− Foreground(styles.Forground).
198+ t := theme.CurrentTheme()
199+ baseStyle := styles.BaseStyle()
200+
201+ toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
202+ toolValue := baseStyle.
203+ Foreground(t.Text()).
198204 Width(p.width - lipgloss.Width(toolKey)).
199205 Render(fmt.Sprintf(": %s", p.permission.ToolName))
200206
201− pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
202− pathValue := styles.BaseStyle.
203− Foreground(styles.Forground).
207+ pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
208+ pathValue := baseStyle.
209+ Foreground(t.Text()).
204210 Width(p.width - lipgloss.Width(pathKey)).
205211 Render(fmt.Sprintf(": %s", p.permission.Path))
206212
@@ -210,45 +216,45 @@ func (p *permissionDialogCmp) renderHeader() string {
210216 toolKey,
211217 toolValue,
212218 ),
213− styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
219+ baseStyle.Render(strings.Repeat(" ", p.width)),
214220 lipgloss.JoinHorizontal(
215221 lipgloss.Left,
216222 pathKey,
217223 pathValue,
218224 ),
219− styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
225+ baseStyle.Render(strings.Repeat(" ", p.width)),
220226 }
221227
222228 // Add tool-specific header information
223229 switch p.permission.ToolName {
224230 case tools.BashToolName:
225− headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command"))
231+ headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
226232 case tools.EditToolName:
227− headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
233+ headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
228234 case tools.WriteToolName:
229− headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
235+ headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
230236 case tools.FetchToolName:
231− headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL"))
237+ headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
232238 }
233239
234− return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
240+ return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
235241}
236242
237243func (p *permissionDialogCmp) renderBashContent() string {
244+ t := theme.CurrentTheme()
245+ baseStyle := styles.BaseStyle()
246+
238247 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
239248 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
240249
241250 // Use the cache for markdown rendering
242251 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
243− r, _ := glamour.NewTermRenderer(
244− glamour.WithStyles(styles.MarkdownTheme(true)),
245− glamour.WithWordWrap(p.width-10),
246− )
252+ r := styles.GetMarkdownRenderer(p.width-10)
247253 s, err := r.Render(content)
248− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
254+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
249255 })
250256
251− finalContent := styles.BaseStyle.
257+ finalContent := baseStyle.
252258 Width(p.contentViewPort.Width).
253259 Render(renderedContent)
254260 p.contentViewPort.SetContent(finalContent)
@@ -295,39 +301,45 @@ func (p *permissionDialogCmp) renderWriteContent() string {
295301}
296302
297303func (p *permissionDialogCmp) renderFetchContent() string {
304+ t := theme.CurrentTheme()
305+ baseStyle := styles.BaseStyle()
306+
298307 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
299308 content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
300309
301310 // Use the cache for markdown rendering
302311 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
303− r, _ := glamour.NewTermRenderer(
304− glamour.WithStyles(styles.MarkdownTheme(true)),
305− glamour.WithWordWrap(p.width-10),
306− )
312+ r := styles.GetMarkdownRenderer(p.width-10)
307313 s, err := r.Render(content)
308− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
314+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
309315 })
310316
311− p.contentViewPort.SetContent(renderedContent)
317+ finalContent := baseStyle.
318+ Width(p.contentViewPort.Width).
319+ Render(renderedContent)
320+ p.contentViewPort.SetContent(finalContent)
312321 return p.styleViewport()
313322 }
314323 return ""
315324}
316325
317326func (p *permissionDialogCmp) renderDefaultContent() string {
327+ t := theme.CurrentTheme()
328+ baseStyle := styles.BaseStyle()
329+
318330 content := p.permission.Description
319331
320332 // Use the cache for markdown rendering
321333 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
322− r, _ := glamour.NewTermRenderer(
323− glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
324− glamour.WithWordWrap(p.width-10),
325− )
334+ r := styles.GetMarkdownRenderer(p.width-10)
326335 s, err := r.Render(content)
327− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
336+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
328337 })
329338
330− p.contentViewPort.SetContent(renderedContent)
339+ finalContent := baseStyle.
340+ Width(p.contentViewPort.Width).
341+ Render(renderedContent)
342+ p.contentViewPort.SetContent(finalContent)
331343
332344 if renderedContent == "" {
333345 return ""
@@ -337,17 +349,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
337349}
338350
339351func (p *permissionDialogCmp) styleViewport() string {
352+ t := theme.CurrentTheme()
340353 contentStyle := lipgloss.NewStyle().
341− Background(styles.Background)
354+ Background(t.Background())
342355
343356 return contentStyle.Render(p.contentViewPort.View())
344357}
345358
346359func (p *permissionDialogCmp) render() string {
347− title := styles.BaseStyle.
360+ t := theme.CurrentTheme()
361+ baseStyle := styles.BaseStyle()
362+
363+ title := baseStyle.
348364 Bold(true).
349365 Width(p.width - 4).
350− Foreground(styles.PrimaryColor).
366+ Foreground(t.Primary()).
351367 Render("Permission Required")
352368 // Render header
353369 headerContent := p.renderHeader()
@@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string {
378394 content := lipgloss.JoinVertical(
379395 lipgloss.Top,
380396 title,
381− styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
397+ baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
382398 headerContent,
383399 contentFinal,
384400 buttons,
385− styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
401+ baseStyle.Render(strings.Repeat(" ", p.width-4)),
386402 )
387403
388− return styles.BaseStyle.
404+ return baseStyle.
389405 Padding(1, 0, 0, 1).
390406 Border(lipgloss.RoundedBorder()).
391− BorderBackground(styles.Background).
392− BorderForeground(styles.ForgroundDim).
407+ BorderBackground(t.Background()).
408+ BorderForeground(t.TextMuted()).
393409 Width(p.width).
394410 Height(p.height).
395411 Render(
internal/tui/components/dialog/quit.go+15−11
@@ -8,6 +8,7 @@ import (
88 "github.com/charmbracelet/lipgloss"
99 "github.com/opencode-ai/opencode/internal/tui/layout"
1010 "github.com/opencode-ai/opencode/internal/tui/styles"
11+ "github.com/opencode-ai/opencode/internal/tui/theme"
1112 "github.com/opencode-ai/opencode/internal/tui/util"
1213)
1314
@@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8182}
8283
8384func (q *quitDialogCmp) View() string {
84− yesStyle := styles.BaseStyle
85− noStyle := styles.BaseStyle
86− spacerStyle := styles.BaseStyle.Background(styles.Background)
85+ t := theme.CurrentTheme()
86+ baseStyle := styles.BaseStyle()
87+
88+ yesStyle := baseStyle
89+ noStyle := baseStyle
90+ spacerStyle := baseStyle.Background(t.Background())
8791
8892 if q.selectedNo {
89− noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
90− yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
93+ noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
94+ yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
9195 } else {
92− yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
93− noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
96+ yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
97+ noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
9498 }
9599
96100 yesButton := yesStyle.Padding(0, 1).Render("Yes")
@@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string {
104108 buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
105109 }
106110
107− content := styles.BaseStyle.Render(
111+ content := baseStyle.Render(
108112 lipgloss.JoinVertical(
109113 lipgloss.Center,
110114 question,
@@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string {
113117 ),
114118 )
115119
116− return styles.BaseStyle.Padding(1, 2).
120+ return baseStyle.Padding(1, 2).
117121 Border(lipgloss.RoundedBorder()).
118− BorderBackground(styles.Background).
119− BorderForeground(styles.ForgroundDim).
122+ BorderBackground(t.Background()).
123+ BorderForeground(t.TextMuted()).
120124 Width(lipgloss.Width(content) + 4).
121125 Render(content)
122126}
internal/tui/components/dialog/session.go+18−14
@@ -7,6 +7,7 @@ import (
77 "github.com/opencode-ai/opencode/internal/session"
88 "github.com/opencode-ai/opencode/internal/tui/layout"
99 "github.com/opencode-ai/opencode/internal/tui/styles"
10+ "github.com/opencode-ai/opencode/internal/tui/theme"
1011 "github.com/opencode-ai/opencode/internal/tui/util"
1112)
1213
@@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
105106}
106107
107108func (s *sessionDialogCmp) View() string {
109+ t := theme.CurrentTheme()
110+ baseStyle := styles.BaseStyle()
111+
108112 if len(s.sessions) == 0 {
109− return styles.BaseStyle.Padding(1, 2).
113+ return baseStyle.Padding(1, 2).
110114 Border(lipgloss.RoundedBorder()).
111− BorderBackground(styles.Background).
112− BorderForeground(styles.ForgroundDim).
115+ BorderBackground(t.Background()).
116+ BorderForeground(t.TextMuted()).
113117 Width(40).
114118 Render("No sessions available")
115119 }
@@ -146,20 +150,20 @@ func (s *sessionDialogCmp) View() string {
146150
147151 for i := startIdx; i < endIdx; i++ {
148152 sess := s.sessions[i]
149− itemStyle := styles.BaseStyle.Width(maxWidth)
153+ itemStyle := baseStyle.Width(maxWidth)
150154
151155 if i == s.selectedIdx {
152156 itemStyle = itemStyle.
153− Background(styles.PrimaryColor).
154− Foreground(styles.Background).
157+ Background(t.Primary()).
158+ Foreground(t.Background()).
155159 Bold(true)
156160 }
157161
158162 sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
159163 }
160164
161− title := styles.BaseStyle.
162− Foreground(styles.PrimaryColor).
165+ title := baseStyle.
166+ Foreground(t.Primary()).
163167 Bold(true).
164168 Width(maxWidth).
165169 Padding(0, 1).
@@ -168,15 +172,15 @@ func (s *sessionDialogCmp) View() string {
168172 content := lipgloss.JoinVertical(
169173 lipgloss.Left,
170174 title,
171− styles.BaseStyle.Width(maxWidth).Render(""),
172− styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
173− styles.BaseStyle.Width(maxWidth).Render(""),
175+ baseStyle.Width(maxWidth).Render(""),
176+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
177+ baseStyle.Width(maxWidth).Render(""),
174178 )
175179
176− return styles.BaseStyle.Padding(1, 2).
180+ return baseStyle.Padding(1, 2).
177181 Border(lipgloss.RoundedBorder()).
178− BorderBackground(styles.Background).
179− BorderForeground(styles.ForgroundDim).
182+ BorderBackground(t.Background()).
183+ BorderForeground(t.TextMuted()).
180184 Width(lipgloss.Width(content) + 4).
181185 Render(content)
182186}
internal/tui/components/dialog/theme.gonew file+198
@@ -0,0 +1,198 @@
1+package dialog
2+
3+import (
4+ "github.com/charmbracelet/bubbles/key"
5+ tea "github.com/charmbracelet/bubbletea"
6+ "github.com/charmbracelet/lipgloss"
7+ "github.com/opencode-ai/opencode/internal/tui/layout"
8+ "github.com/opencode-ai/opencode/internal/tui/styles"
9+ "github.com/opencode-ai/opencode/internal/tui/theme"
10+ "github.com/opencode-ai/opencode/internal/tui/util"
11+)
12+
13+// ThemeChangedMsg is sent when the theme is changed
14+type ThemeChangedMsg struct {
15+ ThemeName string
16+}
17+
18+// CloseThemeDialogMsg is sent when the theme dialog is closed
19+type CloseThemeDialogMsg struct{}
20+
21+// ThemeDialog interface for the theme switching dialog
22+type ThemeDialog interface {
23+ tea.Model
24+ layout.Bindings
25+}
26+
27+type themeDialogCmp struct {
28+ themes []string
29+ selectedIdx int
30+ width int
31+ height int
32+ currentTheme string
33+}
34+
35+type themeKeyMap struct {
36+ Up key.Binding
37+ Down key.Binding
38+ Enter key.Binding
39+ Escape key.Binding
40+ J key.Binding
41+ K key.Binding
42+}
43+
44+var themeKeys = themeKeyMap{
45+ Up: key.NewBinding(
46+ key.WithKeys("up"),
47+ key.WithHelp("↑", "previous theme"),
48+ ),
49+ Down: key.NewBinding(
50+ key.WithKeys("down"),
51+ key.WithHelp("↓", "next theme"),
52+ ),
53+ Enter: key.NewBinding(
54+ key.WithKeys("enter"),
55+ key.WithHelp("enter", "select theme"),
56+ ),
57+ Escape: key.NewBinding(
58+ key.WithKeys("esc"),
59+ key.WithHelp("esc", "close"),
60+ ),
61+ J: key.NewBinding(
62+ key.WithKeys("j"),
63+ key.WithHelp("j", "next theme"),
64+ ),
65+ K: key.NewBinding(
66+ key.WithKeys("k"),
67+ key.WithHelp("k", "previous theme"),
68+ ),
69+}
70+
71+func (t *themeDialogCmp) Init() tea.Cmd {
72+ // Load available themes and update selectedIdx based on current theme
73+ t.themes = theme.AvailableThemes()
74+ t.currentTheme = theme.CurrentThemeName()
75+
76+ // Find the current theme in the list
77+ for i, name := range t.themes {
78+ if name == t.currentTheme {
79+ t.selectedIdx = i
80+ break
81+ }
82+ }
83+
84+ return nil
85+}
86+
87+func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88+ switch msg := msg.(type) {
89+ case tea.KeyMsg:
90+ switch {
91+ case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
92+ if t.selectedIdx > 0 {
93+ t.selectedIdx--
94+ }
95+ return t, nil
96+ case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
97+ if t.selectedIdx < len(t.themes)-1 {
98+ t.selectedIdx++
99+ }
100+ return t, nil
101+ case key.Matches(msg, themeKeys.Enter):
102+ if len(t.themes) > 0 {
103+ previousTheme := theme.CurrentThemeName()
104+ selectedTheme := t.themes[t.selectedIdx]
105+ if previousTheme == selectedTheme {
106+ return t, util.CmdHandler(CloseThemeDialogMsg{})
107+ }
108+ if err := theme.SetTheme(selectedTheme); err != nil {
109+ return t, util.ReportError(err)
110+ }
111+ return t, util.CmdHandler(ThemeChangedMsg{
112+ ThemeName: selectedTheme,
113+ })
114+ }
115+ case key.Matches(msg, themeKeys.Escape):
116+ return t, util.CmdHandler(CloseThemeDialogMsg{})
117+ }
118+ case tea.WindowSizeMsg:
119+ t.width = msg.Width
120+ t.height = msg.Height
121+ }
122+ return t, nil
123+}
124+
125+func (t *themeDialogCmp) View() string {
126+ currentTheme := theme.CurrentTheme()
127+ baseStyle := styles.BaseStyle()
128+
129+ if len(t.themes) == 0 {
130+ return baseStyle.Padding(1, 2).
131+ Border(lipgloss.RoundedBorder()).
132+ BorderBackground(currentTheme.Background()).
133+ BorderForeground(currentTheme.TextMuted()).
134+ Width(40).
135+ Render("No themes available")
136+ }
137+
138+ // Calculate max width needed for theme names
139+ maxWidth := 40 // Minimum width
140+ for _, themeName := range t.themes {
141+ if len(themeName) > maxWidth-4 { // Account for padding
142+ maxWidth = len(themeName) + 4
143+ }
144+ }
145+
146+ maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
147+
148+ // Build the theme list
149+ themeItems := make([]string, 0, len(t.themes))
150+ for i, themeName := range t.themes {
151+ itemStyle := baseStyle.Width(maxWidth)
152+
153+ if i == t.selectedIdx {
154+ itemStyle = itemStyle.
155+ Background(currentTheme.Primary()).
156+ Foreground(currentTheme.Background()).
157+ Bold(true)
158+ }
159+
160+ themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
161+ }
162+
163+ title := baseStyle.
164+ Foreground(currentTheme.Primary()).
165+ Bold(true).
166+ Width(maxWidth).
167+ Padding(0, 1).
168+ Render("Select Theme")
169+
170+ content := lipgloss.JoinVertical(
171+ lipgloss.Left,
172+ title,
173+ baseStyle.Width(maxWidth).Render(""),
174+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
175+ baseStyle.Width(maxWidth).Render(""),
176+ )
177+
178+ return baseStyle.Padding(1, 2).
179+ Border(lipgloss.RoundedBorder()).
180+ BorderBackground(currentTheme.Background()).
181+ BorderForeground(currentTheme.TextMuted()).
182+ Width(lipgloss.Width(content) + 4).
183+ Render(content)
184+}
185+
186+func (t *themeDialogCmp) BindingKeys() []key.Binding {
187+ return layout.KeyMapToSlice(themeKeys)
188+}
189+
190+// NewThemeDialogCmp creates a new theme switching dialog
191+func NewThemeDialogCmp() ThemeDialog {
192+ return &themeDialogCmp{
193+ themes: []string{},
194+ selectedIdx: 0,
195+ currentTheme: "",
196+ }
197+}
198+
internal/tui/components/logs/details.go+16−12
@@ -12,6 +12,7 @@ import (
1212 "github.com/opencode-ai/opencode/internal/logging"
1313 "github.com/opencode-ai/opencode/internal/tui/layout"
1414 "github.com/opencode-ai/opencode/internal/tui/styles"
15+ "github.com/opencode-ai/opencode/internal/tui/theme"
1516)
1617
1718type DetailComponent interface {
@@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4950
5051func (i *detailCmp) updateContent() {
5152 var content strings.Builder
53+ t := theme.CurrentTheme()
5254
5355 // Format the header with timestamp and level
54− timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
56+ timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
5557 levelStyle := getLevelStyle(i.currentLog.Level)
5658
5759 header := lipgloss.JoinHorizontal(
@@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() {
6567 content.WriteString("\n\n")
6668
6769 // Message with styling
68− messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
70+ messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
6971 content.WriteString(messageStyle.Render("Message:"))
7072 content.WriteString("\n")
7173 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
@@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() {
7375
7476 // Attributes section
7577 if len(i.currentLog.Attributes) > 0 {
76− attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
78+ attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
7779 content.WriteString(attrHeaderStyle.Render("Attributes:"))
7880 content.WriteString("\n")
7981
8082 // Create a table-like display for attributes
81− keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
82− valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
83+ keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
84+ valueStyle := lipgloss.NewStyle().Foreground(t.Text())
8385
8486 for _, attr := range i.currentLog.Attributes {
8587 attrLine := fmt.Sprintf("%s: %s",
@@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() {
9698
9799func getLevelStyle(level string) lipgloss.Style {
98100 style := lipgloss.NewStyle().Bold(true)
99−
101+ t := theme.CurrentTheme()
102+
100103 switch strings.ToLower(level) {
101104 case "info":
102− return style.Foreground(styles.Blue)
105+ return style.Foreground(t.Info())
103106 case "warn", "warning":
104− return style.Foreground(styles.Warning)
107+ return style.Foreground(t.Warning())
105108 case "error", "err":
106− return style.Foreground(styles.Error)
109+ return style.Foreground(t.Error())
107110 case "debug":
108− return style.Foreground(styles.Green)
111+ return style.Foreground(t.Success())
109112 default:
110− return style.Foreground(styles.Text)
113+ return style.Foreground(t.Text())
111114 }
112115}
113116
114117func (i *detailCmp) View() string {
115− return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background)
118+ t := theme.CurrentTheme()
119+ return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
116120}
117121
118122func (i *detailCmp) GetSize() (int, int) {
internal/tui/components/logs/table.go+7−4
@@ -11,6 +11,7 @@ import (
1111 "github.com/opencode-ai/opencode/internal/pubsub"
1212 "github.com/opencode-ai/opencode/internal/tui/layout"
1313 "github.com/opencode-ai/opencode/internal/tui/styles"
14+ "github.com/opencode-ai/opencode/internal/tui/theme"
1415 "github.com/opencode-ai/opencode/internal/tui/util"
1516)
1617
@@ -61,7 +62,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6162}
6263
6364func (i *tableCmp) View() string {
64− return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background)
65+ t := theme.CurrentTheme()
66+ defaultStyles := table.DefaultStyles()
67+ defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
68+ i.table.SetStyles(defaultStyles)
69+ return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
6570}
6671
6772func (i *tableCmp) GetSize() (int, int) {
@@ -121,11 +126,9 @@ func NewLogsTable() TableComponent {
121126 {Title: "Message", Width: 10},
122127 {Title: "Attributes", Width: 10},
123128 }
124− defaultStyles := table.DefaultStyles()
125− defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary)
129+
126130 tableModel := table.New(
127131 table.WithColumns(columns),
128− table.WithStyles(defaultStyles),
129132 )
130133 tableModel.Focus()
131134 return &tableCmp{
internal/tui/layout/container.go+8−29
@@ -4,7 +4,7 @@ import (
44 "github.com/charmbracelet/bubbles/key"
55 tea "github.com/charmbracelet/bubbletea"
66 "github.com/charmbracelet/lipgloss"
7− "github.com/opencode-ai/opencode/internal/tui/styles"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
88)
99
1010type Container interface {
@@ -29,9 +29,6 @@ type container struct {
2929 borderBottom bool
3030 borderLeft bool
3131 borderStyle lipgloss.Border
32− borderColor lipgloss.TerminalColor
33−
34− backgroundColor lipgloss.TerminalColor
3532}
3633
3734func (c *container) Init() tea.Cmd {
@@ -45,13 +42,12 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4542}
4643
4744func (c *container) View() string {
45+ t := theme.CurrentTheme()
4846 style := lipgloss.NewStyle()
4947 width := c.width
5048 height := c.height
51− // Apply background color if specified
52− if c.backgroundColor != nil {
53− style = style.Background(c.backgroundColor)
54− }
49+
50+ style = style.Background(t.Background())
5551
5652 // Apply border if any side is enabled
5753 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
@@ -69,11 +65,7 @@ func (c *container) View() string {
6965 width--
7066 }
7167 style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
72−
73− // Apply border color if specified
74− if c.borderColor != nil {
75− style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor)
76− }
68+ style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
7769 }
7870 style = style.
7971 Width(width).
@@ -132,11 +124,10 @@ func (c *container) BindingKeys() []key.Binding {
132124type ContainerOption func(*container)
133125
134126func NewContainer(content tea.Model, options ...ContainerOption) Container {
127+
135128 c := &container{
136− content: content,
137− borderColor: styles.BorderColor,
138− borderStyle: lipgloss.NormalBorder(),
139− backgroundColor: styles.Background,
129+ content: content,
130+ borderStyle: lipgloss.NormalBorder(),
140131 }
141132
142133 for _, option := range options {
@@ -201,12 +192,6 @@ func WithBorderStyle(style lipgloss.Border) ContainerOption {
201192 }
202193}
203194
204−func WithBorderColor(color lipgloss.TerminalColor) ContainerOption {
205− return func(c *container) {
206− c.borderColor = color
207− }
208−}
209−
210195func WithRoundedBorder() ContainerOption {
211196 return WithBorderStyle(lipgloss.RoundedBorder())
212197}
@@ -218,9 +203,3 @@ func WithThickBorder() ContainerOption {
218203func WithDoubleBorder() ContainerOption {
219204 return WithBorderStyle(lipgloss.DoubleBorder())
220205}
221−
222−func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption {
223− return func(c *container) {
224− c.backgroundColor = color
225− }
226−}
internal/tui/layout/overlay.go+7−3
@@ -9,6 +9,7 @@ import (
99 "github.com/muesli/reflow/truncate"
1010 "github.com/muesli/termenv"
1111 "github.com/opencode-ai/opencode/internal/tui/styles"
12+ "github.com/opencode-ai/opencode/internal/tui/theme"
1213 "github.com/opencode-ai/opencode/internal/tui/util"
1314)
1415
@@ -43,12 +44,15 @@ func PlaceOverlay(
4344 fgHeight := len(fgLines)
4445
4546 if shadow {
47+ t := theme.CurrentTheme()
48+ baseStyle := styles.BaseStyle()
49+
4650 var shadowbg string = ""
4751 shadowchar := lipgloss.NewStyle().
48− Background(styles.BackgroundDarker).
49− Foreground(styles.Background).
52+ Background(t.BackgroundDarker()).
53+ Foreground(t.Background()).
5054 Render("░")
51− bgchar := styles.BaseStyle.Render(" ")
55+ bgchar := baseStyle.Render(" ")
5256 for i := 0; i <= fgHeight; i++ {
5357 if i == 0 {
5458 shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
internal/tui/layout/split.go+8−14
@@ -4,7 +4,7 @@ import (
44 "github.com/charmbracelet/bubbles/key"
55 tea "github.com/charmbracelet/bubbletea"
66 "github.com/charmbracelet/lipgloss"
7− "github.com/opencode-ai/opencode/internal/tui/styles"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
88)
99
1010type SplitPaneLayout interface {
@@ -29,8 +29,6 @@ type splitPaneLayout struct {
2929 rightPanel Container
3030 leftPanel Container
3131 bottomPanel Container
32−
33− backgroundColor lipgloss.TerminalColor
3432}
3533
3634type SplitPaneOption func(*splitPaneLayout)
@@ -113,11 +111,13 @@ func (s *splitPaneLayout) View() string {
113111 finalView = topSection
114112 }
115113
116− if s.backgroundColor != nil && finalView != "" {
114+ if finalView != "" {
115+ t := theme.CurrentTheme()
116+
117117 style := lipgloss.NewStyle().
118118 Width(s.width).
119119 Height(s.height).
120− Background(s.backgroundColor)
120+ Background(t.Background())
121121
122122 return style.Render(finalView)
123123 }
@@ -241,10 +241,10 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding {
241241}
242242
243243func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
244+
244245 layout := &splitPaneLayout{
245− ratio: 0.7,
246− verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
247− backgroundColor: styles.Background,
246+ ratio: 0.7,
247+ verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
248248 }
249249 for _, option := range options {
250250 option(layout)
@@ -270,12 +270,6 @@ func WithRatio(ratio float64) SplitPaneOption {
270270 }
271271}
272272
273−func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
274− return func(s *splitPaneLayout) {
275− s.backgroundColor = color
276− }
277−}
278−
279273func WithBottomPanel(panel Container) SplitPaneOption {
280274 return func(s *splitPaneLayout) {
281275 s.bottomPanel = panel
internal/tui/page/chat.go−1
@@ -142,7 +142,6 @@ func NewChatPage(app *app.App) tea.Model {
142142 chat.NewMessagesCmp(app),
143143 layout.WithPadding(1, 1, 0, 1),
144144 )
145−
146145 editorContainer := layout.NewContainer(
147146 chat.NewEditorCmp(app),
148147 layout.WithBorder(true, false, false, false),
internal/tui/page/logs.go+3−3
@@ -42,7 +42,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4242}
4343
4444func (p *logsPage) View() string {
45− style := styles.BaseStyle.Width(p.width).Height(p.height)
45+ style := styles.BaseStyle().Width(p.width).Height(p.height)
4646 return style.Render(lipgloss.JoinVertical(lipgloss.Top,
4747 p.table.View(),
4848 p.details.View(),
@@ -77,7 +77,7 @@ func (p *logsPage) Init() tea.Cmd {
7777
7878func NewLogsPage() LogPage {
7979 return &logsPage{
80− table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
81− details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
80+ table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
81+ details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
8282 }
8383}
internal/tui/styles/huh.godeleted−46
@@ -1,46 +0,0 @@
1−package styles
2−
3−import (
4− "github.com/charmbracelet/huh"
5− "github.com/charmbracelet/lipgloss"
6−)
7−
8−func HuhTheme() *huh.Theme {
9− t := huh.ThemeBase()
10−
11− t.Focused.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
12− t.Focused.Title = t.Focused.Title.Foreground(Text)
13− t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(Text)
14− t.Focused.Directory = t.Focused.Directory.Foreground(Text)
15− t.Focused.Description = t.Focused.Description.Foreground(SubText0)
16− t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(Red)
17− t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(Red)
18− t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(Blue)
19− t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(Blue)
20− t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(Blue)
21− t.Focused.Option = t.Focused.Option.Foreground(Text)
22− t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(Blue)
23− t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(Green)
24− t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(Green)
25− t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(Text)
26− t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(Text)
27− t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(Base).Background(Blue)
28− t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(Text).Background(Base)
29−
30− t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Teal)
31− t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(Overlay0)
32− t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(Blue)
33−
34− t.Blurred = t.Focused
35− t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
36−
37− t.Help.Ellipsis = t.Help.Ellipsis.Foreground(SubText0)
38− t.Help.ShortKey = t.Help.ShortKey.Foreground(SubText0)
39− t.Help.ShortDesc = t.Help.ShortDesc.Foreground(Ovelay1)
40− t.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(SubText0)
41− t.Help.FullKey = t.Help.FullKey.Foreground(SubText0)
42− t.Help.FullDesc = t.Help.FullDesc.Foreground(Ovelay1)
43− t.Help.FullSeparator = t.Help.FullSeparator.Foreground(SubText0)
44−
45− return t
46−}
internal/tui/styles/markdown.go+222−879
@@ -1,8 +1,10 @@
11package styles
22
33import (
4+ "github.com/charmbracelet/glamour"
45 "github.com/charmbracelet/glamour/ansi"
56 "github.com/charmbracelet/lipgloss"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
68)
79
810const defaultMargin = 1
@@ -12,930 +14,271 @@ func boolPtr(b bool) *bool { return &b }
1214func stringPtr(s string) *string { return &s }
1315func uintPtr(u uint) *uint { return &u }
1416
15−// CatppuccinMarkdownStyle is the Catppuccin Mocha style for Glamour markdown rendering.
16−func CatppuccinMarkdownStyle() ansi.StyleConfig {
17− isDark := lipgloss.HasDarkBackground()
18− if isDark {
19− return catppuccinDark
20− }
21− return catppuccinLight
17+// returns a glamour TermRenderer configured with the current theme
18+func GetMarkdownRenderer(width int) *glamour.TermRenderer {
19+ r, _ := glamour.NewTermRenderer(
20+ glamour.WithStyles(generateMarkdownStyleConfig()),
21+ glamour.WithWordWrap(width),
22+ )
23+ return r
2224}
2325
24−var catppuccinDark = ansi.StyleConfig{
25− Document: ansi.StyleBlock{
26− StylePrimitive: ansi.StylePrimitive{
27− BlockPrefix: "\n",
28− BlockSuffix: "",
29− Color: stringPtr(dark.Text().Hex),
30− },
31− Margin: uintPtr(defaultMargin),
32− },
33− BlockQuote: ansi.StyleBlock{
34− StylePrimitive: ansi.StylePrimitive{
35− Color: stringPtr(dark.Yellow().Hex),
36− Italic: boolPtr(true),
37− Prefix: "┃ ",
38− },
39− Indent: uintPtr(1),
40− IndentToken: stringPtr(BaseStyle.Render(" ")),
41− },
42− List: ansi.StyleList{
43− LevelIndent: defaultMargin,
44− StyleBlock: ansi.StyleBlock{
45− IndentToken: stringPtr(BaseStyle.Render(" ")),
46− StylePrimitive: ansi.StylePrimitive{
47− Color: stringPtr(dark.Text().Hex),
48− },
49− },
50− },
51− Heading: ansi.StyleBlock{
52− StylePrimitive: ansi.StylePrimitive{
53− BlockSuffix: "\n",
54− Color: stringPtr(dark.Mauve().Hex),
55− Bold: boolPtr(true),
56− },
57− },
58− H1: ansi.StyleBlock{
59− StylePrimitive: ansi.StylePrimitive{
60− Prefix: "# ",
61− Color: stringPtr(dark.Lavender().Hex),
62− Bold: boolPtr(true),
63− BlockPrefix: "\n",
64− },
65− },
66− H2: ansi.StyleBlock{
67− StylePrimitive: ansi.StylePrimitive{
68− Prefix: "## ",
69− Color: stringPtr(dark.Mauve().Hex),
70− Bold: boolPtr(true),
71− },
72− },
73− H3: ansi.StyleBlock{
74− StylePrimitive: ansi.StylePrimitive{
75− Prefix: "### ",
76− Color: stringPtr(dark.Pink().Hex),
77− Bold: boolPtr(true),
78− },
79− },
80− H4: ansi.StyleBlock{
81− StylePrimitive: ansi.StylePrimitive{
82− Prefix: "#### ",
83− Color: stringPtr(dark.Flamingo().Hex),
84− Bold: boolPtr(true),
85− },
86− },
87− H5: ansi.StyleBlock{
88− StylePrimitive: ansi.StylePrimitive{
89− Prefix: "##### ",
90− Color: stringPtr(dark.Rosewater().Hex),
91− Bold: boolPtr(true),
92− },
93− },
94− H6: ansi.StyleBlock{
95− StylePrimitive: ansi.StylePrimitive{
96− Prefix: "###### ",
97− Color: stringPtr(dark.Rosewater().Hex),
98− Bold: boolPtr(true),
99− },
100− },
101− Strikethrough: ansi.StylePrimitive{
102− CrossedOut: boolPtr(true),
103− Color: stringPtr(dark.Overlay1().Hex),
104− },
105− Emph: ansi.StylePrimitive{
106− Color: stringPtr(dark.Yellow().Hex),
107− Italic: boolPtr(true),
108− },
109− Strong: ansi.StylePrimitive{
110− Bold: boolPtr(true),
111− Color: stringPtr(dark.Peach().Hex),
112− },
113− HorizontalRule: ansi.StylePrimitive{
114− Color: stringPtr(dark.Overlay0().Hex),
115− Format: "\n─────────────────────────────────────────\n",
116− },
117− Item: ansi.StylePrimitive{
118− BlockPrefix: "• ",
119− Color: stringPtr(dark.Blue().Hex),
120− },
121− Enumeration: ansi.StylePrimitive{
122− BlockPrefix: ". ",
123− Color: stringPtr(dark.Sky().Hex),
124− },
125− Task: ansi.StyleTask{
126− StylePrimitive: ansi.StylePrimitive{},
127− Ticked: "[✓] ",
128− Unticked: "[ ] ",
129− },
130− Link: ansi.StylePrimitive{
131− Color: stringPtr(dark.Sky().Hex),
132− Underline: boolPtr(true),
133− },
134− LinkText: ansi.StylePrimitive{
135− Color: stringPtr(dark.Pink().Hex),
136− Bold: boolPtr(true),
137− },
138− Image: ansi.StylePrimitive{
139− Color: stringPtr(dark.Sapphire().Hex),
140− Underline: boolPtr(true),
141− Format: "🖼 {{.text}}",
142− },
143− ImageText: ansi.StylePrimitive{
144− Color: stringPtr(dark.Pink().Hex),
145− Format: "{{.text}}",
146− },
147− Code: ansi.StyleBlock{
148− StylePrimitive: ansi.StylePrimitive{
149− Color: stringPtr(dark.Green().Hex),
150− Prefix: "",
151− Suffix: "",
152− },
153− },
154− CodeBlock: ansi.StyleCodeBlock{
155− StyleBlock: ansi.StyleBlock{
26+// creates an ansi.StyleConfig for markdown rendering
27+// using adaptive colors from the provided theme.
28+func generateMarkdownStyleConfig() ansi.StyleConfig {
29+ t := theme.CurrentTheme()
30+
31+ return ansi.StyleConfig{
32+ Document: ansi.StyleBlock{
15633 StylePrimitive: ansi.StylePrimitive{
157− Prefix: " ",
158− Color: stringPtr(dark.Text().Hex),
34+ BlockPrefix: "",
35+ BlockSuffix: "",
36+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
15937 },
160−
16138 Margin: uintPtr(defaultMargin),
16239 },
163− Chroma: &ansi.Chroma{
164− Text: ansi.StylePrimitive{
165− Color: stringPtr(dark.Text().Hex),
166− },
167− Error: ansi.StylePrimitive{
168− Color: stringPtr(dark.Text().Hex),
169− },
170− Comment: ansi.StylePrimitive{
171− Color: stringPtr(dark.Overlay1().Hex),
172− },
173− CommentPreproc: ansi.StylePrimitive{
174− Color: stringPtr(dark.Pink().Hex),
175− },
176− Keyword: ansi.StylePrimitive{
177− Color: stringPtr(dark.Pink().Hex),
178− },
179− KeywordReserved: ansi.StylePrimitive{
180− Color: stringPtr(dark.Pink().Hex),
181− },
182− KeywordNamespace: ansi.StylePrimitive{
183− Color: stringPtr(dark.Pink().Hex),
184− },
185− KeywordType: ansi.StylePrimitive{
186− Color: stringPtr(dark.Sky().Hex),
187− },
188− Operator: ansi.StylePrimitive{
189− Color: stringPtr(dark.Pink().Hex),
190− },
191− Punctuation: ansi.StylePrimitive{
192− Color: stringPtr(dark.Text().Hex),
193− },
194− Name: ansi.StylePrimitive{
195− Color: stringPtr(dark.Sky().Hex),
196− },
197− NameBuiltin: ansi.StylePrimitive{
198− Color: stringPtr(dark.Sky().Hex),
199− },
200− NameTag: ansi.StylePrimitive{
201− Color: stringPtr(dark.Pink().Hex),
202− },
203− NameAttribute: ansi.StylePrimitive{
204− Color: stringPtr(dark.Green().Hex),
205− },
206− NameClass: ansi.StylePrimitive{
207− Color: stringPtr(dark.Sky().Hex),
208− },
209− NameConstant: ansi.StylePrimitive{
210− Color: stringPtr(dark.Mauve().Hex),
211− },
212− NameDecorator: ansi.StylePrimitive{
213− Color: stringPtr(dark.Green().Hex),
214− },
215− NameFunction: ansi.StylePrimitive{
216− Color: stringPtr(dark.Green().Hex),
217− },
218− LiteralNumber: ansi.StylePrimitive{
219− Color: stringPtr(dark.Teal().Hex),
220− },
221− LiteralString: ansi.StylePrimitive{
222− Color: stringPtr(dark.Yellow().Hex),
223− },
224− LiteralStringEscape: ansi.StylePrimitive{
225− Color: stringPtr(dark.Pink().Hex),
226− },
227− GenericDeleted: ansi.StylePrimitive{
228− Color: stringPtr(dark.Red().Hex),
229− },
230− GenericEmph: ansi.StylePrimitive{
231− Color: stringPtr(dark.Yellow().Hex),
40+ BlockQuote: ansi.StyleBlock{
41+ StylePrimitive: ansi.StylePrimitive{
42+ Color: stringPtr(adaptiveColorToString(t.MarkdownBlockQuote())),
23243 Italic: boolPtr(true),
44+ Prefix: "┃ ",
23345 },
234− GenericInserted: ansi.StylePrimitive{
235− Color: stringPtr(dark.Green().Hex),
236− },
237− GenericStrong: ansi.StylePrimitive{
238− Color: stringPtr(dark.Peach().Hex),
239− Bold: boolPtr(true),
240− },
241− GenericSubheading: ansi.StylePrimitive{
242− Color: stringPtr(dark.Mauve().Hex),
46+ Indent: uintPtr(1),
47+ IndentToken: stringPtr(BaseStyle().Render(" ")),
48+ },
49+ List: ansi.StyleList{
50+ LevelIndent: defaultMargin,
51+ StyleBlock: ansi.StyleBlock{
52+ IndentToken: stringPtr(BaseStyle().Render(" ")),
53+ StylePrimitive: ansi.StylePrimitive{
54+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
55+ },
24356 },
24457 },
245− },
246− Table: ansi.StyleTable{
247− StyleBlock: ansi.StyleBlock{
58+ Heading: ansi.StyleBlock{
24859 StylePrimitive: ansi.StylePrimitive{
249− BlockPrefix: "\n",
25060 BlockSuffix: "\n",
61+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
62+ Bold: boolPtr(true),
25163 },
25264 },
253− CenterSeparator: stringPtr("┼"),
254− ColumnSeparator: stringPtr("│"),
255− RowSeparator: stringPtr("─"),
256− },
257− DefinitionDescription: ansi.StylePrimitive{
258− BlockPrefix: "\n ❯ ",
259− Color: stringPtr(dark.Sapphire().Hex),
260− },
261−}
262−
263−var catppuccinLight = ansi.StyleConfig{
264− Document: ansi.StyleBlock{
265− StylePrimitive: ansi.StylePrimitive{
266− BlockPrefix: "\n",
267− BlockSuffix: "\n",
268− Color: stringPtr(light.Text().Hex),
269− },
270− Margin: uintPtr(defaultMargin),
271− },
272− BlockQuote: ansi.StyleBlock{
273− StylePrimitive: ansi.StylePrimitive{
274− Color: stringPtr(light.Yellow().Hex),
275− Italic: boolPtr(true),
276− Prefix: "┃ ",
277− },
278− Indent: uintPtr(1),
279− Margin: uintPtr(defaultMargin),
280− },
281− List: ansi.StyleList{
282− LevelIndent: defaultMargin,
283− StyleBlock: ansi.StyleBlock{
65+ H1: ansi.StyleBlock{
28466 StylePrimitive: ansi.StylePrimitive{
285− Color: stringPtr(light.Text().Hex),
67+ Prefix: "# ",
68+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
69+ Bold: boolPtr(true),
28670 },
28771 },
288− },
289− Heading: ansi.StyleBlock{
290− StylePrimitive: ansi.StylePrimitive{
291− BlockSuffix: "\n",
292− Color: stringPtr(light.Mauve().Hex),
293− Bold: boolPtr(true),
294− },
295− },
296− H1: ansi.StyleBlock{
297− StylePrimitive: ansi.StylePrimitive{
298− Prefix: "# ",
299− Color: stringPtr(light.Lavender().Hex),
300− Bold: boolPtr(true),
301− BlockPrefix: "\n",
302− },
303− },
304− H2: ansi.StyleBlock{
305− StylePrimitive: ansi.StylePrimitive{
306− Prefix: "## ",
307− Color: stringPtr(light.Mauve().Hex),
308− Bold: boolPtr(true),
309− },
310− },
311− H3: ansi.StyleBlock{
312− StylePrimitive: ansi.StylePrimitive{
313− Prefix: "### ",
314− Color: stringPtr(light.Pink().Hex),
315− Bold: boolPtr(true),
316− },
317− },
318− H4: ansi.StyleBlock{
319− StylePrimitive: ansi.StylePrimitive{
320− Prefix: "#### ",
321− Color: stringPtr(light.Flamingo().Hex),
322− Bold: boolPtr(true),
323− },
324− },
325− H5: ansi.StyleBlock{
326− StylePrimitive: ansi.StylePrimitive{
327− Prefix: "##### ",
328− Color: stringPtr(light.Rosewater().Hex),
329− Bold: boolPtr(true),
330− },
331− },
332− H6: ansi.StyleBlock{
333− StylePrimitive: ansi.StylePrimitive{
334− Prefix: "###### ",
335− Color: stringPtr(light.Rosewater().Hex),
336− Bold: boolPtr(true),
337− },
338− },
339− Strikethrough: ansi.StylePrimitive{
340− CrossedOut: boolPtr(true),
341− Color: stringPtr(light.Overlay1().Hex),
342− },
343− Emph: ansi.StylePrimitive{
344− Color: stringPtr(light.Yellow().Hex),
345− Italic: boolPtr(true),
346− },
347− Strong: ansi.StylePrimitive{
348− Bold: boolPtr(true),
349− Color: stringPtr(light.Peach().Hex),
350− },
351− HorizontalRule: ansi.StylePrimitive{
352− Color: stringPtr(light.Overlay0().Hex),
353− Format: "\n─────────────────────────────────────────\n",
354− },
355− Item: ansi.StylePrimitive{
356− BlockPrefix: "• ",
357− Color: stringPtr(light.Blue().Hex),
358− },
359− Enumeration: ansi.StylePrimitive{
360− BlockPrefix: ". ",
361− Color: stringPtr(light.Sky().Hex),
362− },
363− Task: ansi.StyleTask{
364− StylePrimitive: ansi.StylePrimitive{},
365− Ticked: "[✓] ",
366− Unticked: "[ ] ",
367− },
368− Link: ansi.StylePrimitive{
369− Color: stringPtr(light.Sky().Hex),
370− Underline: boolPtr(true),
371− },
372− LinkText: ansi.StylePrimitive{
373− Color: stringPtr(light.Pink().Hex),
374− Bold: boolPtr(true),
375− },
376− Image: ansi.StylePrimitive{
377− Color: stringPtr(light.Sapphire().Hex),
378− Underline: boolPtr(true),
379− Format: "🖼 {{.text}}",
380− },
381− ImageText: ansi.StylePrimitive{
382− Color: stringPtr(light.Pink().Hex),
383− Format: "{{.text}}",
384− },
385− Code: ansi.StyleBlock{
386− StylePrimitive: ansi.StylePrimitive{
387− Color: stringPtr(light.Green().Hex),
388− Prefix: " ",
389− Suffix: " ",
390− },
391− },
392− CodeBlock: ansi.StyleCodeBlock{
393− StyleBlock: ansi.StyleBlock{
72+ H2: ansi.StyleBlock{
39473 StylePrimitive: ansi.StylePrimitive{
395− Prefix: " ",
396− Color: stringPtr(light.Text().Hex),
74+ Prefix: "## ",
75+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
76+ Bold: boolPtr(true),
39777 },
398−
399− Margin: uintPtr(defaultMargin),
40078 },
401− Chroma: &ansi.Chroma{
402− Text: ansi.StylePrimitive{
403− Color: stringPtr(light.Text().Hex),
404− },
405− Error: ansi.StylePrimitive{
406− Color: stringPtr(light.Text().Hex),
407− },
408− Comment: ansi.StylePrimitive{
409− Color: stringPtr(light.Overlay1().Hex),
410− },
411− CommentPreproc: ansi.StylePrimitive{
412− Color: stringPtr(light.Pink().Hex),
413− },
414− Keyword: ansi.StylePrimitive{
415− Color: stringPtr(light.Pink().Hex),
416− },
417− KeywordReserved: ansi.StylePrimitive{
418− Color: stringPtr(light.Pink().Hex),
419− },
420− KeywordNamespace: ansi.StylePrimitive{
421− Color: stringPtr(light.Pink().Hex),
422− },
423− KeywordType: ansi.StylePrimitive{
424− Color: stringPtr(light.Sky().Hex),
425− },
426− Operator: ansi.StylePrimitive{
427− Color: stringPtr(light.Pink().Hex),
428− },
429− Punctuation: ansi.StylePrimitive{
430− Color: stringPtr(light.Text().Hex),
431− },
432− Name: ansi.StylePrimitive{
433− Color: stringPtr(light.Sky().Hex),
434− },
435− NameBuiltin: ansi.StylePrimitive{
436− Color: stringPtr(light.Sky().Hex),
437− },
438− NameTag: ansi.StylePrimitive{
439− Color: stringPtr(light.Pink().Hex),
440− },
441− NameAttribute: ansi.StylePrimitive{
442− Color: stringPtr(light.Green().Hex),
443− },
444− NameClass: ansi.StylePrimitive{
445− Color: stringPtr(light.Sky().Hex),
446− },
447− NameConstant: ansi.StylePrimitive{
448− Color: stringPtr(light.Mauve().Hex),
449− },
450− NameDecorator: ansi.StylePrimitive{
451− Color: stringPtr(light.Green().Hex),
452− },
453− NameFunction: ansi.StylePrimitive{
454− Color: stringPtr(light.Green().Hex),
455− },
456− LiteralNumber: ansi.StylePrimitive{
457− Color: stringPtr(light.Teal().Hex),
458− },
459− LiteralString: ansi.StylePrimitive{
460− Color: stringPtr(light.Yellow().Hex),
461− },
462− LiteralStringEscape: ansi.StylePrimitive{
463− Color: stringPtr(light.Pink().Hex),
464− },
465− GenericDeleted: ansi.StylePrimitive{
466− Color: stringPtr(light.Red().Hex),
467− },
468− GenericEmph: ansi.StylePrimitive{
469− Color: stringPtr(light.Yellow().Hex),
470− Italic: boolPtr(true),
471− },
472− GenericInserted: ansi.StylePrimitive{
473− Color: stringPtr(light.Green().Hex),
474− },
475− GenericStrong: ansi.StylePrimitive{
476− Color: stringPtr(light.Peach().Hex),
477− Bold: boolPtr(true),
478− },
479− GenericSubheading: ansi.StylePrimitive{
480− Color: stringPtr(light.Mauve().Hex),
481− },
482− },
483− },
484− Table: ansi.StyleTable{
485− StyleBlock: ansi.StyleBlock{
79+ H3: ansi.StyleBlock{
48680 StylePrimitive: ansi.StylePrimitive{
487− BlockPrefix: "\n",
488− BlockSuffix: "\n",
81+ Prefix: "### ",
82+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
83+ Bold: boolPtr(true),
48984 },
49085 },
491− CenterSeparator: stringPtr("┼"),
492− ColumnSeparator: stringPtr("│"),
493− RowSeparator: stringPtr("─"),
494− },
495− DefinitionDescription: ansi.StylePrimitive{
496− BlockPrefix: "\n ❯ ",
497− Color: stringPtr(light.Sapphire().Hex),
498− },
499−}
500−
501−func MarkdownTheme(focused bool) ansi.StyleConfig {
502− if !focused {
503− return ASCIIStyleConfig
504− } else {
505− return DraculaStyleConfig
506− }
507−}
508−
509−const (
510− defaultListIndent = 2
511− defaultListLevelIndent = 4
512−)
513−
514−var ASCIIStyleConfig = ansi.StyleConfig{
515− Document: ansi.StyleBlock{
516− StylePrimitive: ansi.StylePrimitive{
517− BackgroundColor: stringPtr(Background.Dark),
518− Color: stringPtr(ForgroundDim.Dark),
519− },
520− Indent: uintPtr(1),
521− IndentToken: stringPtr(BaseStyle.Render(" ")),
522− },
523− BlockQuote: ansi.StyleBlock{
524− StylePrimitive: ansi.StylePrimitive{
525− BackgroundColor: stringPtr(Background.Dark),
526− },
527− Indent: uintPtr(1),
528− IndentToken: stringPtr("| "),
529− },
530− Paragraph: ansi.StyleBlock{
531− StylePrimitive: ansi.StylePrimitive{
532− BackgroundColor: stringPtr(Background.Dark),
533− },
534− },
535− List: ansi.StyleList{
536− StyleBlock: ansi.StyleBlock{
537− IndentToken: stringPtr(BaseStyle.Render(" ")),
86+ H4: ansi.StyleBlock{
53887 StylePrimitive: ansi.StylePrimitive{
539− BackgroundColor: stringPtr(Background.Dark),
88+ Prefix: "#### ",
89+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
90+ Bold: boolPtr(true),
54091 },
54192 },
542− LevelIndent: defaultListLevelIndent,
543− },
544− Heading: ansi.StyleBlock{
545− StylePrimitive: ansi.StylePrimitive{
546− BackgroundColor: stringPtr(Background.Dark),
547− BlockSuffix: "\n",
548− },
549− },
550− H1: ansi.StyleBlock{
551− StylePrimitive: ansi.StylePrimitive{
552− BackgroundColor: stringPtr(Background.Dark),
553− Prefix: "# ",
554− },
555− },
556− H2: ansi.StyleBlock{
557− StylePrimitive: ansi.StylePrimitive{
558− BackgroundColor: stringPtr(Background.Dark),
559− Prefix: "## ",
560− },
561− },
562− H3: ansi.StyleBlock{
563− StylePrimitive: ansi.StylePrimitive{
564− BackgroundColor: stringPtr(Background.Dark),
565− Prefix: "### ",
566− },
567− },
568− H4: ansi.StyleBlock{
569− StylePrimitive: ansi.StylePrimitive{
570− BackgroundColor: stringPtr(Background.Dark),
571− Prefix: "#### ",
572− },
573− },
574− H5: ansi.StyleBlock{
575− StylePrimitive: ansi.StylePrimitive{
576− BackgroundColor: stringPtr(Background.Dark),
577− Prefix: "##### ",
578− },
579− },
580− H6: ansi.StyleBlock{
581− StylePrimitive: ansi.StylePrimitive{
582− BackgroundColor: stringPtr(Background.Dark),
583− Prefix: "###### ",
584− },
585− },
586− Strikethrough: ansi.StylePrimitive{
587− BackgroundColor: stringPtr(Background.Dark),
588− BlockPrefix: "~~",
589− BlockSuffix: "~~",
590− },
591− Emph: ansi.StylePrimitive{
592− BackgroundColor: stringPtr(Background.Dark),
593− BlockPrefix: "*",
594− BlockSuffix: "*",
595− },
596− Strong: ansi.StylePrimitive{
597− BackgroundColor: stringPtr(Background.Dark),
598− BlockPrefix: "**",
599− BlockSuffix: "**",
600− },
601− HorizontalRule: ansi.StylePrimitive{
602− BackgroundColor: stringPtr(Background.Dark),
603− Format: "\n--------\n",
604− },
605− Item: ansi.StylePrimitive{
606− BlockPrefix: "• ",
607− BackgroundColor: stringPtr(Background.Dark),
608− },
609− Enumeration: ansi.StylePrimitive{
610− BlockPrefix: ". ",
611− BackgroundColor: stringPtr(Background.Dark),
612− },
613− Task: ansi.StyleTask{
614− Ticked: "[x] ",
615− Unticked: "[ ] ",
616− StylePrimitive: ansi.StylePrimitive{
617− BackgroundColor: stringPtr(Background.Dark),
618− },
619− },
620− ImageText: ansi.StylePrimitive{
621− BackgroundColor: stringPtr(Background.Dark),
622− Format: "Image: {{.text}} →",
623− },
624− Code: ansi.StyleBlock{
625− StylePrimitive: ansi.StylePrimitive{
626− BlockPrefix: "`",
627− BlockSuffix: "`",
628− BackgroundColor: stringPtr(Background.Dark),
629− },
630− },
631− CodeBlock: ansi.StyleCodeBlock{
632− StyleBlock: ansi.StyleBlock{
93+ H5: ansi.StyleBlock{
63394 StylePrimitive: ansi.StylePrimitive{
634− BackgroundColor: stringPtr(Background.Dark),
95+ Prefix: "##### ",
96+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
97+ Bold: boolPtr(true),
63598 },
636− Margin: uintPtr(defaultMargin),
63799 },
638− },
639− Table: ansi.StyleTable{
640− StyleBlock: ansi.StyleBlock{
100+ H6: ansi.StyleBlock{
641101 StylePrimitive: ansi.StylePrimitive{
642− BackgroundColor: stringPtr(Background.Dark),
102+ Prefix: "###### ",
103+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
104+ Bold: boolPtr(true),
643105 },
644− IndentToken: stringPtr(BaseStyle.Render(" ")),
645− },
646− CenterSeparator: stringPtr("|"),
647− ColumnSeparator: stringPtr("|"),
648− RowSeparator: stringPtr("-"),
649− },
650− DefinitionDescription: ansi.StylePrimitive{
651− BackgroundColor: stringPtr(Background.Dark),
652− BlockPrefix: "\n* ",
653− },
654−}
655−
656−var DraculaStyleConfig = ansi.StyleConfig{
657− Document: ansi.StyleBlock{
658− StylePrimitive: ansi.StylePrimitive{
659− Color: stringPtr(Forground.Dark),
660− BackgroundColor: stringPtr(Background.Dark),
661− },
662− Indent: uintPtr(defaultMargin),
663− IndentToken: stringPtr(BaseStyle.Render(" ")),
664− },
665− BlockQuote: ansi.StyleBlock{
666− StylePrimitive: ansi.StylePrimitive{
667− Color: stringPtr("#f1fa8c"),
668− Italic: boolPtr(true),
669− BackgroundColor: stringPtr(Background.Dark),
670106 },
671− Indent: uintPtr(defaultMargin),
672− IndentToken: stringPtr(BaseStyle.Render(" ")),
673− },
674− Paragraph: ansi.StyleBlock{
675− StylePrimitive: ansi.StylePrimitive{
676− BackgroundColor: stringPtr(Background.Dark),
107+ Strikethrough: ansi.StylePrimitive{
108+ CrossedOut: boolPtr(true),
109+ Color: stringPtr(adaptiveColorToString(t.TextMuted())),
677110 },
678− },
679− List: ansi.StyleList{
680− LevelIndent: defaultMargin,
681− StyleBlock: ansi.StyleBlock{
682− IndentToken: stringPtr(BaseStyle.Render(" ")),
683− StylePrimitive: ansi.StylePrimitive{
684− Color: stringPtr(Forground.Dark),
685− BackgroundColor: stringPtr(Background.Dark),
686− },
111+ Emph: ansi.StylePrimitive{
112+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
113+ Italic: boolPtr(true),
687114 },
688− },
689− Heading: ansi.StyleBlock{
690− StylePrimitive: ansi.StylePrimitive{
691− BlockSuffix: "\n",
692− Color: stringPtr(PrimaryColor.Dark),
693− Bold: boolPtr(true),
694− BackgroundColor: stringPtr(Background.Dark),
115+ Strong: ansi.StylePrimitive{
116+ Bold: boolPtr(true),
117+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
695118 },
696− },
697− H1: ansi.StyleBlock{
698− StylePrimitive: ansi.StylePrimitive{
699− Prefix: "# ",
700− BackgroundColor: stringPtr(Background.Dark),
119+ HorizontalRule: ansi.StylePrimitive{
120+ Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())),
121+ Format: "\n─────────────────────────────────────────\n",
701122 },
702− },
703− H2: ansi.StyleBlock{
704− StylePrimitive: ansi.StylePrimitive{
705− Prefix: "## ",
706− BackgroundColor: stringPtr(Background.Dark),
123+ Item: ansi.StylePrimitive{
124+ BlockPrefix: "• ",
125+ Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())),
707126 },
708− },
709− H3: ansi.StyleBlock{
710− StylePrimitive: ansi.StylePrimitive{
711− Prefix: "### ",
712− BackgroundColor: stringPtr(Background.Dark),
127+ Enumeration: ansi.StylePrimitive{
128+ BlockPrefix: ". ",
129+ Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())),
713130 },
714− },
715− H4: ansi.StyleBlock{
716− StylePrimitive: ansi.StylePrimitive{
717− Prefix: "#### ",
718− BackgroundColor: stringPtr(Background.Dark),
131+ Task: ansi.StyleTask{
132+ StylePrimitive: ansi.StylePrimitive{},
133+ Ticked: "[✓] ",
134+ Unticked: "[ ] ",
719135 },
720− },
721− H5: ansi.StyleBlock{
722− StylePrimitive: ansi.StylePrimitive{
723− Prefix: "##### ",
724− BackgroundColor: stringPtr(Background.Dark),
136+ Link: ansi.StylePrimitive{
137+ Color: stringPtr(adaptiveColorToString(t.MarkdownLink())),
138+ Underline: boolPtr(true),
725139 },
726− },
727− H6: ansi.StyleBlock{
728− StylePrimitive: ansi.StylePrimitive{
729− Prefix: "###### ",
730− BackgroundColor: stringPtr(Background.Dark),
140+ LinkText: ansi.StylePrimitive{
141+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
142+ Bold: boolPtr(true),
731143 },
732− },
733− Strikethrough: ansi.StylePrimitive{
734− CrossedOut: boolPtr(true),
735− BackgroundColor: stringPtr(Background.Dark),
736− },
737− Emph: ansi.StylePrimitive{
738− Color: stringPtr("#f1fa8c"),
739− Italic: boolPtr(true),
740− BackgroundColor: stringPtr(Background.Dark),
741− },
742− Strong: ansi.StylePrimitive{
743− Bold: boolPtr(true),
744− Color: stringPtr(Blue.Dark),
745− BackgroundColor: stringPtr(Background.Dark),
746− },
747− HorizontalRule: ansi.StylePrimitive{
748− Color: stringPtr("#6272A4"),
749− Format: "\n--------\n",
750− BackgroundColor: stringPtr(Background.Dark),
751− },
752− Item: ansi.StylePrimitive{
753− BlockPrefix: "• ",
754− BackgroundColor: stringPtr(Background.Dark),
755− },
756− Enumeration: ansi.StylePrimitive{
757− BlockPrefix: ". ",
758− Color: stringPtr("#8be9fd"),
759− BackgroundColor: stringPtr(Background.Dark),
760− },
761− Task: ansi.StyleTask{
762− StylePrimitive: ansi.StylePrimitive{
763− BackgroundColor: stringPtr(Background.Dark),
144+ Image: ansi.StylePrimitive{
145+ Color: stringPtr(adaptiveColorToString(t.MarkdownImage())),
146+ Underline: boolPtr(true),
147+ Format: "🖼 {{.text}}",
764148 },
765− Ticked: "[✓] ",
766− Unticked: "[ ] ",
767− },
768− Link: ansi.StylePrimitive{
769− Color: stringPtr("#8be9fd"),
770− Underline: boolPtr(true),
771− BackgroundColor: stringPtr(Background.Dark),
772− },
773− LinkText: ansi.StylePrimitive{
774− Color: stringPtr("#ff79c6"),
775− BackgroundColor: stringPtr(Background.Dark),
776− },
777− Image: ansi.StylePrimitive{
778− Color: stringPtr("#8be9fd"),
779− Underline: boolPtr(true),
780− BackgroundColor: stringPtr(Background.Dark),
781− },
782− ImageText: ansi.StylePrimitive{
783− Color: stringPtr("#ff79c6"),
784− Format: "Image: {{.text}} →",
785− BackgroundColor: stringPtr(Background.Dark),
786− },
787− Code: ansi.StyleBlock{
788− StylePrimitive: ansi.StylePrimitive{
789− Color: stringPtr("#50fa7b"),
790− BackgroundColor: stringPtr(Background.Dark),
149+ ImageText: ansi.StylePrimitive{
150+ Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())),
151+ Format: "{{.text}}",
791152 },
792− },
793− Text: ansi.StylePrimitive{
794− BackgroundColor: stringPtr(Background.Dark),
795− },
796− DefinitionList: ansi.StyleBlock{},
797− CodeBlock: ansi.StyleCodeBlock{
798− StyleBlock: ansi.StyleBlock{
153+ Code: ansi.StyleBlock{
799154 StylePrimitive: ansi.StylePrimitive{
800− Color: stringPtr(Blue.Dark),
801− BackgroundColor: stringPtr(Background.Dark),
802− },
803− Margin: uintPtr(defaultMargin),
804− },
805− Chroma: &ansi.Chroma{
806− NameOther: ansi.StylePrimitive{
807− BackgroundColor: stringPtr(Background.Dark),
808− },
809− Literal: ansi.StylePrimitive{
810− BackgroundColor: stringPtr(Background.Dark),
811− },
812− NameException: ansi.StylePrimitive{
813− BackgroundColor: stringPtr(Background.Dark),
814− },
815− LiteralDate: ansi.StylePrimitive{
816− BackgroundColor: stringPtr(Background.Dark),
817− },
818− Text: ansi.StylePrimitive{
819− Color: stringPtr(Forground.Dark),
820− BackgroundColor: stringPtr(Background.Dark),
821− },
822− Error: ansi.StylePrimitive{
823− Color: stringPtr("#f8f8f2"),
824− BackgroundColor: stringPtr("#ff5555"),
825− },
826− Comment: ansi.StylePrimitive{
827− Color: stringPtr("#6272A4"),
828− BackgroundColor: stringPtr(Background.Dark),
829− },
830− CommentPreproc: ansi.StylePrimitive{
831− Color: stringPtr("#ff79c6"),
832− BackgroundColor: stringPtr(Background.Dark),
833− },
834− Keyword: ansi.StylePrimitive{
835− Color: stringPtr("#ff79c6"),
836− BackgroundColor: stringPtr(Background.Dark),
837− },
838− KeywordReserved: ansi.StylePrimitive{
839− Color: stringPtr("#ff79c6"),
840− BackgroundColor: stringPtr(Background.Dark),
841− },
842− KeywordNamespace: ansi.StylePrimitive{
843− Color: stringPtr("#ff79c6"),
844− BackgroundColor: stringPtr(Background.Dark),
845− },
846− KeywordType: ansi.StylePrimitive{
847− Color: stringPtr("#8be9fd"),
848− BackgroundColor: stringPtr(Background.Dark),
849− },
850− Operator: ansi.StylePrimitive{
851− Color: stringPtr("#ff79c6"),
852− BackgroundColor: stringPtr(Background.Dark),
853− },
854− Punctuation: ansi.StylePrimitive{
855− Color: stringPtr(Forground.Dark),
856− BackgroundColor: stringPtr(Background.Dark),
857− },
858− Name: ansi.StylePrimitive{
859− Color: stringPtr("#8be9fd"),
860− BackgroundColor: stringPtr(Background.Dark),
861− },
862− NameBuiltin: ansi.StylePrimitive{
863− Color: stringPtr("#8be9fd"),
864− BackgroundColor: stringPtr(Background.Dark),
865− },
866− NameTag: ansi.StylePrimitive{
867− Color: stringPtr("#ff79c6"),
868− BackgroundColor: stringPtr(Background.Dark),
869− },
870− NameAttribute: ansi.StylePrimitive{
871− Color: stringPtr("#50fa7b"),
872− BackgroundColor: stringPtr(Background.Dark),
873− },
874− NameClass: ansi.StylePrimitive{
875− Color: stringPtr("#8be9fd"),
876− BackgroundColor: stringPtr(Background.Dark),
877− },
878− NameConstant: ansi.StylePrimitive{
879− Color: stringPtr("#bd93f9"),
880− BackgroundColor: stringPtr(Background.Dark),
881− },
882− NameDecorator: ansi.StylePrimitive{
883− Color: stringPtr("#50fa7b"),
884− BackgroundColor: stringPtr(Background.Dark),
885− },
886− NameFunction: ansi.StylePrimitive{
887− Color: stringPtr("#50fa7b"),
888− BackgroundColor: stringPtr(Background.Dark),
889− },
890− LiteralNumber: ansi.StylePrimitive{
891− Color: stringPtr("#6EEFC0"),
892− BackgroundColor: stringPtr(Background.Dark),
893− },
894− LiteralString: ansi.StylePrimitive{
895− Color: stringPtr("#f1fa8c"),
896− BackgroundColor: stringPtr(Background.Dark),
897− },
898− LiteralStringEscape: ansi.StylePrimitive{
899− Color: stringPtr("#ff79c6"),
900− BackgroundColor: stringPtr(Background.Dark),
901− },
902− GenericDeleted: ansi.StylePrimitive{
903− Color: stringPtr("#ff5555"),
904− BackgroundColor: stringPtr(Background.Dark),
905− },
906− GenericEmph: ansi.StylePrimitive{
907− Color: stringPtr("#f1fa8c"),
908− Italic: boolPtr(true),
909− BackgroundColor: stringPtr(Background.Dark),
910− },
911− GenericInserted: ansi.StylePrimitive{
912− Color: stringPtr("#50fa7b"),
913− BackgroundColor: stringPtr(Background.Dark),
914− },
915− GenericStrong: ansi.StylePrimitive{
916− Color: stringPtr("#ffb86c"),
917− Bold: boolPtr(true),
918− BackgroundColor: stringPtr(Background.Dark),
919− },
920− GenericSubheading: ansi.StylePrimitive{
921− Color: stringPtr("#bd93f9"),
922− BackgroundColor: stringPtr(Background.Dark),
923− },
924− Background: ansi.StylePrimitive{
925− BackgroundColor: stringPtr(Background.Dark),
926− },
927− },
928− },
929− Table: ansi.StyleTable{
930− StyleBlock: ansi.StyleBlock{
155+ Color: stringPtr(adaptiveColorToString(t.MarkdownCode())),
156+ Prefix: "",
157+ Suffix: "",
158+ },
159+ },
160+ CodeBlock: ansi.StyleCodeBlock{
161+ StyleBlock: ansi.StyleBlock{
162+ StylePrimitive: ansi.StylePrimitive{
163+ Prefix: " ",
164+ Color: stringPtr(adaptiveColorToString(t.MarkdownCodeBlock())),
165+ },
166+ Margin: uintPtr(defaultMargin),
167+ },
168+ Chroma: &ansi.Chroma{
169+ Text: ansi.StylePrimitive{
170+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
171+ },
172+ Error: ansi.StylePrimitive{
173+ Color: stringPtr(adaptiveColorToString(t.Error())),
174+ },
175+ Comment: ansi.StylePrimitive{
176+ Color: stringPtr(adaptiveColorToString(t.SyntaxComment())),
177+ },
178+ CommentPreproc: ansi.StylePrimitive{
179+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
180+ },
181+ Keyword: ansi.StylePrimitive{
182+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
183+ },
184+ KeywordReserved: ansi.StylePrimitive{
185+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
186+ },
187+ KeywordNamespace: ansi.StylePrimitive{
188+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
189+ },
190+ KeywordType: ansi.StylePrimitive{
191+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
192+ },
193+ Operator: ansi.StylePrimitive{
194+ Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())),
195+ },
196+ Punctuation: ansi.StylePrimitive{
197+ Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())),
198+ },
199+ Name: ansi.StylePrimitive{
200+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
201+ },
202+ NameBuiltin: ansi.StylePrimitive{
203+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
204+ },
205+ NameTag: ansi.StylePrimitive{
206+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
207+ },
208+ NameAttribute: ansi.StylePrimitive{
209+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
210+ },
211+ NameClass: ansi.StylePrimitive{
212+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
213+ },
214+ NameConstant: ansi.StylePrimitive{
215+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
216+ },
217+ NameDecorator: ansi.StylePrimitive{
218+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
219+ },
220+ NameFunction: ansi.StylePrimitive{
221+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
222+ },
223+ LiteralNumber: ansi.StylePrimitive{
224+ Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())),
225+ },
226+ LiteralString: ansi.StylePrimitive{
227+ Color: stringPtr(adaptiveColorToString(t.SyntaxString())),
228+ },
229+ LiteralStringEscape: ansi.StylePrimitive{
230+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
231+ },
232+ GenericDeleted: ansi.StylePrimitive{
233+ Color: stringPtr(adaptiveColorToString(t.DiffRemoved())),
234+ },
235+ GenericEmph: ansi.StylePrimitive{
236+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
237+ Italic: boolPtr(true),
238+ },
239+ GenericInserted: ansi.StylePrimitive{
240+ Color: stringPtr(adaptiveColorToString(t.DiffAdded())),
241+ },
242+ GenericStrong: ansi.StylePrimitive{
243+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
244+ Bold: boolPtr(true),
245+ },
246+ GenericSubheading: ansi.StylePrimitive{
247+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
248+ },
249+ },
250+ },
251+ Table: ansi.StyleTable{
252+ StyleBlock: ansi.StyleBlock{
253+ StylePrimitive: ansi.StylePrimitive{
254+ BlockPrefix: "\n",
255+ BlockSuffix: "\n",
256+ },
257+ },
258+ CenterSeparator: stringPtr("┼"),
259+ ColumnSeparator: stringPtr("│"),
260+ RowSeparator: stringPtr("─"),
261+ },
262+ DefinitionDescription: ansi.StylePrimitive{
263+ BlockPrefix: "\n ❯ ",
264+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
265+ },
266+ Text: ansi.StylePrimitive{
267+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
268+ },
269+ Paragraph: ansi.StyleBlock{
931270 StylePrimitive: ansi.StylePrimitive{
932− BackgroundColor: stringPtr(Background.Dark),
271+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
933272 },
934− IndentToken: stringPtr(BaseStyle.Render(" ")),
935273 },
936− },
937− DefinitionDescription: ansi.StylePrimitive{
938− BlockPrefix: "\n* ",
939− BackgroundColor: stringPtr(Background.Dark),
940− },
274+ }
275+}
276+
277+// adaptiveColorToString converts a lipgloss.AdaptiveColor to the appropriate
278+// hex color string based on the current terminal background
279+func adaptiveColorToString(color lipgloss.AdaptiveColor) string {
280+ if lipgloss.HasDarkBackground() {
281+ return color.Dark
282+ }
283+ return color.Light
941284}
internal/tui/styles/styles.go+145−170
@@ -1,177 +1,152 @@
11package styles
22
33import (
4− catppuccin "github.com/catppuccin/go"
54 "github.com/charmbracelet/lipgloss"
5+ "github.com/opencode-ai/opencode/internal/tui/theme"
66)
77
8−var (
9− light = catppuccin.Latte
10− dark = catppuccin.Mocha
11−)
12−
13−// NEW STYLES
14−var (
15− Background = lipgloss.AdaptiveColor{
16− Dark: "#212121",
17− Light: "#212121",
18− }
19− BackgroundDim = lipgloss.AdaptiveColor{
20− Dark: "#2c2c2c",
21− Light: "#2c2c2c",
22− }
23− BackgroundDarker = lipgloss.AdaptiveColor{
24− Dark: "#181818",
25− Light: "#181818",
26− }
27− BorderColor = lipgloss.AdaptiveColor{
28− Dark: "#4b4c5c",
29− Light: "#4b4c5c",
30− }
31−
32− Forground = lipgloss.AdaptiveColor{
33− Dark: "#d3d3d3",
34− Light: "#d3d3d3",
35− }
36−
37− ForgroundMid = lipgloss.AdaptiveColor{
38− Dark: "#a0a0a0",
39− Light: "#a0a0a0",
40− }
41−
42− ForgroundDim = lipgloss.AdaptiveColor{
43− Dark: "#737373",
44− Light: "#737373",
45− }
46−
47− BaseStyle = lipgloss.NewStyle().
48− Background(Background).
49− Foreground(Forground)
50−
51− PrimaryColor = lipgloss.AdaptiveColor{
52− Dark: "#fab283",
53− Light: "#fab283",
54− }
55−)
8+// Style generation functions that use the current theme
9+
10+// BaseStyle returns the base style with background and foreground colors
11+func BaseStyle() lipgloss.Style {
12+ t := theme.CurrentTheme()
13+ return lipgloss.NewStyle().
14+ Background(t.Background()).
15+ Foreground(t.Text())
16+}
17+
18+// Regular returns a basic unstyled lipgloss.Style
19+func Regular() lipgloss.Style {
20+ return lipgloss.NewStyle()
21+}
22+
23+// Bold returns a bold style
24+func Bold() lipgloss.Style {
25+ return Regular().Bold(true)
26+}
27+
28+// Padded returns a style with horizontal padding
29+func Padded() lipgloss.Style {
30+ return Regular().Padding(0, 1)
31+}
32+
33+// Border returns a style with a normal border
34+func Border() lipgloss.Style {
35+ t := theme.CurrentTheme()
36+ return Regular().
37+ Border(lipgloss.NormalBorder()).
38+ BorderForeground(t.BorderNormal())
39+}
40+
41+// ThickBorder returns a style with a thick border
42+func ThickBorder() lipgloss.Style {
43+ t := theme.CurrentTheme()
44+ return Regular().
45+ Border(lipgloss.ThickBorder()).
46+ BorderForeground(t.BorderNormal())
47+}
48+
49+// DoubleBorder returns a style with a double border
50+func DoubleBorder() lipgloss.Style {
51+ t := theme.CurrentTheme()
52+ return Regular().
53+ Border(lipgloss.DoubleBorder()).
54+ BorderForeground(t.BorderNormal())
55+}
56+
57+// FocusedBorder returns a style with a border using the focused border color
58+func FocusedBorder() lipgloss.Style {
59+ t := theme.CurrentTheme()
60+ return Regular().
61+ Border(lipgloss.NormalBorder()).
62+ BorderForeground(t.BorderFocused())
63+}
64+
65+// DimBorder returns a style with a border using the dim border color
66+func DimBorder() lipgloss.Style {
67+ t := theme.CurrentTheme()
68+ return Regular().
69+ Border(lipgloss.NormalBorder()).
70+ BorderForeground(t.BorderDim())
71+}
72+
73+// PrimaryColor returns the primary color from the current theme
74+func PrimaryColor() lipgloss.AdaptiveColor {
75+ return theme.CurrentTheme().Primary()
76+}
77+
78+// SecondaryColor returns the secondary color from the current theme
79+func SecondaryColor() lipgloss.AdaptiveColor {
80+ return theme.CurrentTheme().Secondary()
81+}
82+
83+// AccentColor returns the accent color from the current theme
84+func AccentColor() lipgloss.AdaptiveColor {
85+ return theme.CurrentTheme().Accent()
86+}
87+
88+// ErrorColor returns the error color from the current theme
89+func ErrorColor() lipgloss.AdaptiveColor {
90+ return theme.CurrentTheme().Error()
91+}
92+
93+// WarningColor returns the warning color from the current theme
94+func WarningColor() lipgloss.AdaptiveColor {
95+ return theme.CurrentTheme().Warning()
96+}
97+
98+// SuccessColor returns the success color from the current theme
99+func SuccessColor() lipgloss.AdaptiveColor {
100+ return theme.CurrentTheme().Success()
101+}
102+
103+// InfoColor returns the info color from the current theme
104+func InfoColor() lipgloss.AdaptiveColor {
105+ return theme.CurrentTheme().Info()
106+}
107+
108+// TextColor returns the text color from the current theme
109+func TextColor() lipgloss.AdaptiveColor {
110+ return theme.CurrentTheme().Text()
111+}
112+
113+// TextMutedColor returns the muted text color from the current theme
114+func TextMutedColor() lipgloss.AdaptiveColor {
115+ return theme.CurrentTheme().TextMuted()
116+}
117+
118+// TextEmphasizedColor returns the emphasized text color from the current theme
119+func TextEmphasizedColor() lipgloss.AdaptiveColor {
120+ return theme.CurrentTheme().TextEmphasized()
121+}
122+
123+// BackgroundColor returns the background color from the current theme
124+func BackgroundColor() lipgloss.AdaptiveColor {
125+ return theme.CurrentTheme().Background()
126+}
127+
128+// BackgroundSecondaryColor returns the secondary background color from the current theme
129+func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
130+ return theme.CurrentTheme().BackgroundSecondary()
131+}
132+
133+// BackgroundDarkerColor returns the darker background color from the current theme
134+func BackgroundDarkerColor() lipgloss.AdaptiveColor {
135+ return theme.CurrentTheme().BackgroundDarker()
136+}
137+
138+// BorderNormalColor returns the normal border color from the current theme
139+func BorderNormalColor() lipgloss.AdaptiveColor {
140+ return theme.CurrentTheme().BorderNormal()
141+}
142+
143+// BorderFocusedColor returns the focused border color from the current theme
144+func BorderFocusedColor() lipgloss.AdaptiveColor {
145+ return theme.CurrentTheme().BorderFocused()
146+}
147+
148+// BorderDimColor returns the dim border color from the current theme
149+func BorderDimColor() lipgloss.AdaptiveColor {
150+ return theme.CurrentTheme().BorderDim()
151+}
56152
57−var (
58− Regular = lipgloss.NewStyle()
59− Bold = Regular.Bold(true)
60− Padded = Regular.Padding(0, 1)
61−
62− Border = Regular.Border(lipgloss.NormalBorder())
63− ThickBorder = Regular.Border(lipgloss.ThickBorder())
64− DoubleBorder = Regular.Border(lipgloss.DoubleBorder())
65−
66− // Colors
67− White = lipgloss.Color("#ffffff")
68− Surface0 = lipgloss.AdaptiveColor{
69− Dark: dark.Surface0().Hex,
70− Light: light.Surface0().Hex,
71− }
72−
73− Overlay0 = lipgloss.AdaptiveColor{
74− Dark: dark.Overlay0().Hex,
75− Light: light.Overlay0().Hex,
76− }
77−
78− Ovelay1 = lipgloss.AdaptiveColor{
79− Dark: dark.Overlay1().Hex,
80− Light: light.Overlay1().Hex,
81− }
82−
83− Text = lipgloss.AdaptiveColor{
84− Dark: dark.Text().Hex,
85− Light: light.Text().Hex,
86− }
87−
88− SubText0 = lipgloss.AdaptiveColor{
89− Dark: dark.Subtext0().Hex,
90− Light: light.Subtext0().Hex,
91− }
92−
93− SubText1 = lipgloss.AdaptiveColor{
94− Dark: dark.Subtext1().Hex,
95− Light: light.Subtext1().Hex,
96− }
97−
98− LightGrey = lipgloss.AdaptiveColor{
99− Dark: dark.Surface0().Hex,
100− Light: light.Surface0().Hex,
101− }
102− Grey = lipgloss.AdaptiveColor{
103− Dark: dark.Surface1().Hex,
104− Light: light.Surface1().Hex,
105− }
106−
107− DarkGrey = lipgloss.AdaptiveColor{
108− Dark: dark.Surface2().Hex,
109− Light: light.Surface2().Hex,
110− }
111−
112− Base = lipgloss.AdaptiveColor{
113− Dark: dark.Base().Hex,
114− Light: light.Base().Hex,
115− }
116−
117− Crust = lipgloss.AdaptiveColor{
118− Dark: dark.Crust().Hex,
119− Light: light.Crust().Hex,
120− }
121−
122− Blue = lipgloss.AdaptiveColor{
123− Dark: dark.Blue().Hex,
124− Light: light.Blue().Hex,
125− }
126−
127− Red = lipgloss.AdaptiveColor{
128− Dark: dark.Red().Hex,
129− Light: light.Red().Hex,
130− }
131−
132− Green = lipgloss.AdaptiveColor{
133− Dark: dark.Green().Hex,
134− Light: light.Green().Hex,
135− }
136−
137− Mauve = lipgloss.AdaptiveColor{
138− Dark: dark.Mauve().Hex,
139− Light: light.Mauve().Hex,
140− }
141−
142− Teal = lipgloss.AdaptiveColor{
143− Dark: dark.Teal().Hex,
144− Light: light.Teal().Hex,
145− }
146−
147− Rosewater = lipgloss.AdaptiveColor{
148− Dark: dark.Rosewater().Hex,
149− Light: light.Rosewater().Hex,
150− }
151−
152− Flamingo = lipgloss.AdaptiveColor{
153− Dark: dark.Flamingo().Hex,
154− Light: light.Flamingo().Hex,
155− }
156−
157− Lavender = lipgloss.AdaptiveColor{
158− Dark: dark.Lavender().Hex,
159− Light: light.Lavender().Hex,
160− }
161−
162− Peach = lipgloss.AdaptiveColor{
163− Dark: dark.Peach().Hex,
164− Light: light.Peach().Hex,
165− }
166−
167− Yellow = lipgloss.AdaptiveColor{
168− Dark: dark.Yellow().Hex,
169− Light: light.Yellow().Hex,
170− }
171−
172− Primary = Blue
173− Secondary = Mauve
174−
175− Warning = Peach
176− Error = Red
177−)
internal/tui/theme/catppuccin.gonew file+248
@@ -0,0 +1,248 @@
1+package theme
2+
3+import (
4+ catppuccin "github.com/catppuccin/go"
5+ "github.com/charmbracelet/lipgloss"
6+)
7+
8+// CatppuccinTheme implements the Theme interface with Catppuccin colors.
9+// It provides both dark (Mocha) and light (Latte) variants.
10+type CatppuccinTheme struct {
11+ BaseTheme
12+}
13+
14+// NewCatppuccinTheme creates a new instance of the Catppuccin theme.
15+func NewCatppuccinTheme() *CatppuccinTheme {
16+ // Get the Catppuccin palettes
17+ mocha := catppuccin.Mocha
18+ latte := catppuccin.Latte
19+
20+ theme := &CatppuccinTheme{}
21+
22+ // Base colors
23+ theme.PrimaryColor = lipgloss.AdaptiveColor{
24+ Dark: mocha.Blue().Hex,
25+ Light: latte.Blue().Hex,
26+ }
27+ theme.SecondaryColor = lipgloss.AdaptiveColor{
28+ Dark: mocha.Mauve().Hex,
29+ Light: latte.Mauve().Hex,
30+ }
31+ theme.AccentColor = lipgloss.AdaptiveColor{
32+ Dark: mocha.Peach().Hex,
33+ Light: latte.Peach().Hex,
34+ }
35+
36+ // Status colors
37+ theme.ErrorColor = lipgloss.AdaptiveColor{
38+ Dark: mocha.Red().Hex,
39+ Light: latte.Red().Hex,
40+ }
41+ theme.WarningColor = lipgloss.AdaptiveColor{
42+ Dark: mocha.Peach().Hex,
43+ Light: latte.Peach().Hex,
44+ }
45+ theme.SuccessColor = lipgloss.AdaptiveColor{
46+ Dark: mocha.Green().Hex,
47+ Light: latte.Green().Hex,
48+ }
49+ theme.InfoColor = lipgloss.AdaptiveColor{
50+ Dark: mocha.Blue().Hex,
51+ Light: latte.Blue().Hex,
52+ }
53+
54+ // Text colors
55+ theme.TextColor = lipgloss.AdaptiveColor{
56+ Dark: mocha.Text().Hex,
57+ Light: latte.Text().Hex,
58+ }
59+ theme.TextMutedColor = lipgloss.AdaptiveColor{
60+ Dark: mocha.Subtext0().Hex,
61+ Light: latte.Subtext0().Hex,
62+ }
63+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
64+ Dark: mocha.Lavender().Hex,
65+ Light: latte.Lavender().Hex,
66+ }
67+
68+ // Background colors
69+ theme.BackgroundColor = lipgloss.AdaptiveColor{
70+ Dark: "#212121", // From existing styles
71+ Light: "#EEEEEE", // Light equivalent
72+ }
73+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
74+ Dark: "#2c2c2c", // From existing styles
75+ Light: "#E0E0E0", // Light equivalent
76+ }
77+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
78+ Dark: "#181818", // From existing styles
79+ Light: "#F5F5F5", // Light equivalent
80+ }
81+
82+ // Border colors
83+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
84+ Dark: "#4b4c5c", // From existing styles
85+ Light: "#BDBDBD", // Light equivalent
86+ }
87+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
88+ Dark: mocha.Blue().Hex,
89+ Light: latte.Blue().Hex,
90+ }
91+ theme.BorderDimColor = lipgloss.AdaptiveColor{
92+ Dark: mocha.Surface0().Hex,
93+ Light: latte.Surface0().Hex,
94+ }
95+
96+ // Diff view colors
97+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
98+ Dark: "#478247", // From existing diff.go
99+ Light: "#2E7D32", // Light equivalent
100+ }
101+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
102+ Dark: "#7C4444", // From existing diff.go
103+ Light: "#C62828", // Light equivalent
104+ }
105+ theme.DiffContextColor = lipgloss.AdaptiveColor{
106+ Dark: "#a0a0a0", // From existing diff.go
107+ Light: "#757575", // Light equivalent
108+ }
109+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
110+ Dark: "#a0a0a0", // From existing diff.go
111+ Light: "#757575", // Light equivalent
112+ }
113+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
114+ Dark: "#DAFADA", // From existing diff.go
115+ Light: "#A5D6A7", // Light equivalent
116+ }
117+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
118+ Dark: "#FADADD", // From existing diff.go
119+ Light: "#EF9A9A", // Light equivalent
120+ }
121+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
122+ Dark: "#303A30", // From existing diff.go
123+ Light: "#E8F5E9", // Light equivalent
124+ }
125+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
126+ Dark: "#3A3030", // From existing diff.go
127+ Light: "#FFEBEE", // Light equivalent
128+ }
129+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
130+ Dark: "#212121", // From existing diff.go
131+ Light: "#F5F5F5", // Light equivalent
132+ }
133+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
134+ Dark: "#888888", // From existing diff.go
135+ Light: "#9E9E9E", // Light equivalent
136+ }
137+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
138+ Dark: "#293229", // From existing diff.go
139+ Light: "#C8E6C9", // Light equivalent
140+ }
141+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
142+ Dark: "#332929", // From existing diff.go
143+ Light: "#FFCDD2", // Light equivalent
144+ }
145+
146+ // Markdown colors
147+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
148+ Dark: mocha.Text().Hex,
149+ Light: latte.Text().Hex,
150+ }
151+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
152+ Dark: mocha.Mauve().Hex,
153+ Light: latte.Mauve().Hex,
154+ }
155+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
156+ Dark: mocha.Sky().Hex,
157+ Light: latte.Sky().Hex,
158+ }
159+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
160+ Dark: mocha.Pink().Hex,
161+ Light: latte.Pink().Hex,
162+ }
163+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
164+ Dark: mocha.Green().Hex,
165+ Light: latte.Green().Hex,
166+ }
167+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
168+ Dark: mocha.Yellow().Hex,
169+ Light: latte.Yellow().Hex,
170+ }
171+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
172+ Dark: mocha.Yellow().Hex,
173+ Light: latte.Yellow().Hex,
174+ }
175+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
176+ Dark: mocha.Peach().Hex,
177+ Light: latte.Peach().Hex,
178+ }
179+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
180+ Dark: mocha.Overlay0().Hex,
181+ Light: latte.Overlay0().Hex,
182+ }
183+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
184+ Dark: mocha.Blue().Hex,
185+ Light: latte.Blue().Hex,
186+ }
187+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
188+ Dark: mocha.Sky().Hex,
189+ Light: latte.Sky().Hex,
190+ }
191+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
192+ Dark: mocha.Sapphire().Hex,
193+ Light: latte.Sapphire().Hex,
194+ }
195+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
196+ Dark: mocha.Pink().Hex,
197+ Light: latte.Pink().Hex,
198+ }
199+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
200+ Dark: mocha.Text().Hex,
201+ Light: latte.Text().Hex,
202+ }
203+
204+ // Syntax highlighting colors
205+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
206+ Dark: mocha.Overlay1().Hex,
207+ Light: latte.Overlay1().Hex,
208+ }
209+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
210+ Dark: mocha.Pink().Hex,
211+ Light: latte.Pink().Hex,
212+ }
213+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
214+ Dark: mocha.Green().Hex,
215+ Light: latte.Green().Hex,
216+ }
217+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
218+ Dark: mocha.Sky().Hex,
219+ Light: latte.Sky().Hex,
220+ }
221+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
222+ Dark: mocha.Yellow().Hex,
223+ Light: latte.Yellow().Hex,
224+ }
225+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
226+ Dark: mocha.Teal().Hex,
227+ Light: latte.Teal().Hex,
228+ }
229+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
230+ Dark: mocha.Sky().Hex,
231+ Light: latte.Sky().Hex,
232+ }
233+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
234+ Dark: mocha.Pink().Hex,
235+ Light: latte.Pink().Hex,
236+ }
237+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
238+ Dark: mocha.Text().Hex,
239+ Light: latte.Text().Hex,
240+ }
241+
242+ return theme
243+}
244+
245+func init() {
246+ // Register the Catppuccin theme with the theme manager
247+ RegisterTheme("catppuccin", NewCatppuccinTheme())
248+}
internal/tui/theme/dracula.gonew file+274
@@ -0,0 +1,274 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// DraculaTheme implements the Theme interface with Dracula colors.
8+// It provides both dark and light variants, though Dracula is primarily a dark theme.
9+type DraculaTheme struct {
10+ BaseTheme
11+}
12+
13+// NewDraculaTheme creates a new instance of the Dracula theme.
14+func NewDraculaTheme() *DraculaTheme {
15+ // Dracula color palette
16+ // Official colors from https://draculatheme.com/
17+ darkBackground := "#282a36"
18+ darkCurrentLine := "#44475a"
19+ darkSelection := "#44475a"
20+ darkForeground := "#f8f8f2"
21+ darkComment := "#6272a4"
22+ darkCyan := "#8be9fd"
23+ darkGreen := "#50fa7b"
24+ darkOrange := "#ffb86c"
25+ darkPink := "#ff79c6"
26+ darkPurple := "#bd93f9"
27+ darkRed := "#ff5555"
28+ darkYellow := "#f1fa8c"
29+ darkBorder := "#44475a"
30+
31+ // Light mode approximation (Dracula is primarily a dark theme)
32+ lightBackground := "#f8f8f2"
33+ lightCurrentLine := "#e6e6e6"
34+ lightSelection := "#d8d8d8"
35+ lightForeground := "#282a36"
36+ lightComment := "#6272a4"
37+ lightCyan := "#0097a7"
38+ lightGreen := "#388e3c"
39+ lightOrange := "#f57c00"
40+ lightPink := "#d81b60"
41+ lightPurple := "#7e57c2"
42+ lightRed := "#e53935"
43+ lightYellow := "#fbc02d"
44+ lightBorder := "#d8d8d8"
45+
46+ theme := &DraculaTheme{}
47+
48+ // Base colors
49+ theme.PrimaryColor = lipgloss.AdaptiveColor{
50+ Dark: darkPurple,
51+ Light: lightPurple,
52+ }
53+ theme.SecondaryColor = lipgloss.AdaptiveColor{
54+ Dark: darkPink,
55+ Light: lightPink,
56+ }
57+ theme.AccentColor = lipgloss.AdaptiveColor{
58+ Dark: darkCyan,
59+ Light: lightCyan,
60+ }
61+
62+ // Status colors
63+ theme.ErrorColor = lipgloss.AdaptiveColor{
64+ Dark: darkRed,
65+ Light: lightRed,
66+ }
67+ theme.WarningColor = lipgloss.AdaptiveColor{
68+ Dark: darkOrange,
69+ Light: lightOrange,
70+ }
71+ theme.SuccessColor = lipgloss.AdaptiveColor{
72+ Dark: darkGreen,
73+ Light: lightGreen,
74+ }
75+ theme.InfoColor = lipgloss.AdaptiveColor{
76+ Dark: darkCyan,
77+ Light: lightCyan,
78+ }
79+
80+ // Text colors
81+ theme.TextColor = lipgloss.AdaptiveColor{
82+ Dark: darkForeground,
83+ Light: lightForeground,
84+ }
85+ theme.TextMutedColor = lipgloss.AdaptiveColor{
86+ Dark: darkComment,
87+ Light: lightComment,
88+ }
89+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
90+ Dark: darkYellow,
91+ Light: lightYellow,
92+ }
93+
94+ // Background colors
95+ theme.BackgroundColor = lipgloss.AdaptiveColor{
96+ Dark: darkBackground,
97+ Light: lightBackground,
98+ }
99+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
100+ Dark: darkCurrentLine,
101+ Light: lightCurrentLine,
102+ }
103+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
104+ Dark: "#21222c", // Slightly darker than background
105+ Light: "#ffffff", // Slightly lighter than background
106+ }
107+
108+ // Border colors
109+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
110+ Dark: darkBorder,
111+ Light: lightBorder,
112+ }
113+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
114+ Dark: darkPurple,
115+ Light: lightPurple,
116+ }
117+ theme.BorderDimColor = lipgloss.AdaptiveColor{
118+ Dark: darkSelection,
119+ Light: lightSelection,
120+ }
121+
122+ // Diff view colors
123+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
124+ Dark: darkGreen,
125+ Light: lightGreen,
126+ }
127+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
128+ Dark: darkRed,
129+ Light: lightRed,
130+ }
131+ theme.DiffContextColor = lipgloss.AdaptiveColor{
132+ Dark: darkComment,
133+ Light: lightComment,
134+ }
135+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
136+ Dark: darkPurple,
137+ Light: lightPurple,
138+ }
139+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
140+ Dark: "#50fa7b",
141+ Light: "#a5d6a7",
142+ }
143+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
144+ Dark: "#ff5555",
145+ Light: "#ef9a9a",
146+ }
147+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
148+ Dark: "#2c3b2c",
149+ Light: "#e8f5e9",
150+ }
151+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
152+ Dark: "#3b2c2c",
153+ Light: "#ffebee",
154+ }
155+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
156+ Dark: darkBackground,
157+ Light: lightBackground,
158+ }
159+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
160+ Dark: darkComment,
161+ Light: lightComment,
162+ }
163+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
164+ Dark: "#253025",
165+ Light: "#c8e6c9",
166+ }
167+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
168+ Dark: "#302525",
169+ Light: "#ffcdd2",
170+ }
171+
172+ // Markdown colors
173+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
174+ Dark: darkForeground,
175+ Light: lightForeground,
176+ }
177+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
178+ Dark: darkPink,
179+ Light: lightPink,
180+ }
181+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
182+ Dark: darkPurple,
183+ Light: lightPurple,
184+ }
185+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
186+ Dark: darkCyan,
187+ Light: lightCyan,
188+ }
189+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
190+ Dark: darkGreen,
191+ Light: lightGreen,
192+ }
193+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
194+ Dark: darkYellow,
195+ Light: lightYellow,
196+ }
197+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
198+ Dark: darkYellow,
199+ Light: lightYellow,
200+ }
201+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
202+ Dark: darkOrange,
203+ Light: lightOrange,
204+ }
205+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
206+ Dark: darkComment,
207+ Light: lightComment,
208+ }
209+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
210+ Dark: darkPurple,
211+ Light: lightPurple,
212+ }
213+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
214+ Dark: darkCyan,
215+ Light: lightCyan,
216+ }
217+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
218+ Dark: darkPurple,
219+ Light: lightPurple,
220+ }
221+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
222+ Dark: darkCyan,
223+ Light: lightCyan,
224+ }
225+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
226+ Dark: darkForeground,
227+ Light: lightForeground,
228+ }
229+
230+ // Syntax highlighting colors
231+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
232+ Dark: darkComment,
233+ Light: lightComment,
234+ }
235+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
236+ Dark: darkPink,
237+ Light: lightPink,
238+ }
239+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
240+ Dark: darkGreen,
241+ Light: lightGreen,
242+ }
243+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
244+ Dark: darkOrange,
245+ Light: lightOrange,
246+ }
247+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
248+ Dark: darkYellow,
249+ Light: lightYellow,
250+ }
251+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
252+ Dark: darkPurple,
253+ Light: lightPurple,
254+ }
255+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
256+ Dark: darkCyan,
257+ Light: lightCyan,
258+ }
259+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
260+ Dark: darkPink,
261+ Light: lightPink,
262+ }
263+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
264+ Dark: darkForeground,
265+ Light: lightForeground,
266+ }
267+
268+ return theme
269+}
270+
271+func init() {
272+ // Register the Dracula theme with the theme manager
273+ RegisterTheme("dracula", NewDraculaTheme())
274+}
internal/tui/theme/flexoki.gonew file+282
@@ -0,0 +1,282 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// Flexoki color palette constants
8+const (
9+ // Base colors
10+ flexokiPaper = "#FFFCF0" // Paper (lightest)
11+ flexokiBase50 = "#F2F0E5" // bg-2 (light)
12+ flexokiBase100 = "#E6E4D9" // ui (light)
13+ flexokiBase150 = "#DAD8CE" // ui-2 (light)
14+ flexokiBase200 = "#CECDC3" // ui-3 (light)
15+ flexokiBase300 = "#B7B5AC" // tx-3 (light)
16+ flexokiBase500 = "#878580" // tx-2 (light)
17+ flexokiBase600 = "#6F6E69" // tx (light)
18+ flexokiBase700 = "#575653" // tx-3 (dark)
19+ flexokiBase800 = "#403E3C" // ui-3 (dark)
20+ flexokiBase850 = "#343331" // ui-2 (dark)
21+ flexokiBase900 = "#282726" // ui (dark)
22+ flexokiBase950 = "#1C1B1A" // bg-2 (dark)
23+ flexokiBlack = "#100F0F" // bg (darkest)
24+
25+ // Accent colors - Light theme (600)
26+ flexokiRed600 = "#AF3029"
27+ flexokiOrange600 = "#BC5215"
28+ flexokiYellow600 = "#AD8301"
29+ flexokiGreen600 = "#66800B"
30+ flexokiCyan600 = "#24837B"
31+ flexokiBlue600 = "#205EA6"
32+ flexokiPurple600 = "#5E409D"
33+ flexokiMagenta600 = "#A02F6F"
34+
35+ // Accent colors - Dark theme (400)
36+ flexokiRed400 = "#D14D41"
37+ flexokiOrange400 = "#DA702C"
38+ flexokiYellow400 = "#D0A215"
39+ flexokiGreen400 = "#879A39"
40+ flexokiCyan400 = "#3AA99F"
41+ flexokiBlue400 = "#4385BE"
42+ flexokiPurple400 = "#8B7EC8"
43+ flexokiMagenta400 = "#CE5D97"
44+)
45+
46+// FlexokiTheme implements the Theme interface with Flexoki colors.
47+// It provides both dark and light variants.
48+type FlexokiTheme struct {
49+ BaseTheme
50+}
51+
52+// NewFlexokiTheme creates a new instance of the Flexoki theme.
53+func NewFlexokiTheme() *FlexokiTheme {
54+ theme := &FlexokiTheme{}
55+
56+ // Base colors
57+ theme.PrimaryColor = lipgloss.AdaptiveColor{
58+ Dark: flexokiBlue400,
59+ Light: flexokiBlue600,
60+ }
61+ theme.SecondaryColor = lipgloss.AdaptiveColor{
62+ Dark: flexokiPurple400,
63+ Light: flexokiPurple600,
64+ }
65+ theme.AccentColor = lipgloss.AdaptiveColor{
66+ Dark: flexokiOrange400,
67+ Light: flexokiOrange600,
68+ }
69+
70+ // Status colors
71+ theme.ErrorColor = lipgloss.AdaptiveColor{
72+ Dark: flexokiRed400,
73+ Light: flexokiRed600,
74+ }
75+ theme.WarningColor = lipgloss.AdaptiveColor{
76+ Dark: flexokiYellow400,
77+ Light: flexokiYellow600,
78+ }
79+ theme.SuccessColor = lipgloss.AdaptiveColor{
80+ Dark: flexokiGreen400,
81+ Light: flexokiGreen600,
82+ }
83+ theme.InfoColor = lipgloss.AdaptiveColor{
84+ Dark: flexokiCyan400,
85+ Light: flexokiCyan600,
86+ }
87+
88+ // Text colors
89+ theme.TextColor = lipgloss.AdaptiveColor{
90+ Dark: flexokiBase300,
91+ Light: flexokiBase600,
92+ }
93+ theme.TextMutedColor = lipgloss.AdaptiveColor{
94+ Dark: flexokiBase700,
95+ Light: flexokiBase500,
96+ }
97+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
98+ Dark: flexokiYellow400,
99+ Light: flexokiYellow600,
100+ }
101+
102+ // Background colors
103+ theme.BackgroundColor = lipgloss.AdaptiveColor{
104+ Dark: flexokiBlack,
105+ Light: flexokiPaper,
106+ }
107+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
108+ Dark: flexokiBase950,
109+ Light: flexokiBase50,
110+ }
111+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
112+ Dark: flexokiBase900,
113+ Light: flexokiBase100,
114+ }
115+
116+ // Border colors
117+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
118+ Dark: flexokiBase900,
119+ Light: flexokiBase100,
120+ }
121+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
122+ Dark: flexokiBlue400,
123+ Light: flexokiBlue600,
124+ }
125+ theme.BorderDimColor = lipgloss.AdaptiveColor{
126+ Dark: flexokiBase850,
127+ Light: flexokiBase150,
128+ }
129+
130+ // Diff view colors
131+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
132+ Dark: flexokiGreen400,
133+ Light: flexokiGreen600,
134+ }
135+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
136+ Dark: flexokiRed400,
137+ Light: flexokiRed600,
138+ }
139+ theme.DiffContextColor = lipgloss.AdaptiveColor{
140+ Dark: flexokiBase700,
141+ Light: flexokiBase500,
142+ }
143+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
144+ Dark: flexokiBase700,
145+ Light: flexokiBase500,
146+ }
147+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
148+ Dark: flexokiGreen400,
149+ Light: flexokiGreen600,
150+ }
151+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
152+ Dark: flexokiRed400,
153+ Light: flexokiRed600,
154+ }
155+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
156+ Dark: "#1D2419", // Darker green background
157+ Light: "#EFF2E2", // Light green background
158+ }
159+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
160+ Dark: "#241919", // Darker red background
161+ Light: "#F2E2E2", // Light red background
162+ }
163+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
164+ Dark: flexokiBlack,
165+ Light: flexokiPaper,
166+ }
167+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
168+ Dark: flexokiBase700,
169+ Light: flexokiBase500,
170+ }
171+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
172+ Dark: "#1A2017", // Slightly darker green
173+ Light: "#E5EBD9", // Light green
174+ }
175+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
176+ Dark: "#201717", // Slightly darker red
177+ Light: "#EBD9D9", // Light red
178+ }
179+
180+ // Markdown colors
181+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
182+ Dark: flexokiBase300,
183+ Light: flexokiBase600,
184+ }
185+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
186+ Dark: flexokiYellow400,
187+ Light: flexokiYellow600,
188+ }
189+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
190+ Dark: flexokiCyan400,
191+ Light: flexokiCyan600,
192+ }
193+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
194+ Dark: flexokiMagenta400,
195+ Light: flexokiMagenta600,
196+ }
197+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
198+ Dark: flexokiGreen400,
199+ Light: flexokiGreen600,
200+ }
201+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
202+ Dark: flexokiCyan400,
203+ Light: flexokiCyan600,
204+ }
205+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
206+ Dark: flexokiYellow400,
207+ Light: flexokiYellow600,
208+ }
209+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
210+ Dark: flexokiOrange400,
211+ Light: flexokiOrange600,
212+ }
213+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
214+ Dark: flexokiBase800,
215+ Light: flexokiBase200,
216+ }
217+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
218+ Dark: flexokiBlue400,
219+ Light: flexokiBlue600,
220+ }
221+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
222+ Dark: flexokiBlue400,
223+ Light: flexokiBlue600,
224+ }
225+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
226+ Dark: flexokiPurple400,
227+ Light: flexokiPurple600,
228+ }
229+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
230+ Dark: flexokiMagenta400,
231+ Light: flexokiMagenta600,
232+ }
233+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
234+ Dark: flexokiBase300,
235+ Light: flexokiBase600,
236+ }
237+
238+ // Syntax highlighting colors (based on Flexoki's mappings)
239+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
240+ Dark: flexokiBase700, // tx-3
241+ Light: flexokiBase300, // tx-3
242+ }
243+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
244+ Dark: flexokiGreen400, // gr
245+ Light: flexokiGreen600, // gr
246+ }
247+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
248+ Dark: flexokiOrange400, // or
249+ Light: flexokiOrange600, // or
250+ }
251+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
252+ Dark: flexokiBlue400, // bl
253+ Light: flexokiBlue600, // bl
254+ }
255+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
256+ Dark: flexokiCyan400, // cy
257+ Light: flexokiCyan600, // cy
258+ }
259+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
260+ Dark: flexokiPurple400, // pu
261+ Light: flexokiPurple600, // pu
262+ }
263+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
264+ Dark: flexokiYellow400, // ye
265+ Light: flexokiYellow600, // ye
266+ }
267+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
268+ Dark: flexokiBase500, // tx-2
269+ Light: flexokiBase500, // tx-2
270+ }
271+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
272+ Dark: flexokiBase500, // tx-2
273+ Light: flexokiBase500, // tx-2
274+ }
275+
276+ return theme
277+}
278+
279+func init() {
280+ // Register the Flexoki theme with the theme manager
281+ RegisterTheme("flexoki", NewFlexokiTheme())
282+}
internal/tui/theme/gruvbox.gonew file+302
@@ -0,0 +1,302 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// Gruvbox color palette constants
8+const (
9+ // Dark theme colors
10+ gruvboxDarkBg0 = "#282828"
11+ gruvboxDarkBg0Soft = "#32302f"
12+ gruvboxDarkBg1 = "#3c3836"
13+ gruvboxDarkBg2 = "#504945"
14+ gruvboxDarkBg3 = "#665c54"
15+ gruvboxDarkBg4 = "#7c6f64"
16+ gruvboxDarkFg0 = "#fbf1c7"
17+ gruvboxDarkFg1 = "#ebdbb2"
18+ gruvboxDarkFg2 = "#d5c4a1"
19+ gruvboxDarkFg3 = "#bdae93"
20+ gruvboxDarkFg4 = "#a89984"
21+ gruvboxDarkGray = "#928374"
22+ gruvboxDarkRed = "#cc241d"
23+ gruvboxDarkRedBright = "#fb4934"
24+ gruvboxDarkGreen = "#98971a"
25+ gruvboxDarkGreenBright = "#b8bb26"
26+ gruvboxDarkYellow = "#d79921"
27+ gruvboxDarkYellowBright = "#fabd2f"
28+ gruvboxDarkBlue = "#458588"
29+ gruvboxDarkBlueBright = "#83a598"
30+ gruvboxDarkPurple = "#b16286"
31+ gruvboxDarkPurpleBright = "#d3869b"
32+ gruvboxDarkAqua = "#689d6a"
33+ gruvboxDarkAquaBright = "#8ec07c"
34+ gruvboxDarkOrange = "#d65d0e"
35+ gruvboxDarkOrangeBright = "#fe8019"
36+
37+ // Light theme colors
38+ gruvboxLightBg0 = "#fbf1c7"
39+ gruvboxLightBg0Soft = "#f2e5bc"
40+ gruvboxLightBg1 = "#ebdbb2"
41+ gruvboxLightBg2 = "#d5c4a1"
42+ gruvboxLightBg3 = "#bdae93"
43+ gruvboxLightBg4 = "#a89984"
44+ gruvboxLightFg0 = "#282828"
45+ gruvboxLightFg1 = "#3c3836"
46+ gruvboxLightFg2 = "#504945"
47+ gruvboxLightFg3 = "#665c54"
48+ gruvboxLightFg4 = "#7c6f64"
49+ gruvboxLightGray = "#928374"
50+ gruvboxLightRed = "#9d0006"
51+ gruvboxLightRedBright = "#cc241d"
52+ gruvboxLightGreen = "#79740e"
53+ gruvboxLightGreenBright = "#98971a"
54+ gruvboxLightYellow = "#b57614"
55+ gruvboxLightYellowBright = "#d79921"
56+ gruvboxLightBlue = "#076678"
57+ gruvboxLightBlueBright = "#458588"
58+ gruvboxLightPurple = "#8f3f71"
59+ gruvboxLightPurpleBright = "#b16286"
60+ gruvboxLightAqua = "#427b58"
61+ gruvboxLightAquaBright = "#689d6a"
62+ gruvboxLightOrange = "#af3a03"
63+ gruvboxLightOrangeBright = "#d65d0e"
64+)
65+
66+// GruvboxTheme implements the Theme interface with Gruvbox colors.
67+// It provides both dark and light variants.
68+type GruvboxTheme struct {
69+ BaseTheme
70+}
71+
72+// NewGruvboxTheme creates a new instance of the Gruvbox theme.
73+func NewGruvboxTheme() *GruvboxTheme {
74+ theme := &GruvboxTheme{}
75+
76+ // Base colors
77+ theme.PrimaryColor = lipgloss.AdaptiveColor{
78+ Dark: gruvboxDarkBlueBright,
79+ Light: gruvboxLightBlueBright,
80+ }
81+ theme.SecondaryColor = lipgloss.AdaptiveColor{
82+ Dark: gruvboxDarkPurpleBright,
83+ Light: gruvboxLightPurpleBright,
84+ }
85+ theme.AccentColor = lipgloss.AdaptiveColor{
86+ Dark: gruvboxDarkOrangeBright,
87+ Light: gruvboxLightOrangeBright,
88+ }
89+
90+ // Status colors
91+ theme.ErrorColor = lipgloss.AdaptiveColor{
92+ Dark: gruvboxDarkRedBright,
93+ Light: gruvboxLightRedBright,
94+ }
95+ theme.WarningColor = lipgloss.AdaptiveColor{
96+ Dark: gruvboxDarkYellowBright,
97+ Light: gruvboxLightYellowBright,
98+ }
99+ theme.SuccessColor = lipgloss.AdaptiveColor{
100+ Dark: gruvboxDarkGreenBright,
101+ Light: gruvboxLightGreenBright,
102+ }
103+ theme.InfoColor = lipgloss.AdaptiveColor{
104+ Dark: gruvboxDarkBlueBright,
105+ Light: gruvboxLightBlueBright,
106+ }
107+
108+ // Text colors
109+ theme.TextColor = lipgloss.AdaptiveColor{
110+ Dark: gruvboxDarkFg1,
111+ Light: gruvboxLightFg1,
112+ }
113+ theme.TextMutedColor = lipgloss.AdaptiveColor{
114+ Dark: gruvboxDarkFg4,
115+ Light: gruvboxLightFg4,
116+ }
117+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
118+ Dark: gruvboxDarkYellowBright,
119+ Light: gruvboxLightYellowBright,
120+ }
121+
122+ // Background colors
123+ theme.BackgroundColor = lipgloss.AdaptiveColor{
124+ Dark: gruvboxDarkBg0,
125+ Light: gruvboxLightBg0,
126+ }
127+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
128+ Dark: gruvboxDarkBg1,
129+ Light: gruvboxLightBg1,
130+ }
131+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
132+ Dark: gruvboxDarkBg0Soft,
133+ Light: gruvboxLightBg0Soft,
134+ }
135+
136+ // Border colors
137+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
138+ Dark: gruvboxDarkBg2,
139+ Light: gruvboxLightBg2,
140+ }
141+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
142+ Dark: gruvboxDarkBlueBright,
143+ Light: gruvboxLightBlueBright,
144+ }
145+ theme.BorderDimColor = lipgloss.AdaptiveColor{
146+ Dark: gruvboxDarkBg1,
147+ Light: gruvboxLightBg1,
148+ }
149+
150+ // Diff view colors
151+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
152+ Dark: gruvboxDarkGreenBright,
153+ Light: gruvboxLightGreenBright,
154+ }
155+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
156+ Dark: gruvboxDarkRedBright,
157+ Light: gruvboxLightRedBright,
158+ }
159+ theme.DiffContextColor = lipgloss.AdaptiveColor{
160+ Dark: gruvboxDarkFg4,
161+ Light: gruvboxLightFg4,
162+ }
163+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
164+ Dark: gruvboxDarkFg3,
165+ Light: gruvboxLightFg3,
166+ }
167+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
168+ Dark: gruvboxDarkGreenBright,
169+ Light: gruvboxLightGreenBright,
170+ }
171+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
172+ Dark: gruvboxDarkRedBright,
173+ Light: gruvboxLightRedBright,
174+ }
175+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
176+ Dark: "#3C4C3C", // Darker green background
177+ Light: "#E8F5E9", // Light green background
178+ }
179+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
180+ Dark: "#4C3C3C", // Darker red background
181+ Light: "#FFEBEE", // Light red background
182+ }
183+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
184+ Dark: gruvboxDarkBg0,
185+ Light: gruvboxLightBg0,
186+ }
187+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
188+ Dark: gruvboxDarkFg4,
189+ Light: gruvboxLightFg4,
190+ }
191+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
192+ Dark: "#32432F", // Slightly darker green
193+ Light: "#C8E6C9", // Light green
194+ }
195+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
196+ Dark: "#43322F", // Slightly darker red
197+ Light: "#FFCDD2", // Light red
198+ }
199+
200+ // Markdown colors
201+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
202+ Dark: gruvboxDarkFg1,
203+ Light: gruvboxLightFg1,
204+ }
205+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
206+ Dark: gruvboxDarkYellowBright,
207+ Light: gruvboxLightYellowBright,
208+ }
209+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
210+ Dark: gruvboxDarkBlueBright,
211+ Light: gruvboxLightBlueBright,
212+ }
213+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
214+ Dark: gruvboxDarkAquaBright,
215+ Light: gruvboxLightAquaBright,
216+ }
217+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
218+ Dark: gruvboxDarkGreenBright,
219+ Light: gruvboxLightGreenBright,
220+ }
221+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
222+ Dark: gruvboxDarkAquaBright,
223+ Light: gruvboxLightAquaBright,
224+ }
225+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
226+ Dark: gruvboxDarkYellowBright,
227+ Light: gruvboxLightYellowBright,
228+ }
229+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
230+ Dark: gruvboxDarkOrangeBright,
231+ Light: gruvboxLightOrangeBright,
232+ }
233+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
234+ Dark: gruvboxDarkBg3,
235+ Light: gruvboxLightBg3,
236+ }
237+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
238+ Dark: gruvboxDarkBlueBright,
239+ Light: gruvboxLightBlueBright,
240+ }
241+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
242+ Dark: gruvboxDarkBlueBright,
243+ Light: gruvboxLightBlueBright,
244+ }
245+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
246+ Dark: gruvboxDarkPurpleBright,
247+ Light: gruvboxLightPurpleBright,
248+ }
249+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
250+ Dark: gruvboxDarkAquaBright,
251+ Light: gruvboxLightAquaBright,
252+ }
253+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
254+ Dark: gruvboxDarkFg1,
255+ Light: gruvboxLightFg1,
256+ }
257+
258+ // Syntax highlighting colors
259+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
260+ Dark: gruvboxDarkGray,
261+ Light: gruvboxLightGray,
262+ }
263+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
264+ Dark: gruvboxDarkRedBright,
265+ Light: gruvboxLightRedBright,
266+ }
267+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
268+ Dark: gruvboxDarkGreenBright,
269+ Light: gruvboxLightGreenBright,
270+ }
271+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
272+ Dark: gruvboxDarkBlueBright,
273+ Light: gruvboxLightBlueBright,
274+ }
275+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
276+ Dark: gruvboxDarkYellowBright,
277+ Light: gruvboxLightYellowBright,
278+ }
279+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
280+ Dark: gruvboxDarkPurpleBright,
281+ Light: gruvboxLightPurpleBright,
282+ }
283+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
284+ Dark: gruvboxDarkYellow,
285+ Light: gruvboxLightYellow,
286+ }
287+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
288+ Dark: gruvboxDarkAquaBright,
289+ Light: gruvboxLightAquaBright,
290+ }
291+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
292+ Dark: gruvboxDarkFg1,
293+ Light: gruvboxLightFg1,
294+ }
295+
296+ return theme
297+}
298+
299+func init() {
300+ // Register the Gruvbox theme with the theme manager
301+ RegisterTheme("gruvbox", NewGruvboxTheme())
302+}
internal/tui/theme/manager.gonew file+118
@@ -0,0 +1,118 @@
1+package theme
2+
3+import (
4+ "fmt"
5+ "slices"
6+ "strings"
7+ "sync"
8+
9+ "github.com/alecthomas/chroma/v2/styles"
10+ "github.com/opencode-ai/opencode/internal/config"
11+ "github.com/opencode-ai/opencode/internal/logging"
12+)
13+
14+// Manager handles theme registration, selection, and retrieval.
15+// It maintains a registry of available themes and tracks the currently active theme.
16+type Manager struct {
17+ themes map[string]Theme
18+ currentName string
19+ mu sync.RWMutex
20+}
21+
22+// Global instance of the theme manager
23+var globalManager = &Manager{
24+ themes: make(map[string]Theme),
25+ currentName: "",
26+}
27+
28+// RegisterTheme adds a new theme to the registry.
29+// If this is the first theme registered, it becomes the default.
30+func RegisterTheme(name string, theme Theme) {
31+ globalManager.mu.Lock()
32+ defer globalManager.mu.Unlock()
33+
34+ globalManager.themes[name] = theme
35+
36+ // If this is the first theme, make it the default
37+ if globalManager.currentName == "" {
38+ globalManager.currentName = name
39+ }
40+}
41+
42+// SetTheme changes the active theme to the one with the specified name.
43+// Returns an error if the theme doesn't exist.
44+func SetTheme(name string) error {
45+ globalManager.mu.Lock()
46+ defer globalManager.mu.Unlock()
47+
48+ delete(styles.Registry, "charm")
49+ if _, exists := globalManager.themes[name]; !exists {
50+ return fmt.Errorf("theme '%s' not found", name)
51+ }
52+
53+ globalManager.currentName = name
54+
55+ // Update the config file using viper
56+ if err := updateConfigTheme(name); err != nil {
57+ // Log the error but don't fail the theme change
58+ logging.Warn("Warning: Failed to update config file with new theme", "err", err)
59+ }
60+
61+ return nil
62+}
63+
64+// CurrentTheme returns the currently active theme.
65+// If no theme is set, it returns nil.
66+func CurrentTheme() Theme {
67+ globalManager.mu.RLock()
68+ defer globalManager.mu.RUnlock()
69+
70+ if globalManager.currentName == "" {
71+ return nil
72+ }
73+
74+ return globalManager.themes[globalManager.currentName]
75+}
76+
77+// CurrentThemeName returns the name of the currently active theme.
78+func CurrentThemeName() string {
79+ globalManager.mu.RLock()
80+ defer globalManager.mu.RUnlock()
81+
82+ return globalManager.currentName
83+}
84+
85+// AvailableThemes returns a list of all registered theme names.
86+func AvailableThemes() []string {
87+ globalManager.mu.RLock()
88+ defer globalManager.mu.RUnlock()
89+
90+ names := make([]string, 0, len(globalManager.themes))
91+ for name := range globalManager.themes {
92+ names = append(names, name)
93+ }
94+ slices.SortFunc(names, func(a, b string) int {
95+ if a == "opencode" {
96+ return -1
97+ } else if b == "opencode" {
98+ return 1
99+ }
100+ return strings.Compare(a, b)
101+ })
102+ return names
103+}
104+
105+// GetTheme returns a specific theme by name.
106+// Returns nil if the theme doesn't exist.
107+func GetTheme(name string) Theme {
108+ globalManager.mu.RLock()
109+ defer globalManager.mu.RUnlock()
110+
111+ return globalManager.themes[name]
112+}
113+
114+// updateConfigTheme updates the theme setting in the configuration file
115+func updateConfigTheme(themeName string) error {
116+ // Use the config package to update the theme
117+ return config.UpdateTheme(themeName)
118+}
internal/tui/theme/monokai.gonew file+273
@@ -0,0 +1,273 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// MonokaiProTheme implements the Theme interface with Monokai Pro colors.
8+// It provides both dark and light variants.
9+type MonokaiProTheme struct {
10+ BaseTheme
11+}
12+
13+// NewMonokaiProTheme creates a new instance of the Monokai Pro theme.
14+func NewMonokaiProTheme() *MonokaiProTheme {
15+ // Monokai Pro color palette (dark mode)
16+ darkBackground := "#2d2a2e"
17+ darkCurrentLine := "#403e41"
18+ darkSelection := "#5b595c"
19+ darkForeground := "#fcfcfa"
20+ darkComment := "#727072"
21+ darkRed := "#ff6188"
22+ darkOrange := "#fc9867"
23+ darkYellow := "#ffd866"
24+ darkGreen := "#a9dc76"
25+ darkCyan := "#78dce8"
26+ darkBlue := "#ab9df2"
27+ darkPurple := "#ab9df2"
28+ darkBorder := "#403e41"
29+
30+ // Light mode colors (adapted from dark)
31+ lightBackground := "#fafafa"
32+ lightCurrentLine := "#f0f0f0"
33+ lightSelection := "#e5e5e6"
34+ lightForeground := "#2d2a2e"
35+ lightComment := "#939293"
36+ lightRed := "#f92672"
37+ lightOrange := "#fd971f"
38+ lightYellow := "#e6db74"
39+ lightGreen := "#9bca65"
40+ lightCyan := "#66d9ef"
41+ lightBlue := "#7e75db"
42+ lightPurple := "#ae81ff"
43+ lightBorder := "#d3d3d3"
44+
45+ theme := &MonokaiProTheme{}
46+
47+ // Base colors
48+ theme.PrimaryColor = lipgloss.AdaptiveColor{
49+ Dark: darkCyan,
50+ Light: lightCyan,
51+ }
52+ theme.SecondaryColor = lipgloss.AdaptiveColor{
53+ Dark: darkPurple,
54+ Light: lightPurple,
55+ }
56+ theme.AccentColor = lipgloss.AdaptiveColor{
57+ Dark: darkOrange,
58+ Light: lightOrange,
59+ }
60+
61+ // Status colors
62+ theme.ErrorColor = lipgloss.AdaptiveColor{
63+ Dark: darkRed,
64+ Light: lightRed,
65+ }
66+ theme.WarningColor = lipgloss.AdaptiveColor{
67+ Dark: darkOrange,
68+ Light: lightOrange,
69+ }
70+ theme.SuccessColor = lipgloss.AdaptiveColor{
71+ Dark: darkGreen,
72+ Light: lightGreen,
73+ }
74+ theme.InfoColor = lipgloss.AdaptiveColor{
75+ Dark: darkBlue,
76+ Light: lightBlue,
77+ }
78+
79+ // Text colors
80+ theme.TextColor = lipgloss.AdaptiveColor{
81+ Dark: darkForeground,
82+ Light: lightForeground,
83+ }
84+ theme.TextMutedColor = lipgloss.AdaptiveColor{
85+ Dark: darkComment,
86+ Light: lightComment,
87+ }
88+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
89+ Dark: darkYellow,
90+ Light: lightYellow,
91+ }
92+
93+ // Background colors
94+ theme.BackgroundColor = lipgloss.AdaptiveColor{
95+ Dark: darkBackground,
96+ Light: lightBackground,
97+ }
98+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
99+ Dark: darkCurrentLine,
100+ Light: lightCurrentLine,
101+ }
102+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
103+ Dark: "#221f22", // Slightly darker than background
104+ Light: "#ffffff", // Slightly lighter than background
105+ }
106+
107+ // Border colors
108+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
109+ Dark: darkBorder,
110+ Light: lightBorder,
111+ }
112+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
113+ Dark: darkCyan,
114+ Light: lightCyan,
115+ }
116+ theme.BorderDimColor = lipgloss.AdaptiveColor{
117+ Dark: darkSelection,
118+ Light: lightSelection,
119+ }
120+
121+ // Diff view colors
122+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
123+ Dark: "#a9dc76",
124+ Light: "#9bca65",
125+ }
126+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
127+ Dark: "#ff6188",
128+ Light: "#f92672",
129+ }
130+ theme.DiffContextColor = lipgloss.AdaptiveColor{
131+ Dark: "#a0a0a0",
132+ Light: "#757575",
133+ }
134+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
135+ Dark: "#a0a0a0",
136+ Light: "#757575",
137+ }
138+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
139+ Dark: "#c2e7a9",
140+ Light: "#c5e0b4",
141+ }
142+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
143+ Dark: "#ff8ca6",
144+ Light: "#ffb3c8",
145+ }
146+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
147+ Dark: "#3a4a35",
148+ Light: "#e8f5e9",
149+ }
150+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
151+ Dark: "#4a3439",
152+ Light: "#ffebee",
153+ }
154+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
155+ Dark: darkBackground,
156+ Light: lightBackground,
157+ }
158+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
159+ Dark: "#888888",
160+ Light: "#9e9e9e",
161+ }
162+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
163+ Dark: "#2d3a28",
164+ Light: "#c8e6c9",
165+ }
166+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
167+ Dark: "#3d2a2e",
168+ Light: "#ffcdd2",
169+ }
170+
171+ // Markdown colors
172+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
173+ Dark: darkForeground,
174+ Light: lightForeground,
175+ }
176+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
177+ Dark: darkPurple,
178+ Light: lightPurple,
179+ }
180+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
181+ Dark: darkCyan,
182+ Light: lightCyan,
183+ }
184+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
185+ Dark: darkBlue,
186+ Light: lightBlue,
187+ }
188+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
189+ Dark: darkGreen,
190+ Light: lightGreen,
191+ }
192+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
193+ Dark: darkYellow,
194+ Light: lightYellow,
195+ }
196+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
197+ Dark: darkYellow,
198+ Light: lightYellow,
199+ }
200+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
201+ Dark: darkOrange,
202+ Light: lightOrange,
203+ }
204+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
205+ Dark: darkComment,
206+ Light: lightComment,
207+ }
208+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
209+ Dark: darkCyan,
210+ Light: lightCyan,
211+ }
212+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
213+ Dark: darkBlue,
214+ Light: lightBlue,
215+ }
216+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
217+ Dark: darkCyan,
218+ Light: lightCyan,
219+ }
220+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
221+ Dark: darkBlue,
222+ Light: lightBlue,
223+ }
224+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
225+ Dark: darkForeground,
226+ Light: lightForeground,
227+ }
228+
229+ // Syntax highlighting colors
230+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
231+ Dark: darkComment,
232+ Light: lightComment,
233+ }
234+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
235+ Dark: darkRed,
236+ Light: lightRed,
237+ }
238+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
239+ Dark: darkGreen,
240+ Light: lightGreen,
241+ }
242+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
243+ Dark: darkForeground,
244+ Light: lightForeground,
245+ }
246+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
247+ Dark: darkYellow,
248+ Light: lightYellow,
249+ }
250+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
251+ Dark: darkPurple,
252+ Light: lightPurple,
253+ }
254+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
255+ Dark: darkBlue,
256+ Light: lightBlue,
257+ }
258+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
259+ Dark: darkCyan,
260+ Light: lightCyan,
261+ }
262+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
263+ Dark: darkForeground,
264+ Light: lightForeground,
265+ }
266+
267+ return theme
268+}
269+
270+func init() {
271+ // Register the Monokai Pro theme with the theme manager
272+ RegisterTheme("monokai", NewMonokaiProTheme())
273+}
internal/tui/theme/onedark.gonew file+274
@@ -0,0 +1,274 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// OneDarkTheme implements the Theme interface with Atom's One Dark colors.
8+// It provides both dark and light variants.
9+type OneDarkTheme struct {
10+ BaseTheme
11+}
12+
13+// NewOneDarkTheme creates a new instance of the One Dark theme.
14+func NewOneDarkTheme() *OneDarkTheme {
15+ // One Dark color palette
16+ // Dark mode colors from Atom One Dark
17+ darkBackground := "#282c34"
18+ darkCurrentLine := "#2c313c"
19+ darkSelection := "#3e4451"
20+ darkForeground := "#abb2bf"
21+ darkComment := "#5c6370"
22+ darkRed := "#e06c75"
23+ darkOrange := "#d19a66"
24+ darkYellow := "#e5c07b"
25+ darkGreen := "#98c379"
26+ darkCyan := "#56b6c2"
27+ darkBlue := "#61afef"
28+ darkPurple := "#c678dd"
29+ darkBorder := "#3b4048"
30+
31+ // Light mode colors from Atom One Light
32+ lightBackground := "#fafafa"
33+ lightCurrentLine := "#f0f0f0"
34+ lightSelection := "#e5e5e6"
35+ lightForeground := "#383a42"
36+ lightComment := "#a0a1a7"
37+ lightRed := "#e45649"
38+ lightOrange := "#da8548"
39+ lightYellow := "#c18401"
40+ lightGreen := "#50a14f"
41+ lightCyan := "#0184bc"
42+ lightBlue := "#4078f2"
43+ lightPurple := "#a626a4"
44+ lightBorder := "#d3d3d3"
45+
46+ theme := &OneDarkTheme{}
47+
48+ // Base colors
49+ theme.PrimaryColor = lipgloss.AdaptiveColor{
50+ Dark: darkBlue,
51+ Light: lightBlue,
52+ }
53+ theme.SecondaryColor = lipgloss.AdaptiveColor{
54+ Dark: darkPurple,
55+ Light: lightPurple,
56+ }
57+ theme.AccentColor = lipgloss.AdaptiveColor{
58+ Dark: darkOrange,
59+ Light: lightOrange,
60+ }
61+
62+ // Status colors
63+ theme.ErrorColor = lipgloss.AdaptiveColor{
64+ Dark: darkRed,
65+ Light: lightRed,
66+ }
67+ theme.WarningColor = lipgloss.AdaptiveColor{
68+ Dark: darkOrange,
69+ Light: lightOrange,
70+ }
71+ theme.SuccessColor = lipgloss.AdaptiveColor{
72+ Dark: darkGreen,
73+ Light: lightGreen,
74+ }
75+ theme.InfoColor = lipgloss.AdaptiveColor{
76+ Dark: darkBlue,
77+ Light: lightBlue,
78+ }
79+
80+ // Text colors
81+ theme.TextColor = lipgloss.AdaptiveColor{
82+ Dark: darkForeground,
83+ Light: lightForeground,
84+ }
85+ theme.TextMutedColor = lipgloss.AdaptiveColor{
86+ Dark: darkComment,
87+ Light: lightComment,
88+ }
89+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
90+ Dark: darkYellow,
91+ Light: lightYellow,
92+ }
93+
94+ // Background colors
95+ theme.BackgroundColor = lipgloss.AdaptiveColor{
96+ Dark: darkBackground,
97+ Light: lightBackground,
98+ }
99+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
100+ Dark: darkCurrentLine,
101+ Light: lightCurrentLine,
102+ }
103+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
104+ Dark: "#21252b", // Slightly darker than background
105+ Light: "#ffffff", // Slightly lighter than background
106+ }
107+
108+ // Border colors
109+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
110+ Dark: darkBorder,
111+ Light: lightBorder,
112+ }
113+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
114+ Dark: darkBlue,
115+ Light: lightBlue,
116+ }
117+ theme.BorderDimColor = lipgloss.AdaptiveColor{
118+ Dark: darkSelection,
119+ Light: lightSelection,
120+ }
121+
122+ // Diff view colors
123+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
124+ Dark: "#478247",
125+ Light: "#2E7D32",
126+ }
127+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
128+ Dark: "#7C4444",
129+ Light: "#C62828",
130+ }
131+ theme.DiffContextColor = lipgloss.AdaptiveColor{
132+ Dark: "#a0a0a0",
133+ Light: "#757575",
134+ }
135+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
136+ Dark: "#a0a0a0",
137+ Light: "#757575",
138+ }
139+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
140+ Dark: "#DAFADA",
141+ Light: "#A5D6A7",
142+ }
143+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
144+ Dark: "#FADADD",
145+ Light: "#EF9A9A",
146+ }
147+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
148+ Dark: "#303A30",
149+ Light: "#E8F5E9",
150+ }
151+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
152+ Dark: "#3A3030",
153+ Light: "#FFEBEE",
154+ }
155+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
156+ Dark: darkBackground,
157+ Light: lightBackground,
158+ }
159+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
160+ Dark: "#888888",
161+ Light: "#9E9E9E",
162+ }
163+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
164+ Dark: "#293229",
165+ Light: "#C8E6C9",
166+ }
167+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
168+ Dark: "#332929",
169+ Light: "#FFCDD2",
170+ }
171+
172+ // Markdown colors
173+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
174+ Dark: darkForeground,
175+ Light: lightForeground,
176+ }
177+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
178+ Dark: darkPurple,
179+ Light: lightPurple,
180+ }
181+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
182+ Dark: darkBlue,
183+ Light: lightBlue,
184+ }
185+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
186+ Dark: darkCyan,
187+ Light: lightCyan,
188+ }
189+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
190+ Dark: darkGreen,
191+ Light: lightGreen,
192+ }
193+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
194+ Dark: darkYellow,
195+ Light: lightYellow,
196+ }
197+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
198+ Dark: darkYellow,
199+ Light: lightYellow,
200+ }
201+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
202+ Dark: darkOrange,
203+ Light: lightOrange,
204+ }
205+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
206+ Dark: darkComment,
207+ Light: lightComment,
208+ }
209+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
210+ Dark: darkBlue,
211+ Light: lightBlue,
212+ }
213+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
214+ Dark: darkCyan,
215+ Light: lightCyan,
216+ }
217+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
218+ Dark: darkBlue,
219+ Light: lightBlue,
220+ }
221+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
222+ Dark: darkCyan,
223+ Light: lightCyan,
224+ }
225+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
226+ Dark: darkForeground,
227+ Light: lightForeground,
228+ }
229+
230+ // Syntax highlighting colors
231+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
232+ Dark: darkComment,
233+ Light: lightComment,
234+ }
235+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
236+ Dark: darkPurple,
237+ Light: lightPurple,
238+ }
239+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
240+ Dark: darkBlue,
241+ Light: lightBlue,
242+ }
243+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
244+ Dark: darkRed,
245+ Light: lightRed,
246+ }
247+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
248+ Dark: darkGreen,
249+ Light: lightGreen,
250+ }
251+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
252+ Dark: darkOrange,
253+ Light: lightOrange,
254+ }
255+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
256+ Dark: darkYellow,
257+ Light: lightYellow,
258+ }
259+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
260+ Dark: darkCyan,
261+ Light: lightCyan,
262+ }
263+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
264+ Dark: darkForeground,
265+ Light: lightForeground,
266+ }
267+
268+ return theme
269+}
270+
271+func init() {
272+ // Register the One Dark theme with the theme manager
273+ RegisterTheme("onedark", NewOneDarkTheme())
274+}
internal/tui/theme/opencode.gonew file+277
@@ -0,0 +1,277 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
8+// It provides both dark and light variants.
9+type OpenCodeTheme struct {
10+ BaseTheme
11+}
12+
13+// NewOpenCodeTheme creates a new instance of the OpenCode theme.
14+func NewOpenCodeTheme() *OpenCodeTheme {
15+ // OpenCode color palette
16+ // Dark mode colors
17+ darkBackground := "#212121"
18+ darkCurrentLine := "#252525"
19+ darkSelection := "#303030"
20+ darkForeground := "#e0e0e0"
21+ darkComment := "#6a6a6a"
22+ darkPrimary := "#fab283" // Primary orange/gold
23+ darkSecondary := "#5c9cf5" // Secondary blue
24+ darkAccent := "#9d7cd8" // Accent purple
25+ darkRed := "#e06c75" // Error red
26+ darkOrange := "#f5a742" // Warning orange
27+ darkGreen := "#7fd88f" // Success green
28+ darkCyan := "#56b6c2" // Info cyan
29+ darkYellow := "#e5c07b" // Emphasized text
30+ darkBorder := "#4b4c5c" // Border color
31+
32+ // Light mode colors
33+ lightBackground := "#f8f8f8"
34+ lightCurrentLine := "#f0f0f0"
35+ lightSelection := "#e5e5e6"
36+ lightForeground := "#2a2a2a"
37+ lightComment := "#8a8a8a"
38+ lightPrimary := "#3b7dd8" // Primary blue
39+ lightSecondary := "#7b5bb6" // Secondary purple
40+ lightAccent := "#d68c27" // Accent orange/gold
41+ lightRed := "#d1383d" // Error red
42+ lightOrange := "#d68c27" // Warning orange
43+ lightGreen := "#3d9a57" // Success green
44+ lightCyan := "#318795" // Info cyan
45+ lightYellow := "#b0851f" // Emphasized text
46+ lightBorder := "#d3d3d3" // Border color
47+
48+ theme := &OpenCodeTheme{}
49+
50+ // Base colors
51+ theme.PrimaryColor = lipgloss.AdaptiveColor{
52+ Dark: darkPrimary,
53+ Light: lightPrimary,
54+ }
55+ theme.SecondaryColor = lipgloss.AdaptiveColor{
56+ Dark: darkSecondary,
57+ Light: lightSecondary,
58+ }
59+ theme.AccentColor = lipgloss.AdaptiveColor{
60+ Dark: darkAccent,
61+ Light: lightAccent,
62+ }
63+
64+ // Status colors
65+ theme.ErrorColor = lipgloss.AdaptiveColor{
66+ Dark: darkRed,
67+ Light: lightRed,
68+ }
69+ theme.WarningColor = lipgloss.AdaptiveColor{
70+ Dark: darkOrange,
71+ Light: lightOrange,
72+ }
73+ theme.SuccessColor = lipgloss.AdaptiveColor{
74+ Dark: darkGreen,
75+ Light: lightGreen,
76+ }
77+ theme.InfoColor = lipgloss.AdaptiveColor{
78+ Dark: darkCyan,
79+ Light: lightCyan,
80+ }
81+
82+ // Text colors
83+ theme.TextColor = lipgloss.AdaptiveColor{
84+ Dark: darkForeground,
85+ Light: lightForeground,
86+ }
87+ theme.TextMutedColor = lipgloss.AdaptiveColor{
88+ Dark: darkComment,
89+ Light: lightComment,
90+ }
91+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
92+ Dark: darkYellow,
93+ Light: lightYellow,
94+ }
95+
96+ // Background colors
97+ theme.BackgroundColor = lipgloss.AdaptiveColor{
98+ Dark: darkBackground,
99+ Light: lightBackground,
100+ }
101+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
102+ Dark: darkCurrentLine,
103+ Light: lightCurrentLine,
104+ }
105+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
106+ Dark: "#121212", // Slightly darker than background
107+ Light: "#ffffff", // Slightly lighter than background
108+ }
109+
110+ // Border colors
111+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
112+ Dark: darkBorder,
113+ Light: lightBorder,
114+ }
115+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
116+ Dark: darkPrimary,
117+ Light: lightPrimary,
118+ }
119+ theme.BorderDimColor = lipgloss.AdaptiveColor{
120+ Dark: darkSelection,
121+ Light: lightSelection,
122+ }
123+
124+ // Diff view colors
125+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
126+ Dark: "#478247",
127+ Light: "#2E7D32",
128+ }
129+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
130+ Dark: "#7C4444",
131+ Light: "#C62828",
132+ }
133+ theme.DiffContextColor = lipgloss.AdaptiveColor{
134+ Dark: "#a0a0a0",
135+ Light: "#757575",
136+ }
137+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
138+ Dark: "#a0a0a0",
139+ Light: "#757575",
140+ }
141+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
142+ Dark: "#DAFADA",
143+ Light: "#A5D6A7",
144+ }
145+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
146+ Dark: "#FADADD",
147+ Light: "#EF9A9A",
148+ }
149+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
150+ Dark: "#303A30",
151+ Light: "#E8F5E9",
152+ }
153+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
154+ Dark: "#3A3030",
155+ Light: "#FFEBEE",
156+ }
157+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
158+ Dark: darkBackground,
159+ Light: lightBackground,
160+ }
161+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
162+ Dark: "#888888",
163+ Light: "#9E9E9E",
164+ }
165+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
166+ Dark: "#293229",
167+ Light: "#C8E6C9",
168+ }
169+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
170+ Dark: "#332929",
171+ Light: "#FFCDD2",
172+ }
173+
174+ // Markdown colors
175+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
176+ Dark: darkForeground,
177+ Light: lightForeground,
178+ }
179+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
180+ Dark: darkSecondary,
181+ Light: lightSecondary,
182+ }
183+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
184+ Dark: darkPrimary,
185+ Light: lightPrimary,
186+ }
187+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
188+ Dark: darkCyan,
189+ Light: lightCyan,
190+ }
191+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
192+ Dark: darkGreen,
193+ Light: lightGreen,
194+ }
195+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
196+ Dark: darkYellow,
197+ Light: lightYellow,
198+ }
199+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
200+ Dark: darkYellow,
201+ Light: lightYellow,
202+ }
203+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
204+ Dark: darkAccent,
205+ Light: lightAccent,
206+ }
207+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
208+ Dark: darkComment,
209+ Light: lightComment,
210+ }
211+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
212+ Dark: darkPrimary,
213+ Light: lightPrimary,
214+ }
215+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
216+ Dark: darkCyan,
217+ Light: lightCyan,
218+ }
219+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
220+ Dark: darkPrimary,
221+ Light: lightPrimary,
222+ }
223+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
224+ Dark: darkCyan,
225+ Light: lightCyan,
226+ }
227+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
228+ Dark: darkForeground,
229+ Light: lightForeground,
230+ }
231+
232+ // Syntax highlighting colors
233+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
234+ Dark: darkComment,
235+ Light: lightComment,
236+ }
237+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
238+ Dark: darkSecondary,
239+ Light: lightSecondary,
240+ }
241+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
242+ Dark: darkPrimary,
243+ Light: lightPrimary,
244+ }
245+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
246+ Dark: darkRed,
247+ Light: lightRed,
248+ }
249+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
250+ Dark: darkGreen,
251+ Light: lightGreen,
252+ }
253+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
254+ Dark: darkAccent,
255+ Light: lightAccent,
256+ }
257+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
258+ Dark: darkYellow,
259+ Light: lightYellow,
260+ }
261+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
262+ Dark: darkCyan,
263+ Light: lightCyan,
264+ }
265+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
266+ Dark: darkForeground,
267+ Light: lightForeground,
268+ }
269+
270+ return theme
271+}
272+
273+func init() {
274+ // Register the OpenCode theme with the theme manager
275+ RegisterTheme("opencode", NewOpenCodeTheme())
276+}
277+
internal/tui/theme/theme.gonew file+208
@@ -0,0 +1,208 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// Theme defines the interface for all UI themes in the application.
8+// All colors must be defined as lipgloss.AdaptiveColor to support
9+// both light and dark terminal backgrounds.
10+type Theme interface {
11+ // Base colors
12+ Primary() lipgloss.AdaptiveColor
13+ Secondary() lipgloss.AdaptiveColor
14+ Accent() lipgloss.AdaptiveColor
15+
16+ // Status colors
17+ Error() lipgloss.AdaptiveColor
18+ Warning() lipgloss.AdaptiveColor
19+ Success() lipgloss.AdaptiveColor
20+ Info() lipgloss.AdaptiveColor
21+
22+ // Text colors
23+ Text() lipgloss.AdaptiveColor
24+ TextMuted() lipgloss.AdaptiveColor
25+ TextEmphasized() lipgloss.AdaptiveColor
26+
27+ // Background colors
28+ Background() lipgloss.AdaptiveColor
29+ BackgroundSecondary() lipgloss.AdaptiveColor
30+ BackgroundDarker() lipgloss.AdaptiveColor
31+
32+ // Border colors
33+ BorderNormal() lipgloss.AdaptiveColor
34+ BorderFocused() lipgloss.AdaptiveColor
35+ BorderDim() lipgloss.AdaptiveColor
36+
37+ // Diff view colors
38+ DiffAdded() lipgloss.AdaptiveColor
39+ DiffRemoved() lipgloss.AdaptiveColor
40+ DiffContext() lipgloss.AdaptiveColor
41+ DiffHunkHeader() lipgloss.AdaptiveColor
42+ DiffHighlightAdded() lipgloss.AdaptiveColor
43+ DiffHighlightRemoved() lipgloss.AdaptiveColor
44+ DiffAddedBg() lipgloss.AdaptiveColor
45+ DiffRemovedBg() lipgloss.AdaptiveColor
46+ DiffContextBg() lipgloss.AdaptiveColor
47+ DiffLineNumber() lipgloss.AdaptiveColor
48+ DiffAddedLineNumberBg() lipgloss.AdaptiveColor
49+ DiffRemovedLineNumberBg() lipgloss.AdaptiveColor
50+
51+ // Markdown colors
52+ MarkdownText() lipgloss.AdaptiveColor
53+ MarkdownHeading() lipgloss.AdaptiveColor
54+ MarkdownLink() lipgloss.AdaptiveColor
55+ MarkdownLinkText() lipgloss.AdaptiveColor
56+ MarkdownCode() lipgloss.AdaptiveColor
57+ MarkdownBlockQuote() lipgloss.AdaptiveColor
58+ MarkdownEmph() lipgloss.AdaptiveColor
59+ MarkdownStrong() lipgloss.AdaptiveColor
60+ MarkdownHorizontalRule() lipgloss.AdaptiveColor
61+ MarkdownListItem() lipgloss.AdaptiveColor
62+ MarkdownListEnumeration() lipgloss.AdaptiveColor
63+ MarkdownImage() lipgloss.AdaptiveColor
64+ MarkdownImageText() lipgloss.AdaptiveColor
65+ MarkdownCodeBlock() lipgloss.AdaptiveColor
66+
67+ // Syntax highlighting colors
68+ SyntaxComment() lipgloss.AdaptiveColor
69+ SyntaxKeyword() lipgloss.AdaptiveColor
70+ SyntaxFunction() lipgloss.AdaptiveColor
71+ SyntaxVariable() lipgloss.AdaptiveColor
72+ SyntaxString() lipgloss.AdaptiveColor
73+ SyntaxNumber() lipgloss.AdaptiveColor
74+ SyntaxType() lipgloss.AdaptiveColor
75+ SyntaxOperator() lipgloss.AdaptiveColor
76+ SyntaxPunctuation() lipgloss.AdaptiveColor
77+}
78+
79+// BaseTheme provides a default implementation of the Theme interface
80+// that can be embedded in concrete theme implementations.
81+type BaseTheme struct {
82+ // Base colors
83+ PrimaryColor lipgloss.AdaptiveColor
84+ SecondaryColor lipgloss.AdaptiveColor
85+ AccentColor lipgloss.AdaptiveColor
86+
87+ // Status colors
88+ ErrorColor lipgloss.AdaptiveColor
89+ WarningColor lipgloss.AdaptiveColor
90+ SuccessColor lipgloss.AdaptiveColor
91+ InfoColor lipgloss.AdaptiveColor
92+
93+ // Text colors
94+ TextColor lipgloss.AdaptiveColor
95+ TextMutedColor lipgloss.AdaptiveColor
96+ TextEmphasizedColor lipgloss.AdaptiveColor
97+
98+ // Background colors
99+ BackgroundColor lipgloss.AdaptiveColor
100+ BackgroundSecondaryColor lipgloss.AdaptiveColor
101+ BackgroundDarkerColor lipgloss.AdaptiveColor
102+
103+ // Border colors
104+ BorderNormalColor lipgloss.AdaptiveColor
105+ BorderFocusedColor lipgloss.AdaptiveColor
106+ BorderDimColor lipgloss.AdaptiveColor
107+
108+ // Diff view colors
109+ DiffAddedColor lipgloss.AdaptiveColor
110+ DiffRemovedColor lipgloss.AdaptiveColor
111+ DiffContextColor lipgloss.AdaptiveColor
112+ DiffHunkHeaderColor lipgloss.AdaptiveColor
113+ DiffHighlightAddedColor lipgloss.AdaptiveColor
114+ DiffHighlightRemovedColor lipgloss.AdaptiveColor
115+ DiffAddedBgColor lipgloss.AdaptiveColor
116+ DiffRemovedBgColor lipgloss.AdaptiveColor
117+ DiffContextBgColor lipgloss.AdaptiveColor
118+ DiffLineNumberColor lipgloss.AdaptiveColor
119+ DiffAddedLineNumberBgColor lipgloss.AdaptiveColor
120+ DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor
121+
122+ // Markdown colors
123+ MarkdownTextColor lipgloss.AdaptiveColor
124+ MarkdownHeadingColor lipgloss.AdaptiveColor
125+ MarkdownLinkColor lipgloss.AdaptiveColor
126+ MarkdownLinkTextColor lipgloss.AdaptiveColor
127+ MarkdownCodeColor lipgloss.AdaptiveColor
128+ MarkdownBlockQuoteColor lipgloss.AdaptiveColor
129+ MarkdownEmphColor lipgloss.AdaptiveColor
130+ MarkdownStrongColor lipgloss.AdaptiveColor
131+ MarkdownHorizontalRuleColor lipgloss.AdaptiveColor
132+ MarkdownListItemColor lipgloss.AdaptiveColor
133+ MarkdownListEnumerationColor lipgloss.AdaptiveColor
134+ MarkdownImageColor lipgloss.AdaptiveColor
135+ MarkdownImageTextColor lipgloss.AdaptiveColor
136+ MarkdownCodeBlockColor lipgloss.AdaptiveColor
137+
138+ // Syntax highlighting colors
139+ SyntaxCommentColor lipgloss.AdaptiveColor
140+ SyntaxKeywordColor lipgloss.AdaptiveColor
141+ SyntaxFunctionColor lipgloss.AdaptiveColor
142+ SyntaxVariableColor lipgloss.AdaptiveColor
143+ SyntaxStringColor lipgloss.AdaptiveColor
144+ SyntaxNumberColor lipgloss.AdaptiveColor
145+ SyntaxTypeColor lipgloss.AdaptiveColor
146+ SyntaxOperatorColor lipgloss.AdaptiveColor
147+ SyntaxPunctuationColor lipgloss.AdaptiveColor
148+}
149+
150+// Implement the Theme interface for BaseTheme
151+func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor }
152+func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor }
153+func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor }
154+
155+func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor }
156+func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor }
157+func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
158+func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
159+
160+func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
161+func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
162+func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor }
163+
164+func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
165+func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor }
166+func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor }
167+
168+func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor }
169+func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor }
170+func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor }
171+
172+func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
173+func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
174+func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor }
175+func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor }
176+func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor }
177+func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor }
178+func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor }
179+func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor }
180+func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor }
181+func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor }
182+func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffAddedLineNumberBgColor }
183+func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffRemovedLineNumberBgColor }
184+
185+func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor }
186+func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor }
187+func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor }
188+func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor }
189+func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor }
190+func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor }
191+func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor }
192+func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor }
193+func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { return t.MarkdownHorizontalRuleColor }
194+func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor }
195+func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { return t.MarkdownListEnumerationColor }
196+func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor }
197+func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor }
198+func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor }
199+
200+func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor }
201+func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor }
202+func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor }
203+func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor }
204+func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor }
205+func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
206+func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
207+func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
208+func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
internal/tui/theme/theme_test.gonew file+89
@@ -0,0 +1,89 @@
1+package theme
2+
3+import (
4+ "testing"
5+)
6+
7+func TestThemeRegistration(t *testing.T) {
8+ // Get list of available themes
9+ availableThemes := AvailableThemes()
10+
11+ // Check if "catppuccin" theme is registered
12+ catppuccinFound := false
13+ for _, themeName := range availableThemes {
14+ if themeName == "catppuccin" {
15+ catppuccinFound = true
16+ break
17+ }
18+ }
19+
20+ if !catppuccinFound {
21+ t.Errorf("Catppuccin theme is not registered")
22+ }
23+
24+ // Check if "gruvbox" theme is registered
25+ gruvboxFound := false
26+ for _, themeName := range availableThemes {
27+ if themeName == "gruvbox" {
28+ gruvboxFound = true
29+ break
30+ }
31+ }
32+
33+ if !gruvboxFound {
34+ t.Errorf("Gruvbox theme is not registered")
35+ }
36+
37+ // Check if "monokai" theme is registered
38+ monokaiFound := false
39+ for _, themeName := range availableThemes {
40+ if themeName == "monokai" {
41+ monokaiFound = true
42+ break
43+ }
44+ }
45+
46+ if !monokaiFound {
47+ t.Errorf("Monokai theme is not registered")
48+ }
49+
50+ // Try to get the themes and make sure they're not nil
51+ catppuccin := GetTheme("catppuccin")
52+ if catppuccin == nil {
53+ t.Errorf("Catppuccin theme is nil")
54+ }
55+
56+ gruvbox := GetTheme("gruvbox")
57+ if gruvbox == nil {
58+ t.Errorf("Gruvbox theme is nil")
59+ }
60+
61+ monokai := GetTheme("monokai")
62+ if monokai == nil {
63+ t.Errorf("Monokai theme is nil")
64+ }
65+
66+ // Test switching theme
67+ originalTheme := CurrentThemeName()
68+
69+ err := SetTheme("gruvbox")
70+ if err != nil {
71+ t.Errorf("Failed to set theme to gruvbox: %v", err)
72+ }
73+
74+ if CurrentThemeName() != "gruvbox" {
75+ t.Errorf("Theme not properly switched to gruvbox")
76+ }
77+
78+ err = SetTheme("monokai")
79+ if err != nil {
80+ t.Errorf("Failed to set theme to monokai: %v", err)
81+ }
82+
83+ if CurrentThemeName() != "monokai" {
84+ t.Errorf("Theme not properly switched to monokai")
85+ }
86+
87+ // Switch back to original theme
88+ _ = SetTheme(originalTheme)
89+}
internal/tui/theme/tokyonight.gonew file+274
@@ -0,0 +1,274 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
8+// It provides both dark and light variants.
9+type TokyoNightTheme struct {
10+ BaseTheme
11+}
12+
13+// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
14+func NewTokyoNightTheme() *TokyoNightTheme {
15+ // Tokyo Night color palette
16+ // Dark mode colors
17+ darkBackground := "#222436"
18+ darkCurrentLine := "#1e2030"
19+ darkSelection := "#2f334d"
20+ darkForeground := "#c8d3f5"
21+ darkComment := "#636da6"
22+ darkRed := "#ff757f"
23+ darkOrange := "#ff966c"
24+ darkYellow := "#ffc777"
25+ darkGreen := "#c3e88d"
26+ darkCyan := "#86e1fc"
27+ darkBlue := "#82aaff"
28+ darkPurple := "#c099ff"
29+ darkBorder := "#3b4261"
30+
31+ // Light mode colors (Tokyo Night Day)
32+ lightBackground := "#e1e2e7"
33+ lightCurrentLine := "#d5d6db"
34+ lightSelection := "#c8c9ce"
35+ lightForeground := "#3760bf"
36+ lightComment := "#848cb5"
37+ lightRed := "#f52a65"
38+ lightOrange := "#b15c00"
39+ lightYellow := "#8c6c3e"
40+ lightGreen := "#587539"
41+ lightCyan := "#007197"
42+ lightBlue := "#2e7de9"
43+ lightPurple := "#9854f1"
44+ lightBorder := "#a8aecb"
45+
46+ theme := &TokyoNightTheme{}
47+
48+ // Base colors
49+ theme.PrimaryColor = lipgloss.AdaptiveColor{
50+ Dark: darkBlue,
51+ Light: lightBlue,
52+ }
53+ theme.SecondaryColor = lipgloss.AdaptiveColor{
54+ Dark: darkPurple,
55+ Light: lightPurple,
56+ }
57+ theme.AccentColor = lipgloss.AdaptiveColor{
58+ Dark: darkOrange,
59+ Light: lightOrange,
60+ }
61+
62+ // Status colors
63+ theme.ErrorColor = lipgloss.AdaptiveColor{
64+ Dark: darkRed,
65+ Light: lightRed,
66+ }
67+ theme.WarningColor = lipgloss.AdaptiveColor{
68+ Dark: darkOrange,
69+ Light: lightOrange,
70+ }
71+ theme.SuccessColor = lipgloss.AdaptiveColor{
72+ Dark: darkGreen,
73+ Light: lightGreen,
74+ }
75+ theme.InfoColor = lipgloss.AdaptiveColor{
76+ Dark: darkBlue,
77+ Light: lightBlue,
78+ }
79+
80+ // Text colors
81+ theme.TextColor = lipgloss.AdaptiveColor{
82+ Dark: darkForeground,
83+ Light: lightForeground,
84+ }
85+ theme.TextMutedColor = lipgloss.AdaptiveColor{
86+ Dark: darkComment,
87+ Light: lightComment,
88+ }
89+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
90+ Dark: darkYellow,
91+ Light: lightYellow,
92+ }
93+
94+ // Background colors
95+ theme.BackgroundColor = lipgloss.AdaptiveColor{
96+ Dark: darkBackground,
97+ Light: lightBackground,
98+ }
99+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
100+ Dark: darkCurrentLine,
101+ Light: lightCurrentLine,
102+ }
103+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
104+ Dark: "#191B29", // Darker background from palette
105+ Light: "#f0f0f5", // Slightly lighter than background
106+ }
107+
108+ // Border colors
109+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
110+ Dark: darkBorder,
111+ Light: lightBorder,
112+ }
113+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
114+ Dark: darkBlue,
115+ Light: lightBlue,
116+ }
117+ theme.BorderDimColor = lipgloss.AdaptiveColor{
118+ Dark: darkSelection,
119+ Light: lightSelection,
120+ }
121+
122+ // Diff view colors
123+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
124+ Dark: "#4fd6be", // teal from palette
125+ Light: "#1e725c",
126+ }
127+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
128+ Dark: "#c53b53", // red1 from palette
129+ Light: "#c53b53",
130+ }
131+ theme.DiffContextColor = lipgloss.AdaptiveColor{
132+ Dark: "#828bb8", // fg_dark from palette
133+ Light: "#7086b5",
134+ }
135+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
136+ Dark: "#828bb8", // fg_dark from palette
137+ Light: "#7086b5",
138+ }
139+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
140+ Dark: "#b8db87", // git.add from palette
141+ Light: "#4db380",
142+ }
143+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
144+ Dark: "#e26a75", // git.delete from palette
145+ Light: "#f52a65",
146+ }
147+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
148+ Dark: "#20303b",
149+ Light: "#d5e5d5",
150+ }
151+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
152+ Dark: "#37222c",
153+ Light: "#f7d8db",
154+ }
155+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
156+ Dark: darkBackground,
157+ Light: lightBackground,
158+ }
159+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
160+ Dark: "#545c7e", // dark3 from palette
161+ Light: "#848cb5",
162+ }
163+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
164+ Dark: "#1b2b34",
165+ Light: "#c5d5c5",
166+ }
167+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
168+ Dark: "#2d1f26",
169+ Light: "#e7c8cb",
170+ }
171+
172+ // Markdown colors
173+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
174+ Dark: darkForeground,
175+ Light: lightForeground,
176+ }
177+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
178+ Dark: darkPurple,
179+ Light: lightPurple,
180+ }
181+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
182+ Dark: darkBlue,
183+ Light: lightBlue,
184+ }
185+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
186+ Dark: darkCyan,
187+ Light: lightCyan,
188+ }
189+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
190+ Dark: darkGreen,
191+ Light: lightGreen,
192+ }
193+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
194+ Dark: darkYellow,
195+ Light: lightYellow,
196+ }
197+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
198+ Dark: darkYellow,
199+ Light: lightYellow,
200+ }
201+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
202+ Dark: darkOrange,
203+ Light: lightOrange,
204+ }
205+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
206+ Dark: darkComment,
207+ Light: lightComment,
208+ }
209+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
210+ Dark: darkBlue,
211+ Light: lightBlue,
212+ }
213+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
214+ Dark: darkCyan,
215+ Light: lightCyan,
216+ }
217+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
218+ Dark: darkBlue,
219+ Light: lightBlue,
220+ }
221+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
222+ Dark: darkCyan,
223+ Light: lightCyan,
224+ }
225+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
226+ Dark: darkForeground,
227+ Light: lightForeground,
228+ }
229+
230+ // Syntax highlighting colors
231+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
232+ Dark: darkComment,
233+ Light: lightComment,
234+ }
235+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
236+ Dark: darkPurple,
237+ Light: lightPurple,
238+ }
239+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
240+ Dark: darkBlue,
241+ Light: lightBlue,
242+ }
243+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
244+ Dark: darkRed,
245+ Light: lightRed,
246+ }
247+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
248+ Dark: darkGreen,
249+ Light: lightGreen,
250+ }
251+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
252+ Dark: darkOrange,
253+ Light: lightOrange,
254+ }
255+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
256+ Dark: darkYellow,
257+ Light: lightYellow,
258+ }
259+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
260+ Dark: darkCyan,
261+ Light: lightCyan,
262+ }
263+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
264+ Dark: darkForeground,
265+ Light: lightForeground,
266+ }
267+
268+ return theme
269+}
270+
271+func init() {
272+ // Register the Tokyo Night theme with the theme manager
273+ RegisterTheme("tokyonight", NewTokyoNightTheme())
274+}
internal/tui/theme/tron.gonew file+276
@@ -0,0 +1,276 @@
1+package theme
2+
3+import (
4+ "github.com/charmbracelet/lipgloss"
5+)
6+
7+// TronTheme implements the Theme interface with Tron-inspired colors.
8+// It provides both dark and light variants, though Tron is primarily a dark theme.
9+type TronTheme struct {
10+ BaseTheme
11+}
12+
13+// NewTronTheme creates a new instance of the Tron theme.
14+func NewTronTheme() *TronTheme {
15+ // Tron color palette
16+ // Inspired by the Tron movie's neon aesthetic
17+ darkBackground := "#0c141f"
18+ darkCurrentLine := "#1a2633"
19+ darkSelection := "#1a2633"
20+ darkForeground := "#caf0ff"
21+ darkComment := "#4d6b87"
22+ darkCyan := "#00d9ff"
23+ darkBlue := "#007fff"
24+ darkOrange := "#ff9000"
25+ darkPink := "#ff00a0"
26+ darkPurple := "#b73fff"
27+ darkRed := "#ff3333"
28+ darkYellow := "#ffcc00"
29+ darkGreen := "#00ff8f"
30+ darkBorder := "#1a2633"
31+
32+ // Light mode approximation
33+ lightBackground := "#f0f8ff"
34+ lightCurrentLine := "#e0f0ff"
35+ lightSelection := "#d0e8ff"
36+ lightForeground := "#0c141f"
37+ lightComment := "#4d6b87"
38+ lightCyan := "#0097b3"
39+ lightBlue := "#0066cc"
40+ lightOrange := "#cc7300"
41+ lightPink := "#cc0080"
42+ lightPurple := "#9932cc"
43+ lightRed := "#cc2929"
44+ lightYellow := "#cc9900"
45+ lightGreen := "#00cc72"
46+ lightBorder := "#d0e8ff"
47+
48+ theme := &TronTheme{}
49+
50+ // Base colors
51+ theme.PrimaryColor = lipgloss.AdaptiveColor{
52+ Dark: darkCyan,
53+ Light: lightCyan,
54+ }
55+ theme.SecondaryColor = lipgloss.AdaptiveColor{
56+ Dark: darkBlue,
57+ Light: lightBlue,
58+ }
59+ theme.AccentColor = lipgloss.AdaptiveColor{
60+ Dark: darkOrange,
61+ Light: lightOrange,
62+ }
63+
64+ // Status colors
65+ theme.ErrorColor = lipgloss.AdaptiveColor{
66+ Dark: darkRed,
67+ Light: lightRed,
68+ }
69+ theme.WarningColor = lipgloss.AdaptiveColor{
70+ Dark: darkOrange,
71+ Light: lightOrange,
72+ }
73+ theme.SuccessColor = lipgloss.AdaptiveColor{
74+ Dark: darkGreen,
75+ Light: lightGreen,
76+ }
77+ theme.InfoColor = lipgloss.AdaptiveColor{
78+ Dark: darkCyan,
79+ Light: lightCyan,
80+ }
81+
82+ // Text colors
83+ theme.TextColor = lipgloss.AdaptiveColor{
84+ Dark: darkForeground,
85+ Light: lightForeground,
86+ }
87+ theme.TextMutedColor = lipgloss.AdaptiveColor{
88+ Dark: darkComment,
89+ Light: lightComment,
90+ }
91+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
92+ Dark: darkYellow,
93+ Light: lightYellow,
94+ }
95+
96+ // Background colors
97+ theme.BackgroundColor = lipgloss.AdaptiveColor{
98+ Dark: darkBackground,
99+ Light: lightBackground,
100+ }
101+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
102+ Dark: darkCurrentLine,
103+ Light: lightCurrentLine,
104+ }
105+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
106+ Dark: "#070d14", // Slightly darker than background
107+ Light: "#ffffff", // Slightly lighter than background
108+ }
109+
110+ // Border colors
111+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
112+ Dark: darkBorder,
113+ Light: lightBorder,
114+ }
115+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
116+ Dark: darkCyan,
117+ Light: lightCyan,
118+ }
119+ theme.BorderDimColor = lipgloss.AdaptiveColor{
120+ Dark: darkSelection,
121+ Light: lightSelection,
122+ }
123+
124+ // Diff view colors
125+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
126+ Dark: darkGreen,
127+ Light: lightGreen,
128+ }
129+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
130+ Dark: darkRed,
131+ Light: lightRed,
132+ }
133+ theme.DiffContextColor = lipgloss.AdaptiveColor{
134+ Dark: darkComment,
135+ Light: lightComment,
136+ }
137+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
138+ Dark: darkBlue,
139+ Light: lightBlue,
140+ }
141+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
142+ Dark: "#00ff8f",
143+ Light: "#a5d6a7",
144+ }
145+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
146+ Dark: "#ff3333",
147+ Light: "#ef9a9a",
148+ }
149+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
150+ Dark: "#0a2a1a",
151+ Light: "#e8f5e9",
152+ }
153+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
154+ Dark: "#2a0a0a",
155+ Light: "#ffebee",
156+ }
157+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
158+ Dark: darkBackground,
159+ Light: lightBackground,
160+ }
161+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
162+ Dark: darkComment,
163+ Light: lightComment,
164+ }
165+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
166+ Dark: "#082015",
167+ Light: "#c8e6c9",
168+ }
169+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
170+ Dark: "#200808",
171+ Light: "#ffcdd2",
172+ }
173+
174+ // Markdown colors
175+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
176+ Dark: darkForeground,
177+ Light: lightForeground,
178+ }
179+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
180+ Dark: darkCyan,
181+ Light: lightCyan,
182+ }
183+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
184+ Dark: darkBlue,
185+ Light: lightBlue,
186+ }
187+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
188+ Dark: darkCyan,
189+ Light: lightCyan,
190+ }
191+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
192+ Dark: darkGreen,
193+ Light: lightGreen,
194+ }
195+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
196+ Dark: darkYellow,
197+ Light: lightYellow,
198+ }
199+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
200+ Dark: darkYellow,
201+ Light: lightYellow,
202+ }
203+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
204+ Dark: darkOrange,
205+ Light: lightOrange,
206+ }
207+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
208+ Dark: darkComment,
209+ Light: lightComment,
210+ }
211+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
212+ Dark: darkBlue,
213+ Light: lightBlue,
214+ }
215+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
216+ Dark: darkCyan,
217+ Light: lightCyan,
218+ }
219+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
220+ Dark: darkBlue,
221+ Light: lightBlue,
222+ }
223+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
224+ Dark: darkCyan,
225+ Light: lightCyan,
226+ }
227+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
228+ Dark: darkForeground,
229+ Light: lightForeground,
230+ }
231+
232+ // Syntax highlighting colors
233+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
234+ Dark: darkComment,
235+ Light: lightComment,
236+ }
237+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
238+ Dark: darkCyan,
239+ Light: lightCyan,
240+ }
241+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
242+ Dark: darkGreen,
243+ Light: lightGreen,
244+ }
245+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
246+ Dark: darkOrange,
247+ Light: lightOrange,
248+ }
249+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
250+ Dark: darkYellow,
251+ Light: lightYellow,
252+ }
253+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
254+ Dark: darkBlue,
255+ Light: lightBlue,
256+ }
257+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
258+ Dark: darkPurple,
259+ Light: lightPurple,
260+ }
261+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
262+ Dark: darkPink,
263+ Light: lightPink,
264+ }
265+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
266+ Dark: darkForeground,
267+ Light: lightForeground,
268+ }
269+
270+ return theme
271+}
272+
273+func init() {
274+ // Register the Tron theme with the theme manager
275+ RegisterTheme("tron", NewTronTheme())
276+}
internal/tui/tui.go+57−4
@@ -27,6 +27,7 @@ type keyMap struct {
2727 SwitchSession key.Binding
2828 Commands key.Binding
2929 Models key.Binding
30+ SwitchTheme key.Binding
3031}
3132
3233var keys = keyMap{
@@ -58,6 +59,11 @@ var keys = keyMap{
5859 key.WithKeys("ctrl+o"),
5960 key.WithHelp("ctrl+o", "model selection"),
6061 ),
62+
63+ SwitchTheme: key.NewBinding(
64+ key.WithKeys("ctrl+t"),
65+ key.WithHelp("ctrl+t", "switch theme"),
66+ ),
6167}
6268
6369var helpEsc = key.NewBinding(
@@ -105,6 +111,9 @@ type appModel struct {
105111
106112 showInitDialog bool
107113 initDialog dialog.InitDialogCmp
114+
115+ showThemeDialog bool
116+ themeDialog dialog.ThemeDialog
108117}
109118
110119func (a appModel) Init() tea.Cmd {
@@ -126,6 +135,8 @@ func (a appModel) Init() tea.Cmd {
126135 cmds = append(cmds, cmd)
127136 cmd = a.initDialog.Init()
128137 cmds = append(cmds, cmd)
138+ cmd = a.themeDialog.Init()
139+ cmds = append(cmds, cmd)
129140
130141 // Check if we should show the init dialog
131142 cmds = append(cmds, func() tea.Msg {
@@ -255,6 +266,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255266 a.showCommandDialog = false
256267 return a, nil
257268
269+ case dialog.CloseThemeDialogMsg:
270+ a.showThemeDialog = false
271+ return a, nil
272+
273+ case dialog.ThemeChangedMsg:
274+ a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
275+ a.showThemeDialog = false
276+ return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
277+
258278 case dialog.CloseModelDialogMsg:
259279 a.showModelDialog = false
260280 return a, nil
@@ -344,7 +364,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
344364 }
345365 return a, nil
346366 case key.Matches(msg, keys.Commands):
347− if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog {
367+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog {
348368 // Show commands dialog
349369 if len(a.commands) == 0 {
350370 return a, util.ReportWarn("No commands available")
@@ -359,12 +379,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
359379 a.showModelDialog = false
360380 return a, nil
361381 }
362−
363382 if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
364383 a.showModelDialog = true
365384 return a, nil
366385 }
367386 return a, nil
387+ case key.Matches(msg, keys.SwitchTheme):
388+ if !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
389+ // Show theme switcher dialog
390+ a.showThemeDialog = true
391+ // Theme list is dynamically loaded by the dialog component
392+ return a, a.themeDialog.Init()
393+ }
394+ return a, nil
368395 case key.Matches(msg, logsKeyReturnKey):
369396 if a.currentPage == page.LogsPage {
370397 return a, a.moveToPage(page.ChatPage)
@@ -465,6 +492,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
465492 }
466493 }
467494
495+ if a.showThemeDialog {
496+ d, themeCmd := a.themeDialog.Update(msg)
497+ a.themeDialog = d.(dialog.ThemeDialog)
498+ cmds = append(cmds, themeCmd)
499+ // Only block key messages send all other messages down
500+ if _, ok := msg.(tea.KeyMsg); ok {
501+ return a, tea.Batch(cmds...)
502+ }
503+ }
504+
468505 s, _ := a.status.Update(msg)
469506 a.status = s.(core.StatusCmp)
470507 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
@@ -523,9 +560,9 @@ func (a appModel) View() string {
523560 }
524561
525562 if !a.app.CoderAgent.IsBusy() {
526− a.status.SetHelpMsg("ctrl+? help")
563+ a.status.SetHelpWidgetMsg("ctrl+? help")
527564 } else {
528− a.status.SetHelpMsg("? help")
565+ a.status.SetHelpWidgetMsg("? help")
529566 }
530567
531568 if a.showHelp {
@@ -629,6 +666,21 @@ func (a appModel) View() string {
629666 )
630667 }
631668
669+ if a.showThemeDialog {
670+ overlay := a.themeDialog.View()
671+ row := lipgloss.Height(appView) / 2
672+ row -= lipgloss.Height(overlay) / 2
673+ col := lipgloss.Width(appView) / 2
674+ col -= lipgloss.Width(overlay) / 2
675+ appView = layout.PlaceOverlay(
676+ col,
677+ row,
678+ overlay,
679+ appView,
680+ true,
681+ )
682+ }
683+
632684 return appView
633685}
634686
@@ -645,6 +697,7 @@ func New(app *app.App) tea.Model {
645697 modelDialog: dialog.NewModelDialogCmp(),
646698 permissions: dialog.NewPermissionDialogCmp(),
647699 initDialog: dialog.NewInitDialogCmp(),
700+ themeDialog: dialog.NewThemeDialogCmp(),
648701 app: app,
649702 commands: []dialog.Command{},
650703 pages: map[page.PageID]tea.Model{
opencode-schema.json+143−104
@@ -12,75 +12,79 @@
1212 "model": {
1313 "description": "Model ID for the agent",
1414 "enum": [
15+ "gpt-4o-mini",
16+ "o1-pro",
17+ "azure.gpt-4o-mini",
18+ "openrouter.gpt-4.1-mini",
19+ "openrouter.o1-mini",
1520 "bedrock.claude-3.7-sonnet",
16− "claude-3-haiku",
17− "claude-3.7-sonnet",
18− "claude-3.5-haiku",
19− "o3",
20− "azure.o3",
21+ "meta-llama/llama-4-scout-17b-16e-instruct",
22+ "openrouter.gpt-4o-mini",
23+ "gemini-2.0-flash",
24+ "deepseek-r1-distill-llama-70b",
25+ "openrouter.claude-3.7-sonnet",
26+ "openrouter.gpt-4.5-preview",
27+ "azure.o3-mini",
28+ "openrouter.claude-3.5-haiku",
29+ "azure.o1-mini",
30+ "openrouter.o1",
31+ "openrouter.gemini-2.5",
32+ "llama-3.3-70b-versatile",
2133 "gpt-4.5-preview",
22− "azure.gpt-4.5-preview",
23− "o1-pro",
34+ "openrouter.claude-3-opus",
35+ "openrouter.claude-3.5-sonnet",
2436 "o4-mini",
25− "azure.o4-mini",
37+ "gemini-2.0-flash-lite",
38+ "azure.gpt-4.5-preview",
39+ "openrouter.gpt-4o",
40+ "o1",
41+ "azure.gpt-4o",
42+ "openrouter.gpt-4.1-nano",
43+ "o3",
2644 "gpt-4.1",
27− "azure.gpt-4.1",
45+ "azure.o1",
46+ "claude-3-haiku",
47+ "claude-3-opus",
48+ "gpt-4.1-mini",
49+ "openrouter.o4-mini",
50+ "openrouter.gemini-2.5-flash",
51+ "claude-3.5-haiku",
2852 "o3-mini",
29− "azure.o3-mini",
53+ "azure.o3",
54+ "gpt-4o",
55+ "azure.gpt-4.1",
56+ "openrouter.claude-3-haiku",
3057 "gpt-4.1-nano",
3158 "azure.gpt-4.1-nano",
32− "gpt-4o-mini",
33− "azure.gpt-4o-mini",
34− "o1",
35− "azure.o1",
36− "gemini-2.5-flash",
59+ "claude-3.7-sonnet",
60+ "gemini-2.5",
61+ "azure.o4-mini",
62+ "o1-mini",
3763 "qwen-qwq",
3864 "meta-llama/llama-4-maverick-17b-128e-instruct",
39− "claude-3-opus",
40− "gpt-4o",
41− "azure.gpt-4o",
42− "gemini-2.0-flash-lite",
43− "gemini-2.0-flash",
44− "deepseek-r1-distill-llama-70b",
45− "llama-3.3-70b-versatile",
46− "claude-3.5-sonnet",
47− "o1-mini",
48− "azure.o1-mini",
49− "gpt-4.1-mini",
50− "azure.gpt-4.1-mini",
51− "gemini-2.5",
52− "meta-llama/llama-4-scout-17b-16e-instruct",
53− "openrouter.deepseek-chat-free",
54− "openrouter.deepseek-r1-free",
5565 "openrouter.gpt-4.1",
56− "openrouter.gpt-4.1-mini",
57− "openrouter.gpt-4.1-nano",
58− "openrouter.gpt-4.5-preview",
59− "openrouter.gpt-4o",
60− "openrouter.gpt-4o-mini",
61− "openrouter.o1",
6266 "openrouter.o1-pro",
63− "openrouter.o1-mini",
6467 "openrouter.o3",
65− "openrouter.o3-mini",
66− "openrouter.o4-mini",
67− "openrouter.gemini-2.5-flash",
68− "openrouter.gemini-2.5",
69− "openrouter.claude-3.5-sonnet",
70− "openrouter.claude-3-haiku",
71− "openrouter.claude-3.7-sonnet",
72− "openrouter.claude-3.5-haiku",
73− "openrouter.claude-3-opus"
68+ "claude-3.5-sonnet",
69+ "gemini-2.5-flash",
70+ "azure.gpt-4.1-mini",
71+ "openrouter.o3-mini"
7472 ],
7573 "type": "string"
7674 },
7775 "reasoningEffort": {
7876 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
79− "enum": ["low", "medium", "high"],
77+ "enum": [
78+ "low",
79+ "medium",
80+ "high"
81+ ],
8082 "type": "string"
8183 }
8284 },
83− "required": ["model"],
85+ "required": [
86+ "model"
87+ ],
8488 "type": "object"
8589 }
8690 },
@@ -98,75 +102,79 @@
98102 "model": {
99103 "description": "Model ID for the agent",
100104 "enum": [
105+ "gpt-4o-mini",
106+ "o1-pro",
107+ "azure.gpt-4o-mini",
108+ "openrouter.gpt-4.1-mini",
109+ "openrouter.o1-mini",
101110 "bedrock.claude-3.7-sonnet",
102− "claude-3-haiku",
103− "claude-3.7-sonnet",
104− "claude-3.5-haiku",
105− "o3",
106− "azure.o3",
111+ "meta-llama/llama-4-scout-17b-16e-instruct",
112+ "openrouter.gpt-4o-mini",
113+ "gemini-2.0-flash",
114+ "deepseek-r1-distill-llama-70b",
115+ "openrouter.claude-3.7-sonnet",
116+ "openrouter.gpt-4.5-preview",
117+ "azure.o3-mini",
118+ "openrouter.claude-3.5-haiku",
119+ "azure.o1-mini",
120+ "openrouter.o1",
121+ "openrouter.gemini-2.5",
122+ "llama-3.3-70b-versatile",
107123 "gpt-4.5-preview",
108− "azure.gpt-4.5-preview",
109− "o1-pro",
124+ "openrouter.claude-3-opus",
125+ "openrouter.claude-3.5-sonnet",
110126 "o4-mini",
111− "azure.o4-mini",
127+ "gemini-2.0-flash-lite",
128+ "azure.gpt-4.5-preview",
129+ "openrouter.gpt-4o",
130+ "o1",
131+ "azure.gpt-4o",
132+ "openrouter.gpt-4.1-nano",
133+ "o3",
112134 "gpt-4.1",
113− "azure.gpt-4.1",
135+ "azure.o1",
136+ "claude-3-haiku",
137+ "claude-3-opus",
138+ "gpt-4.1-mini",
139+ "openrouter.o4-mini",
140+ "openrouter.gemini-2.5-flash",
141+ "claude-3.5-haiku",
114142 "o3-mini",
115− "azure.o3-mini",
143+ "azure.o3",
144+ "gpt-4o",
145+ "azure.gpt-4.1",
146+ "openrouter.claude-3-haiku",
116147 "gpt-4.1-nano",
117148 "azure.gpt-4.1-nano",
118− "gpt-4o-mini",
119− "azure.gpt-4o-mini",
120− "o1",
121− "azure.o1",
122− "gemini-2.5-flash",
149+ "claude-3.7-sonnet",
150+ "gemini-2.5",
151+ "azure.o4-mini",
152+ "o1-mini",
123153 "qwen-qwq",
124154 "meta-llama/llama-4-maverick-17b-128e-instruct",
125− "claude-3-opus",
126− "gpt-4o",
127− "azure.gpt-4o",
128− "gemini-2.0-flash-lite",
129− "gemini-2.0-flash",
130− "deepseek-r1-distill-llama-70b",
131− "llama-3.3-70b-versatile",
132− "claude-3.5-sonnet",
133− "o1-mini",
134− "azure.o1-mini",
135− "gpt-4.1-mini",
136− "azure.gpt-4.1-mini",
137− "gemini-2.5",
138− "meta-llama/llama-4-scout-17b-16e-instruct",
139− "openrouter.deepseek-chat-free",
140− "openrouter.deepseek-r1-free",
141155 "openrouter.gpt-4.1",
142− "openrouter.gpt-4.1-mini",
143− "openrouter.gpt-4.1-nano",
144− "openrouter.gpt-4.5-preview",
145− "openrouter.gpt-4o",
146− "openrouter.gpt-4o-mini",
147− "openrouter.o1",
148156 "openrouter.o1-pro",
149− "openrouter.o1-mini",
150157 "openrouter.o3",
151− "openrouter.o3-mini",
152− "openrouter.o4-mini",
153− "openrouter.gemini-2.5-flash",
154− "openrouter.gemini-2.5",
155− "openrouter.claude-3.5-sonnet",
156− "openrouter.claude-3-haiku",
157− "openrouter.claude-3.7-sonnet",
158− "openrouter.claude-3.5-haiku",
159− "openrouter.claude-3-opus"
158+ "claude-3.5-sonnet",
159+ "gemini-2.5-flash",
160+ "azure.gpt-4.1-mini",
161+ "openrouter.o3-mini"
160162 ],
161163 "type": "string"
162164 },
163165 "reasoningEffort": {
164166 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
165− "enum": ["low", "medium", "high"],
167+ "enum": [
168+ "low",
169+ "medium",
170+ "high"
171+ ],
166172 "type": "string"
167173 }
168174 },
169− "required": ["model"],
175+ "required": [
176+ "model"
177+ ],
170178 "type": "object"
171179 },
172180 "description": "Agent configurations",
@@ -212,7 +220,9 @@
212220 "type": "string"
213221 }
214222 },
215− "required": ["directory"],
223+ "required": [
224+ "directory"
225+ ],
216226 "type": "object"
217227 },
218228 "debug": {
@@ -250,7 +260,9 @@
250260 "type": "object"
251261 }
252262 },
253− "required": ["command"],
263+ "required": [
264+ "command"
265+ ],
254266 "type": "object"
255267 },
256268 "description": "Language Server Protocol configurations",
@@ -288,7 +300,10 @@
288300 "type": {
289301 "default": "stdio",
290302 "description": "Type of MCP server",
291− "enum": ["stdio", "sse"],
303+ "enum": [
304+ "stdio",
305+ "sse"
306+ ],
292307 "type": "string"
293308 },
294309 "url": {
@@ -296,7 +311,9 @@
296311 "type": "string"
297312 }
298313 },
299− "required": ["command"],
314+ "required": [
315+ "command"
316+ ],
300317 "type": "object"
301318 },
302319 "description": "Model Control Protocol server configurations",
@@ -322,9 +339,9 @@
322339 "openai",
323340 "gemini",
324341 "groq",
342+ "openrouter",
325343 "bedrock",
326− "azure",
327− "openrouter"
344+ "azure"
328345 ],
329346 "type": "string"
330347 }
@@ -334,6 +351,28 @@
334351 "description": "LLM provider configurations",
335352 "type": "object"
336353 },
354+ "tui": {
355+ "description": "Terminal User Interface configuration",
356+ "properties": {
357+ "theme": {
358+ "default": "opencode",
359+ "description": "TUI theme name",
360+ "enum": [
361+ "opencode",
362+ "catppuccin",
363+ "dracula",
364+ "flexoki",
365+ "gruvbox",
366+ "monokai",
367+ "onedark",
368+ "tokyonight",
369+ "tron"
370+ ],
371+ "type": "string"
372+ }
373+ },
374+ "type": "object"
375+ },
337376 "wd": {
338377 "description": "Working directory for the application",
339378 "type": "string"