on main help sign in
feat: themes (#113)
82de143Adam
cmd/schema/main.go+23
@@ -98,6 +98,29 @@ func generateSchema() map[string]any {
98 },
98 },
99 }
99 }
100
100
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+
101 // Add MCP servers
124 // Add MCP servers
102 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
125 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
103 "type": "object",
126 "type": "object",
internal/app/app.go+19
@@ -16,6 +16,7 @@ import (
16 "github.com/opencode-ai/opencode/internal/message"
16 "github.com/opencode-ai/opencode/internal/message"
17 "github.com/opencode-ai/opencode/internal/permission"
17 "github.com/opencode-ai/opencode/internal/permission"
18 "github.com/opencode-ai/opencode/internal/session"
18 "github.com/opencode-ai/opencode/internal/session"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
19)
20)
20
21
21type App struct {
22type App struct {
@@ -49,6 +50,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
49 LSPClients: make(map[string]*lsp.Client),
50 LSPClients: make(map[string]*lsp.Client),
50 }
51 }
51
52
53+ // Initialize theme based on configuration
54+ app.initTheme()
55+
52 // Initialize LSP clients in the background
56 // Initialize LSP clients in the background
53 go app.initLSPClients(ctx)
57 go app.initLSPClients(ctx)
54
58
@@ -73,6 +77,21 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
73 return app, nil
77 return app, nil
74}
78}
75
79
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+}
76
95
77// Shutdown performs a clean shutdown of the application
96// Shutdown performs a clean shutdown of the application
78func (app *App) Shutdown() {
97func (app *App) Shutdown() {
internal/config/config.go+68
@@ -2,9 +2,11 @@
2package config
2package config
3
3
4import (
4import (
5+ "encoding/json"
5 "fmt"
6 "fmt"
6 "log/slog"
7 "log/slog"
7 "os"
8 "os"
9+ "path/filepath"
8 "strings"
10 "strings"
9
11
10 "github.com/opencode-ai/opencode/internal/llm/models"
12 "github.com/opencode-ai/opencode/internal/llm/models"
@@ -65,6 +67,11 @@ type LSPConfig struct {
65 Options any `json:"options"`
67 Options any `json:"options"`
66}
68}
67
69
70+// TUIConfig defines the configuration for the Terminal User Interface.
71+type TUIConfig struct {
72+ Theme string `json:"theme,omitempty"`
73+}
74+
68// Config is the main configuration structure for the application.
75// Config is the main configuration structure for the application.
69type Config struct {
76type Config struct {
70 Data Data `json:"data"`
77 Data Data `json:"data"`
@@ -76,6 +83,7 @@ type Config struct {
76 Debug bool `json:"debug,omitempty"`
83 Debug bool `json:"debug,omitempty"`
77 DebugLSP bool `json:"debugLSP,omitempty"`
84 DebugLSP bool `json:"debugLSP,omitempty"`
78 ContextPaths []string `json:"contextPaths,omitempty"`
85 ContextPaths []string `json:"contextPaths,omitempty"`
86+ TUI TUIConfig `json:"tui"`
79}
87}
80
88
81// Application constants
89// Application constants
@@ -203,6 +211,7 @@ func configureViper() {
203func setDefaults(debug bool) {
211func setDefaults(debug bool) {
204 viper.SetDefault("data.directory", defaultDataDirectory)
212 viper.SetDefault("data.directory", defaultDataDirectory)
205 viper.SetDefault("contextPaths", defaultContextPaths)
213 viper.SetDefault("contextPaths", defaultContextPaths)
214+ viper.SetDefault("tui.theme", "opencode")
206
215
207 if debug {
216 if debug {
208 viper.SetDefault("debug", true)
217 viper.SetDefault("debug", true)
@@ -714,3 +723,62 @@ func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
714
723
715 return nil
724 return nil
716}
725}
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 (
16 "github.com/charmbracelet/lipgloss"
16 "github.com/charmbracelet/lipgloss"
17 "github.com/charmbracelet/x/ansi"
17 "github.com/charmbracelet/x/ansi"
18 "github.com/opencode-ai/opencode/internal/config"
18 "github.com/opencode-ai/opencode/internal/config"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
19 "github.com/sergi/go-diff/diffmatchpatch"
20 "github.com/sergi/go-diff/diffmatchpatch"
20)
21)
21
22
@@ -68,143 +69,6 @@ type linePair struct {
68 right *DiffLine
69 right *DiffLine
69}
70}
70
71
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−
208// -------------------------------------------------------------------------
72// -------------------------------------------------------------------------
209// Parse Configuration
73// Parse Configuration
210// -------------------------------------------------------------------------
74// -------------------------------------------------------------------------
@@ -233,7 +97,6 @@ func WithContextSize(size int) ParseOption {
233// SideBySideConfig configures the rendering of side-by-side diffs
97// SideBySideConfig configures the rendering of side-by-side diffs
234type SideBySideConfig struct {
98type SideBySideConfig struct {
235 TotalWidth int
99 TotalWidth int
236− Style StyleConfig
237}
100}
238
101
239// SideBySideOption modifies a SideBySideConfig
102// SideBySideOption modifies a SideBySideConfig
@@ -243,7 +106,6 @@ type SideBySideOption func(*SideBySideConfig)
243func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
106func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
244 config := SideBySideConfig{
107 config := SideBySideConfig{
245 TotalWidth: 160, // Default width for side-by-side view
108 TotalWidth: 160, // Default width for side-by-side view
246− Style: NewStyleConfig(),
247 }
109 }
248
110
249 for _, opt := range opts {
111 for _, opt := range opts {
@@ -262,20 +124,6 @@ func WithTotalWidth(width int) SideBySideOption {
262 }
124 }
263}
125}
264
126
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−
279// -------------------------------------------------------------------------
127// -------------------------------------------------------------------------
280// Diff Parsing
128// Diff Parsing
281// -------------------------------------------------------------------------
129// -------------------------------------------------------------------------
@@ -382,7 +230,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
382}
230}
383
231
384// HighlightIntralineChanges updates lines in a hunk to show character-level differences
232// HighlightIntralineChanges updates lines in a hunk to show character-level differences
385−func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
233+func HighlightIntralineChanges(h *Hunk) {
386 var updated []DiffLine
234 var updated []DiffLine
387 dmp := diffmatchpatch.New()
235 dmp := diffmatchpatch.New()
388
236
@@ -476,6 +324,8 @@ func pairLines(lines []DiffLine) []linePair {
476
324
477// SyntaxHighlight applies syntax highlighting to text based on file extension
325// SyntaxHighlight applies syntax highlighting to text based on file extension
478func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
326func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
327+ t := theme.CurrentTheme()
328+
479 // Determine the language lexer to use
329 // Determine the language lexer to use
480 l := lexers.Match(fileName)
330 l := lexers.Match(fileName)
481 if l == nil {
331 if l == nil {
@@ -491,93 +341,175 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
491 if f == nil {
341 if f == nil {
492 f = formatters.Fallback
342 f = formatters.Fallback
493 }
343 }
494− theme := `
344+
345+ // Dynamic theme based on current theme values
346+ syntaxThemeXml := fmt.Sprintf(`
495− <style name="vscode-dark-plus">
347+ <style name="opencode-theme">
496 <!-- Base colors -->
348 <!-- Base colors -->
497− <entry type="Background" style="bg:#1E1E1E"/>
349+ <entry type="Background" style="bg:%s"/>
498− <entry type="Text" style="#D4D4D4"/>
350+ <entry type="Text" style="%s"/>
499− <entry type="Other" style="#D4D4D4"/>
351+ <entry type="Other" style="%s"/>
500− <entry type="Error" style="#F44747"/>
352+ <entry type="Error" style="%s"/>
501− <!-- Keywords - using the Control flow / Special keywords color -->
353+ <!-- Keywords -->
502− <entry type="Keyword" style="#C586C0"/>
354+ <entry type="Keyword" style="%s"/>
503− <entry type="KeywordConstant" style="#4FC1FF"/>
355+ <entry type="KeywordConstant" style="%s"/>
504− <entry type="KeywordDeclaration" style="#C586C0"/>
356+ <entry type="KeywordDeclaration" style="%s"/>
505− <entry type="KeywordNamespace" style="#C586C0"/>
357+ <entry type="KeywordNamespace" style="%s"/>
506− <entry type="KeywordPseudo" style="#C586C0"/>
358+ <entry type="KeywordPseudo" style="%s"/>
507− <entry type="KeywordReserved" style="#C586C0"/>
359+ <entry type="KeywordReserved" style="%s"/>
508− <entry type="KeywordType" style="#4EC9B0"/>
360+ <entry type="KeywordType" style="%s"/>
509 <!-- Names -->
361 <!-- Names -->
510− <entry type="Name" style="#D4D4D4"/>
362+ <entry type="Name" style="%s"/>
511− <entry type="NameAttribute" style="#9CDCFE"/>
363+ <entry type="NameAttribute" style="%s"/>
512− <entry type="NameBuiltin" style="#4EC9B0"/>
364+ <entry type="NameBuiltin" style="%s"/>
513− <entry type="NameBuiltinPseudo" style="#9CDCFE"/>
365+ <entry type="NameBuiltinPseudo" style="%s"/>
514− <entry type="NameClass" style="#4EC9B0"/>
366+ <entry type="NameClass" style="%s"/>
515− <entry type="NameConstant" style="#4FC1FF"/>
367+ <entry type="NameConstant" style="%s"/>
516− <entry type="NameDecorator" style="#DCDCAA"/>
368+ <entry type="NameDecorator" style="%s"/>
517− <entry type="NameEntity" style="#9CDCFE"/>
369+ <entry type="NameEntity" style="%s"/>
518− <entry type="NameException" style="#4EC9B0"/>
370+ <entry type="NameException" style="%s"/>
519− <entry type="NameFunction" style="#DCDCAA"/>
371+ <entry type="NameFunction" style="%s"/>
520− <entry type="NameLabel" style="#C8C8C8"/>
372+ <entry type="NameLabel" style="%s"/>
521− <entry type="NameNamespace" style="#4EC9B0"/>
373+ <entry type="NameNamespace" style="%s"/>
522− <entry type="NameOther" style="#9CDCFE"/>
374+ <entry type="NameOther" style="%s"/>
523− <entry type="NameTag" style="#569CD6"/>
375+ <entry type="NameTag" style="%s"/>
524− <entry type="NameVariable" style="#9CDCFE"/>
376+ <entry type="NameVariable" style="%s"/>
525− <entry type="NameVariableClass" style="#9CDCFE"/>
377+ <entry type="NameVariableClass" style="%s"/>
526− <entry type="NameVariableGlobal" style="#9CDCFE"/>
378+ <entry type="NameVariableGlobal" style="%s"/>
527− <entry type="NameVariableInstance" style="#9CDCFE"/>
379+ <entry type="NameVariableInstance" style="%s"/>
528 <!-- Literals -->
380 <!-- Literals -->
529− <entry type="Literal" style="#CE9178"/>
381+ <entry type="Literal" style="%s"/>
530− <entry type="LiteralDate" style="#CE9178"/>
382+ <entry type="LiteralDate" style="%s"/>
531− <entry type="LiteralString" style="#CE9178"/>
383+ <entry type="LiteralString" style="%s"/>
532− <entry type="LiteralStringBacktick" style="#CE9178"/>
384+ <entry type="LiteralStringBacktick" style="%s"/>
533− <entry type="LiteralStringChar" style="#CE9178"/>
385+ <entry type="LiteralStringChar" style="%s"/>
534− <entry type="LiteralStringDoc" style="#CE9178"/>
386+ <entry type="LiteralStringDoc" style="%s"/>
535− <entry type="LiteralStringDouble" style="#CE9178"/>
387+ <entry type="LiteralStringDouble" style="%s"/>
536− <entry type="LiteralStringEscape" style="#d7ba7d"/>
388+ <entry type="LiteralStringEscape" style="%s"/>
537− <entry type="LiteralStringHeredoc" style="#CE9178"/>
389+ <entry type="LiteralStringHeredoc" style="%s"/>
538− <entry type="LiteralStringInterpol" style="#CE9178"/>
390+ <entry type="LiteralStringInterpol" style="%s"/>
539− <entry type="LiteralStringOther" style="#CE9178"/>
391+ <entry type="LiteralStringOther" style="%s"/>
540− <entry type="LiteralStringRegex" style="#d16969"/>
392+ <entry type="LiteralStringRegex" style="%s"/>
541− <entry type="LiteralStringSingle" style="#CE9178"/>
393+ <entry type="LiteralStringSingle" style="%s"/>
542− <entry type="LiteralStringSymbol" style="#CE9178"/>
394+ <entry type="LiteralStringSymbol" style="%s"/>
543− <!-- Numbers - using the numberLiteral color -->
395+ <!-- Numbers -->
544− <entry type="LiteralNumber" style="#b5cea8"/>
396+ <entry type="LiteralNumber" style="%s"/>
545− <entry type="LiteralNumberBin" style="#b5cea8"/>
397+ <entry type="LiteralNumberBin" style="%s"/>
546− <entry type="LiteralNumberFloat" style="#b5cea8"/>
398+ <entry type="LiteralNumberFloat" style="%s"/>
547− <entry type="LiteralNumberHex" style="#b5cea8"/>
399+ <entry type="LiteralNumberHex" style="%s"/>
548− <entry type="LiteralNumberInteger" style="#b5cea8"/>
400+ <entry type="LiteralNumberInteger" style="%s"/>
549− <entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
401+ <entry type="LiteralNumberIntegerLong" style="%s"/>
550− <entry type="LiteralNumberOct" style="#b5cea8"/>
402+ <entry type="LiteralNumberOct" style="%s"/>
551 <!-- Operators -->
403 <!-- Operators -->
552− <entry type="Operator" style="#D4D4D4"/>
404+ <entry type="Operator" style="%s"/>
553− <entry type="OperatorWord" style="#C586C0"/>
405+ <entry type="OperatorWord" style="%s"/>
554− <entry type="Punctuation" style="#D4D4D4"/>
406+ <entry type="Punctuation" style="%s"/>
555− <!-- Comments - standard VSCode Dark+ comment color -->
407+ <!-- Comments -->
556− <entry type="Comment" style="#6A9955"/>
408+ <entry type="Comment" style="%s"/>
557− <entry type="CommentHashbang" style="#6A9955"/>
409+ <entry type="CommentHashbang" style="%s"/>
558− <entry type="CommentMultiline" style="#6A9955"/>
410+ <entry type="CommentMultiline" style="%s"/>
559− <entry type="CommentSingle" style="#6A9955"/>
411+ <entry type="CommentSingle" style="%s"/>
560− <entry type="CommentSpecial" style="#6A9955"/>
412+ <entry type="CommentSpecial" style="%s"/>
561− <entry type="CommentPreproc" style="#C586C0"/>
413+ <entry type="CommentPreproc" style="%s"/>
562 <!-- Generic styles -->
414 <!-- Generic styles -->
563− <entry type="Generic" style="#D4D4D4"/>
415+ <entry type="Generic" style="%s"/>
564− <entry type="GenericDeleted" style="#F44747"/>
416+ <entry type="GenericDeleted" style="%s"/>
565− <entry type="GenericEmph" style="italic #D4D4D4"/>
417+ <entry type="GenericEmph" style="italic %s"/>
566− <entry type="GenericError" style="#F44747"/>
418+ <entry type="GenericError" style="%s"/>
567− <entry type="GenericHeading" style="bold #D4D4D4"/>
419+ <entry type="GenericHeading" style="bold %s"/>
568− <entry type="GenericInserted" style="#b5cea8"/>
420+ <entry type="GenericInserted" style="%s"/>
569− <entry type="GenericOutput" style="#808080"/>
421+ <entry type="GenericOutput" style="%s"/>
570− <entry type="GenericPrompt" style="#D4D4D4"/>
422+ <entry type="GenericPrompt" style="%s"/>
571− <entry type="GenericStrong" style="bold #D4D4D4"/>
423+ <entry type="GenericStrong" style="bold %s"/>
572− <entry type="GenericSubheading" style="bold #D4D4D4"/>
424+ <entry type="GenericSubheading" style="bold %s"/>
573− <entry type="GenericTraceback" style="#F44747"/>
425+ <entry type="GenericTraceback" style="%s"/>
574 <entry type="GenericUnderline" style="underline"/>
426 <entry type="GenericUnderline" style="underline"/>
575− <entry type="TextWhitespace" style="#D4D4D4"/>
427+ <entry type="TextWhitespace" style="%s"/>
576</style>
428</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+ )
578
509
579− r := strings.NewReader(theme)
510+ r := strings.NewReader(syntaxThemeXml)
580 style := chroma.MustNewXMLStyle(r)
511 style := chroma.MustNewXMLStyle(r)
512+
581 // Modify the style to use the provided background
513 // Modify the style to use the provided background
582 s, err := style.Builder().Transform(
514 s, err := style.Builder().Transform(
583 func(t chroma.StyleEntry) chroma.StyleEntry {
515 func(t chroma.StyleEntry) chroma.StyleEntry {
@@ -599,6 +531,14 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
599 return f.Format(w, s, it)
531 return f.Format(w, s, it)
600}
532}
601
533
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+
602// highlightLine applies syntax highlighting to a single line
542// highlightLine applies syntax highlighting to a single line
603func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
543func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
604 var buf bytes.Buffer
544 var buf bytes.Buffer
@@ -610,11 +550,11 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
610}
550}
611
551
612// createStyles generates the lipgloss styles needed for rendering diffs
552// createStyles generates the lipgloss styles needed for rendering diffs
613−func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
553+func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
614− removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
554+ removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
615− addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
555+ addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
616− contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
556+ contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
617− lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
557+ lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
618
558
619 return
559 return
620}
560}
@@ -623,9 +563,20 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
623// Rendering Functions
563// Rendering Functions
624// -------------------------------------------------------------------------
564// -------------------------------------------------------------------------
625
565
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+
626// applyHighlighting applies intra-line highlighting to a piece of text
578// applyHighlighting applies intra-line highlighting to a piece of text
627−func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
579+func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
628−) string {
629 // Find all ANSI sequences in the content
580 // Find all ANSI sequences in the content
630 ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
581 ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
631 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
582 ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
@@ -663,6 +614,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
663 inSelection := false
614 inSelection := false
664 currentPos := 0
615 currentPos := 0
665
616
617+ // Get the appropriate color based on terminal background
618+ bgColor := lipgloss.Color(getColor(highlightBg))
619+ fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
620+
666 for i := 0; i < len(content); {
621 for i := 0; i < len(content); {
667 // Check if we're at an ANSI sequence
622 // Check if we're at an ANSI sequence
668 isAnsi := false
623 isAnsi := false
@@ -697,12 +652,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
697 // Get the current styling
652 // Get the current styling
698 currentStyle := ansiSequences[currentPos]
653 currentStyle := ansiSequences[currentPos]
699
654
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))
701 sb.WriteString("\x1b[48;2;")
659 sb.WriteString("\x1b[48;2;")
702− r, g, b, _ := highlightBg.RGBA()
660+ r, g, b, _ = bgColor.RGBA()
703 sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
661 sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
704 sb.WriteString(char)
662 sb.WriteString(char)
663+ // Reset foreground and background
705− sb.WriteString("\x1b[49m") // Reset only background
664+ sb.WriteString("\x1b[39m")
706
665
707 // Reapply the original ANSI sequence
666 // Reapply the original ANSI sequence
708 sb.WriteString(currentStyle)
667 sb.WriteString(currentStyle)
@@ -719,22 +678,24 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
719}
678}
720
679
721// renderLeftColumn formats the left side of a side-by-side diff
680// 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+
723 if dl == nil {
684 if dl == nil {
724− contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
685+ contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
725 return contextLineStyle.Width(colWidth).Render("")
686 return contextLineStyle.Width(colWidth).Render("")
726 }
687 }
727
688
728− removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
689+ removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t)
729
690
730 // Determine line style based on line type
691 // Determine line style based on line type
731 var marker string
692 var marker string
732 var bgStyle lipgloss.Style
693 var bgStyle lipgloss.Style
733 switch dl.Kind {
694 switch dl.Kind {
734 case LineRemoved:
695 case LineRemoved:
735− marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
696+ marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-")
736 bgStyle = removedLineStyle
697 bgStyle = removedLineStyle
737− lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
698+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
738 case LineAdded:
699 case LineAdded:
739 marker = "?"
700 marker = "?"
740 bgStyle = contextLineStyle
701 bgStyle = contextLineStyle
@@ -757,7 +718,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
757
718
758 // Apply intra-line highlighting for removed lines
719 // Apply intra-line highlighting for removed lines
759 if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
720 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())
761 }
722 }
762
723
763 // Add a padding space for removed lines
724 // Add a padding space for removed lines
@@ -771,28 +732,30 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
771 ansi.Truncate(
732 ansi.Truncate(
772 lineText,
733 lineText,
773 colWidth,
734 colWidth,
774− lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
735+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
775 ),
736 ),
776 )
737 )
777}
738}
778
739
779// renderRightColumn formats the right side of a side-by-side diff
740// 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+
781 if dl == nil {
744 if dl == nil {
782− contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
745+ contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
783 return contextLineStyle.Width(colWidth).Render("")
746 return contextLineStyle.Width(colWidth).Render("")
784 }
747 }
785
748
786− _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
749+ _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
787
750
788 // Determine line style based on line type
751 // Determine line style based on line type
789 var marker string
752 var marker string
790 var bgStyle lipgloss.Style
753 var bgStyle lipgloss.Style
791 switch dl.Kind {
754 switch dl.Kind {
792 case LineAdded:
755 case LineAdded:
793− marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
756+ marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+")
794 bgStyle = addedLineStyle
757 bgStyle = addedLineStyle
795− lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
758+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
796 case LineRemoved:
759 case LineRemoved:
797 marker = "?"
760 marker = "?"
798 bgStyle = contextLineStyle
761 bgStyle = contextLineStyle
@@ -815,7 +778,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
815
778
816 // Apply intra-line highlighting for added lines
779 // Apply intra-line highlighting for added lines
817 if dl.Kind == LineAdded && len(dl.Segments) > 0 {
780 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())
819 }
782 }
820
783
821 // Add a padding space for added lines
784 // Add a padding space for added lines
@@ -829,7 +792,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
829 ansi.Truncate(
792 ansi.Truncate(
830 lineText,
793 lineText,
831 colWidth,
794 colWidth,
832− lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
795+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
833 ),
796 ),
834 )
797 )
835}
798}
@@ -848,7 +811,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
848 copy(hunkCopy.Lines, h.Lines)
811 copy(hunkCopy.Lines, h.Lines)
849
812
850 // Highlight changes within lines
813 // Highlight changes within lines
851− HighlightIntralineChanges(&hunkCopy, config.Style)
814+ HighlightIntralineChanges(&hunkCopy)
852
815
853 // Pair lines for side-by-side display
816 // Pair lines for side-by-side display
854 pairs := pairLines(hunkCopy.Lines)
817 pairs := pairLines(hunkCopy.Lines)
@@ -860,8 +823,8 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
860 rightWidth := config.TotalWidth - colWidth
823 rightWidth := config.TotalWidth - colWidth
861 var sb strings.Builder
824 var sb strings.Builder
862 for _, p := range pairs {
825 for _, p := range pairs {
863− leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
826+ leftStr := renderLeftColumn(fileName, p.left, leftWidth)
864− rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
827+ rightStr := renderRightColumn(fileName, p.right, rightWidth)
865 sb.WriteString(leftStr + rightStr + "\n")
828 sb.WriteString(leftStr + rightStr + "\n")
866 }
829 }
867
830
@@ -876,54 +839,7 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
876 }
839 }
877
840
878 var sb strings.Builder
841 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−
916 for _, h := range diffResult.Hunks {
842 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− }
927 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
843 sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
928 }
844 }
929
845
@@ -944,8 +860,8 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
944 removals = 0
860 removals = 0
945 )
861 )
946
862
947− lines := strings.Split(unified, "\n")
863+ lines := strings.SplitSeq(unified, "\n")
948− for _, line := range lines {
864+ for line := range lines {
949 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
865 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
950 additions++
866 additions++
951 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
867 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
internal/tui/components/chat/chat.go+46−26
@@ -9,6 +9,7 @@ import (
9 "github.com/opencode-ai/opencode/internal/config"
9 "github.com/opencode-ai/opencode/internal/config"
10 "github.com/opencode-ai/opencode/internal/session"
10 "github.com/opencode-ai/opencode/internal/session"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
12+ "github.com/opencode-ai/opencode/internal/tui/theme"
12 "github.com/opencode-ai/opencode/internal/version"
13 "github.com/opencode-ai/opencode/internal/version"
13)
14)
14
15
@@ -22,12 +23,29 @@ type SessionClearedMsg struct{}
22
23
23type EditorFocusMsg bool
24type EditorFocusMsg bool
24
25
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+
25func lspsConfigured(width int) string {
36func lspsConfigured(width int) string {
26 cfg := config.Get()
37 cfg := config.Get()
27 title := "LSP Configuration"
38 title := "LSP Configuration"
28 title = ansi.Truncate(title, width, "…")
39 title = ansi.Truncate(title, width, "…")
29
40
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)
31
49
32 // Get LSP names and sort them for consistent ordering
50 // Get LSP names and sort them for consistent ordering
33 var lspNames []string
51 var lspNames []string
@@ -39,16 +57,19 @@ func lspsConfigured(width int) string {
39 var lspViews []string
57 var lspViews []string
40 for _, name := range lspNames {
58 for _, name := range lspNames {
41 lsp := cfg.LSP[name]
59 lsp := cfg.LSP[name]
42− lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
60+ lspName := baseStyle.
61+ Foreground(t.Text()).
43− fmt.Sprintf("• %s", name),
62+ Render(fmt.Sprintf("• %s", name))
44− )
63+
45 cmd := lsp.Command
64 cmd := lsp.Command
46 cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
65 cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
47− lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
66+
67+ lspPath := baseStyle.
68+ Foreground(t.TextMuted()).
48− fmt.Sprintf(" (%s)", cmd),
69+ Render(fmt.Sprintf(" (%s)", cmd))
49− )
70+
50 lspViews = append(lspViews,
71 lspViews = append(lspViews,
51− styles.BaseStyle.
72+ baseStyle.
52 Width(width).
73 Width(width).
53 Render(
74 Render(
54 lipgloss.JoinHorizontal(
75 lipgloss.JoinHorizontal(
@@ -59,7 +80,8 @@ func lspsConfigured(width int) string {
59 ),
80 ),
60 )
81 )
61 }
82 }
62− return styles.BaseStyle.
83+
84+ return baseStyle.
63 Width(width).
85 Width(width).
64 Render(
86 Render(
65 lipgloss.JoinVertical(
87 lipgloss.JoinVertical(
@@ -75,10 +97,14 @@ func lspsConfigured(width int) string {
75
97
76func logo(width int) string {
98func logo(width int) string {
77 logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
99 logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
100+ t := theme.CurrentTheme()
101+ baseStyle := styles.BaseStyle()
78
102
103+ versionText := baseStyle.
104+ Foreground(t.TextMuted()).
79− version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
105+ Render(version.Version)
80
106
81− return styles.BaseStyle.
107+ return baseStyle.
82 Bold(true).
108 Bold(true).
83 Width(width).
109 Width(width).
84 Render(
110 Render(
@@ -86,34 +112,28 @@ func logo(width int) string {
86 lipgloss.Left,
112 lipgloss.Left,
87 logo,
113 logo,
88 " ",
114 " ",
89− version,
115+ versionText,
90 ),
116 ),
91 )
117 )
92}
118}
93
119
94func repo(width int) string {
120func repo(width int) string {
95 repo := "https://github.com/opencode-ai/opencode"
121 repo := "https://github.com/opencode-ai/opencode"
122+ t := theme.CurrentTheme()
123+
96− return styles.BaseStyle.
124+ return styles.BaseStyle().
97− Foreground(styles.ForgroundDim).
125+ Foreground(t.TextMuted()).
98 Width(width).
126 Width(width).
99 Render(repo)
127 Render(repo)
100}
128}
101
129
102func cwd(width int) string {
130func cwd(width int) string {
103 cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
131 cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
132+ t := theme.CurrentTheme()
133+
104− return styles.BaseStyle.
134+ return styles.BaseStyle().
105− Foreground(styles.ForgroundDim).
135+ Foreground(t.TextMuted()).
106 Width(width).
136 Width(width).
107 Render(cwd)
137 Render(cwd)
108}
138}
109
139
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 (
10 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/lipgloss"
11 "github.com/opencode-ai/opencode/internal/app"
11 "github.com/opencode-ai/opencode/internal/app"
12 "github.com/opencode-ai/opencode/internal/session"
12 "github.com/opencode-ai/opencode/internal/session"
13+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
15 "github.com/opencode-ai/opencode/internal/tui/styles"
16+ "github.com/opencode-ai/opencode/internal/tui/theme"
15 "github.com/opencode-ai/opencode/internal/tui/util"
17 "github.com/opencode-ai/opencode/internal/tui/util"
16)
18)
17
19
@@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd {
100func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
102func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101 var cmd tea.Cmd
103 var cmd tea.Cmd
102 switch msg := msg.(type) {
104 switch msg := msg.(type) {
105+ case dialog.ThemeChangedMsg:
106+ m.textarea = CreateTextArea(&m.textarea)
107+ return m, nil
103 case SessionSelectedMsg:
108 case SessionSelectedMsg:
104 if msg.ID != m.session.ID {
109 if msg.ID != m.session.ID {
105 m.session = msg
110 m.session = msg
@@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134}
139}
135
140
136func (m *editorCmp) View() string {
141func (m *editorCmp) View() string {
142+ t := theme.CurrentTheme()
143+
144+ // Style the prompt with theme colors
137− style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
145+ style := lipgloss.NewStyle().
146+ Padding(0, 0, 0, 1).
147+ Bold(true).
148+ Foreground(t.Primary())
138
149
139 return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
150 return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
140}
151}
@@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding {
155 return bindings
166 return bindings
156}
167}
157
168
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+
158func NewEditorCmp(app *app.App) tea.Model {
199func NewEditorCmp(app *app.App) tea.Model {
159− ti := textarea.New()
200+ ta := CreateTextArea(nil)
160− ti.Prompt = " "
201+
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()
173 return &editorCmp{
202 return &editorCmp{
174 app: app,
203 app: app,
175− textarea: ti,
204+ textarea: ta,
176 }
205 }
177}
206}
207+
internal/tui/components/chat/list.go+47−29
@@ -14,7 +14,9 @@ import (
14 "github.com/opencode-ai/opencode/internal/message"
14 "github.com/opencode-ai/opencode/internal/message"
15 "github.com/opencode-ai/opencode/internal/pubsub"
15 "github.com/opencode-ai/opencode/internal/pubsub"
16 "github.com/opencode-ai/opencode/internal/session"
16 "github.com/opencode-ai/opencode/internal/session"
17+ "github.com/opencode-ai/opencode/internal/tui/components/dialog"
17 "github.com/opencode-ai/opencode/internal/tui/styles"
18 "github.com/opencode-ai/opencode/internal/tui/styles"
19+ "github.com/opencode-ai/opencode/internal/tui/theme"
18 "github.com/opencode-ai/opencode/internal/tui/util"
20 "github.com/opencode-ai/opencode/internal/tui/util"
19)
21)
20
22
@@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd {
69func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
71func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
70 var cmds []tea.Cmd
72 var cmds []tea.Cmd
71 switch msg := msg.(type) {
73 switch msg := msg.(type) {
72−
74+ case dialog.ThemeChangedMsg:
75+ m.rerender()
76+ return m, nil
73 case SessionSelectedMsg:
77 case SessionSelectedMsg:
74 if msg.ID != m.session.ID {
78 if msg.ID != m.session.ID {
75 cmd := m.SetSession(msg)
79 cmd := m.SetSession(msg)
@@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string {
174func (m *messagesCmp) renderView() {
178func (m *messagesCmp) renderView() {
175 m.uiMessages = make([]uiMessage, 0)
179 m.uiMessages = make([]uiMessage, 0)
176 pos := 0
180 pos := 0
181+ baseStyle := styles.BaseStyle()
177
182
178 if m.width == 0 {
183 if m.width == 0 {
179 return
184 return
@@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() {
225 messages := make([]string, 0)
230 messages := make([]string, 0)
226 for _, v := range m.uiMessages {
231 for _, v := range m.uiMessages {
227 messages = append(messages, v.content,
232 messages = append(messages, v.content,
228− styles.BaseStyle.
233+ baseStyle.
229 Width(m.width).
234 Width(m.width).
230− Render(
235+ Render(""),
231− "",
232− ),
233 )
236 )
234 }
237 }
235 m.viewport.SetContent(
238 m.viewport.SetContent(
236− styles.BaseStyle.
239+ baseStyle.
237 Width(m.width).
240 Width(m.width).
238 Render(
241 Render(
239 lipgloss.JoinVertical(
242 lipgloss.JoinVertical(
@@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() {
245}
248}
246
249
247func (m *messagesCmp) View() string {
250func (m *messagesCmp) View() string {
251+ baseStyle := styles.BaseStyle()
252+
248 if m.rendering {
253 if m.rendering {
249− return styles.BaseStyle.
254+ return baseStyle.
250 Width(m.width).
255 Width(m.width).
251 Render(
256 Render(
252 lipgloss.JoinVertical(
257 lipgloss.JoinVertical(
@@ -258,14 +263,14 @@ func (m *messagesCmp) View() string {
258 )
263 )
259 }
264 }
260 if len(m.messages) == 0 {
265 if len(m.messages) == 0 {
261− content := styles.BaseStyle.
266+ content := baseStyle.
262 Width(m.width).
267 Width(m.width).
263 Height(m.height - 1).
268 Height(m.height - 1).
264 Render(
269 Render(
265 m.initialScreen(),
270 m.initialScreen(),
266 )
271 )
267
272
268− return styles.BaseStyle.
273+ return baseStyle.
269 Width(m.width).
274 Width(m.width).
270 Render(
275 Render(
271 lipgloss.JoinVertical(
276 lipgloss.JoinVertical(
@@ -277,7 +282,7 @@ func (m *messagesCmp) View() string {
277 )
282 )
278 }
283 }
279
284
280− return styles.BaseStyle.
285+ return baseStyle.
281 Width(m.width).
286 Width(m.width).
282 Render(
287 Render(
283 lipgloss.JoinVertical(
288 lipgloss.JoinVertical(
@@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
328func (m *messagesCmp) working() string {
333func (m *messagesCmp) working() string {
329 text := ""
334 text := ""
330 if m.IsAgentWorking() && len(m.messages) > 0 {
335 if m.IsAgentWorking() && len(m.messages) > 0 {
336+ t := theme.CurrentTheme()
337+ baseStyle := styles.BaseStyle()
338+
331 task := "Thinking..."
339 task := "Thinking..."
332 lastMessage := m.messages[len(m.messages)-1]
340 lastMessage := m.messages[len(m.messages)-1]
333 if hasToolsWithoutResponse(m.messages) {
341 if hasToolsWithoutResponse(m.messages) {
@@ -338,42 +346,49 @@ func (m *messagesCmp) working() string {
338 task = "Generating..."
346 task = "Generating..."
339 }
347 }
340 if task != "" {
348 if task != "" {
341− text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render(
349+ text += baseStyle.
350+ Width(m.width).
351+ Foreground(t.Primary()).
352+ Bold(true).
342− fmt.Sprintf("%s %s ", m.spinner.View(), task),
353+ Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
343− )
344 }
354 }
345 }
355 }
346 return text
356 return text
347}
357}
348
358
349func (m *messagesCmp) help() string {
359func (m *messagesCmp) help() string {
360+ t := theme.CurrentTheme()
361+ baseStyle := styles.BaseStyle()
362+
350 text := ""
363 text := ""
351
364
352 if m.app.CoderAgent.IsBusy() {
365 if m.app.CoderAgent.IsBusy() {
353 text += lipgloss.JoinHorizontal(
366 text += lipgloss.JoinHorizontal(
354 lipgloss.Left,
367 lipgloss.Left,
355− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
368+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
356− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
369+ baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
357− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
370+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
358 )
371 )
359 } else {
372 } else {
360 text += lipgloss.JoinHorizontal(
373 text += lipgloss.JoinHorizontal(
361 lipgloss.Left,
374 lipgloss.Left,
362− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
375+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
363− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"),
376+ baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
364− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"),
377+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
365− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"),
378+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
366− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"),
379+ baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
367− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"),
380+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
368 )
381 )
369 }
382 }
370− return styles.BaseStyle.
383+ return baseStyle.
371 Width(m.width).
384 Width(m.width).
372 Render(text)
385 Render(text)
373}
386}
374
387
375func (m *messagesCmp) initialScreen() string {
388func (m *messagesCmp) initialScreen() string {
376− return styles.BaseStyle.Width(m.width).Render(
389+ baseStyle := styles.BaseStyle()
390+
391+ return baseStyle.Width(m.width).Render(
377 lipgloss.JoinVertical(
392 lipgloss.JoinVertical(
378 lipgloss.Top,
393 lipgloss.Top,
379 header(m.width),
394 header(m.width),
@@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string {
383 )
398 )
384}
399}
385
400
401+func (m *messagesCmp) rerender() {
402+ for _, msg := range m.messages {
403+ delete(m.cachedContent, msg.ID)
404+ }
405+ m.renderView()
406+}
407+
386func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
408func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
387 if m.width == width && m.height == height {
409 if m.width == width && m.height == height {
388 return nil
410 return nil
@@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
391 m.height = height
413 m.height = height
392 m.viewport.Width = width
414 m.viewport.Width = width
393 m.viewport.Height = height - 2
415 m.viewport.Height = height - 2
394− for _, msg := range m.messages {
416+ m.rerender()
395− delete(m.cachedContent, msg.ID)
396− }
397− m.uiMessages = make([]uiMessage, 0)
398− m.renderView()
399 return nil
417 return nil
400}
418}
401
419
internal/tui/components/chat/message.go+94−85
@@ -6,10 +6,8 @@ import (
6 "fmt"
6 "fmt"
7 "path/filepath"
7 "path/filepath"
8 "strings"
8 "strings"
9− "sync"
10 "time"
9 "time"
11
10
12− "github.com/charmbracelet/glamour"
13 "github.com/charmbracelet/lipgloss"
11 "github.com/charmbracelet/lipgloss"
14 "github.com/charmbracelet/x/ansi"
12 "github.com/charmbracelet/x/ansi"
15 "github.com/opencode-ai/opencode/internal/config"
13 "github.com/opencode-ai/opencode/internal/config"
@@ -19,6 +17,7 @@ import (
19 "github.com/opencode-ai/opencode/internal/llm/tools"
17 "github.com/opencode-ai/opencode/internal/llm/tools"
20 "github.com/opencode-ai/opencode/internal/message"
18 "github.com/opencode-ai/opencode/internal/message"
21 "github.com/opencode-ai/opencode/internal/tui/styles"
19 "github.com/opencode-ai/opencode/internal/tui/styles"
20+ "github.com/opencode-ai/opencode/internal/tui/theme"
22)
21)
23
22
24type uiMessageType int
23type uiMessageType int
@@ -31,8 +30,6 @@ const (
31 maxResultHeight = 10
30 maxResultHeight = 10
32)
31)
33
32
34−var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false))
35−
36type uiMessage struct {
33type uiMessage struct {
37 ID string
34 ID string
38 messageType uiMessageType
35 messageType uiMessageType
@@ -41,46 +38,37 @@ type uiMessage struct {
41 content string
38 content string
42}
39}
43
40
44−type renderCache struct {
45− mutex sync.Mutex
46− cache map[string][]uiMessage
47−}
48−
49func toMarkdown(content string, focused bool, width int) string {
41func toMarkdown(content string, focused bool, width int) string {
50− r, _ := glamour.NewTermRenderer(
42+ r := styles.GetMarkdownRenderer(width)
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− }
60 rendered, _ := r.Render(content)
43 rendered, _ := r.Render(content)
61 return rendered
44 return rendered
62}
45}
63
46
64func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
47func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
48+ t := theme.CurrentTheme()
49+
65− style := styles.BaseStyle.
50+ style := styles.BaseStyle().
66 Width(width - 1).
51 Width(width - 1).
67 BorderLeft(true).
52 BorderLeft(true).
68− Foreground(styles.ForgroundDim).
53+ Foreground(t.TextMuted()).
69− BorderForeground(styles.PrimaryColor).
54+ BorderForeground(t.Primary()).
70 BorderStyle(lipgloss.ThickBorder())
55 BorderStyle(lipgloss.ThickBorder())
56+
71 if isUser {
57 if isUser {
72− style = style.
73− BorderForeground(styles.Blue)
58+ style = style.BorderForeground(t.Secondary())
74 }
59 }
60+
61+ // Apply markdown formatting and handle background color
75 parts := []string{
62 parts := []string{
76− styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background),
63+ styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
77 }
64 }
78
65
79− // remove newline at the end
66+ // Remove newline at the end
80 parts[0] = strings.TrimSuffix(parts[0], "\n")
67 parts[0] = strings.TrimSuffix(parts[0], "\n")
81 if len(info) > 0 {
68 if len(info) > 0 {
82 parts = append(parts, info...)
69 parts = append(parts, info...)
83 }
70 }
71+
84 rendered := style.Render(
72 rendered := style.Render(
85 lipgloss.JoinVertical(
73 lipgloss.JoinVertical(
86 lipgloss.Left,
74 lipgloss.Left,
@@ -121,26 +109,37 @@ func renderAssistantMessage(
121 finishData := msg.FinishPart()
109 finishData := msg.FinishPart()
122 info := []string{}
110 info := []string{}
123
111
112+ t := theme.CurrentTheme()
113+ baseStyle := styles.BaseStyle()
114+
124 // Add finish info if available
115 // Add finish info if available
125 if finished {
116 if finished {
126 switch finishData.Reason {
117 switch finishData.Reason {
127 case message.FinishReasonEndTurn:
118 case message.FinishReasonEndTurn:
128− took := formatTimeDifference(msg.CreatedAt, finishData.Time)
119+ took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
129− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
120+ info = append(info, baseStyle.
121+ Width(width-1).
122+ Foreground(t.TextMuted()).
130− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
123+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
131− ))
124+ )
132 case message.FinishReasonCanceled:
125 case message.FinishReasonCanceled:
133− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
126+ info = append(info, baseStyle.
127+ Width(width-1).
128+ Foreground(t.TextMuted()).
134− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"),
129+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
135− ))
130+ )
136 case message.FinishReasonError:
131 case message.FinishReasonError:
137− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
132+ info = append(info, baseStyle.
133+ Width(width-1).
134+ Foreground(t.TextMuted()).
138− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"),
135+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
139− ))
136+ )
140 case message.FinishReasonPermissionDenied:
137 case message.FinishReasonPermissionDenied:
141− info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
138+ info = append(info, baseStyle.
139+ Width(width-1).
140+ Foreground(t.TextMuted()).
142− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"),
141+ Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
143− ))
142+ )
144 }
143 }
145 }
144 }
146 if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
145 if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
@@ -414,32 +413,36 @@ func truncateHeight(content string, height int) string {
414}
413}
415
414
416func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
415func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
416+ t := theme.CurrentTheme()
417+ baseStyle := styles.BaseStyle()
418+
417 if response.IsError {
419 if response.IsError {
418 errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
420 errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
419 errContent = ansi.Truncate(errContent, width-1, "...")
421 errContent = ansi.Truncate(errContent, width-1, "...")
420− return styles.BaseStyle.
422+ return baseStyle.
421 Width(width).
423 Width(width).
422− Foreground(styles.Error).
424+ Foreground(t.Error()).
423 Render(errContent)
425 Render(errContent)
424 }
426 }
427+
425 resultContent := truncateHeight(response.Content, maxResultHeight)
428 resultContent := truncateHeight(response.Content, maxResultHeight)
426 switch toolCall.Name {
429 switch toolCall.Name {
427 case agent.AgentToolName:
430 case agent.AgentToolName:
428 return styles.ForceReplaceBackgroundWithLipgloss(
431 return styles.ForceReplaceBackgroundWithLipgloss(
429 toMarkdown(resultContent, false, width),
432 toMarkdown(resultContent, false, width),
430− styles.Background,
433+ t.Background(),
431 )
434 )
432 case tools.BashToolName:
435 case tools.BashToolName:
433 resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
436 resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
434 return styles.ForceReplaceBackgroundWithLipgloss(
437 return styles.ForceReplaceBackgroundWithLipgloss(
435 toMarkdown(resultContent, true, width),
438 toMarkdown(resultContent, true, width),
436− styles.Background,
439+ t.Background(),
437 )
440 )
438 case tools.EditToolName:
441 case tools.EditToolName:
439 metadata := tools.EditResponseMetadata{}
442 metadata := tools.EditResponseMetadata{}
440 json.Unmarshal([]byte(response.Metadata), &metadata)
443 json.Unmarshal([]byte(response.Metadata), &metadata)
441 truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
444 truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
442− formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle))
445+ formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
443 return formattedDiff
446 return formattedDiff
444 case tools.FetchToolName:
447 case tools.FetchToolName:
445 var params tools.FetchParams
448 var params tools.FetchParams
@@ -454,16 +457,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
454 resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
457 resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
455 return styles.ForceReplaceBackgroundWithLipgloss(
458 return styles.ForceReplaceBackgroundWithLipgloss(
456 toMarkdown(resultContent, true, width),
459 toMarkdown(resultContent, true, width),
457− styles.Background,
460+ t.Background(),
458 )
461 )
459 case tools.GlobToolName:
462 case tools.GlobToolName:
460− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
463+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
461 case tools.GrepToolName:
464 case tools.GrepToolName:
462− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
465+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
463 case tools.LSToolName:
466 case tools.LSToolName:
464− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
467+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
465 case tools.SourcegraphToolName:
468 case tools.SourcegraphToolName:
466− return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
469+ return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
467 case tools.ViewToolName:
470 case tools.ViewToolName:
468 metadata := tools.ViewResponseMetadata{}
471 metadata := tools.ViewResponseMetadata{}
469 json.Unmarshal([]byte(response.Metadata), &metadata)
472 json.Unmarshal([]byte(response.Metadata), &metadata)
@@ -476,7 +479,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
476 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
479 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
477 return styles.ForceReplaceBackgroundWithLipgloss(
480 return styles.ForceReplaceBackgroundWithLipgloss(
478 toMarkdown(resultContent, true, width),
481 toMarkdown(resultContent, true, width),
479− styles.Background,
482+ t.Background(),
480 )
483 )
481 case tools.WriteToolName:
484 case tools.WriteToolName:
482 params := tools.WriteParams{}
485 params := tools.WriteParams{}
@@ -492,13 +495,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
492 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
495 resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
493 return styles.ForceReplaceBackgroundWithLipgloss(
496 return styles.ForceReplaceBackgroundWithLipgloss(
494 toMarkdown(resultContent, true, width),
497 toMarkdown(resultContent, true, width),
495− styles.Background,
498+ t.Background(),
496 )
499 )
497 default:
500 default:
498 resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
501 resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
499 return styles.ForceReplaceBackgroundWithLipgloss(
502 return styles.ForceReplaceBackgroundWithLipgloss(
500 toMarkdown(resultContent, true, width),
503 toMarkdown(resultContent, true, width),
501− styles.Background,
504+ t.Background(),
502 )
505 )
503 }
506 }
504}
507}
@@ -515,39 +518,31 @@ func renderToolMessage(
515 if nested {
518 if nested {
516 width = width - 3
519 width = width - 3
517 }
520 }
521+
522+ t := theme.CurrentTheme()
518− style := styles.BaseStyle.
523+ baseStyle := styles.BaseStyle()
524+
525+ style := baseStyle.
519 Width(width - 1).
526 Width(width - 1).
520 BorderLeft(true).
527 BorderLeft(true).
521 BorderStyle(lipgloss.ThickBorder()).
528 BorderStyle(lipgloss.ThickBorder()).
522 PaddingLeft(1).
529 PaddingLeft(1).
523− BorderForeground(styles.ForgroundDim)
530+ BorderForeground(t.TextMuted())
524
531
525 response := findToolResponse(toolCall.ID, allMessages)
532 response := findToolResponse(toolCall.ID, allMessages)
533+ toolNameText := baseStyle.Foreground(t.TextMuted()).
526− toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
534+ Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
527
535
528 if !toolCall.Finished {
536 if !toolCall.Finished {
529 // Get a brief description of what the tool is doing
537 // Get a brief description of what the tool is doing
530 toolAction := getToolAction(toolCall.Name)
538 toolAction := getToolAction(toolCall.Name)
531
539
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.
540+ progressText := baseStyle.
546− Width(width - 2 - lipgloss.Width(toolName)).
541+ Width(width - 2 - lipgloss.Width(toolNameText)).
547− Foreground(styles.ForgroundDim).
542+ Foreground(t.TextMuted()).
548 Render(fmt.Sprintf("%s", toolAction))
543 Render(fmt.Sprintf("%s", toolAction))
549
544
550− content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
545+ content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
551 toolMsg := uiMessage{
546 toolMsg := uiMessage{
552 messageType: toolMessageType,
547 messageType: toolMessageType,
553 position: position,
548 position: position,
@@ -556,37 +551,39 @@ func renderToolMessage(
556 }
551 }
557 return toolMsg
552 return toolMsg
558 }
553 }
554+
559− params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
555+ params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
560 responseContent := ""
556 responseContent := ""
561 if response != nil {
557 if response != nil {
562 responseContent = renderToolResponse(toolCall, *response, width-2)
558 responseContent = renderToolResponse(toolCall, *response, width-2)
563 responseContent = strings.TrimSuffix(responseContent, "\n")
559 responseContent = strings.TrimSuffix(responseContent, "\n")
564 } else {
560 } else {
565− responseContent = styles.BaseStyle.
561+ responseContent = baseStyle.
566 Italic(true).
562 Italic(true).
567 Width(width - 2).
563 Width(width - 2).
568− Foreground(styles.ForgroundDim).
564+ Foreground(t.TextMuted()).
569 Render("Waiting for response...")
565 Render("Waiting for response...")
570 }
566 }
571
567
572 parts := []string{}
568 parts := []string{}
573 if !nested {
569 if !nested {
574− params := styles.BaseStyle.
570+ formattedParams := baseStyle.
575− Width(width - 2 - lipgloss.Width(toolName)).
571+ Width(width - 2 - lipgloss.Width(toolNameText)).
576− Foreground(styles.ForgroundDim).
572+ Foreground(t.TextMuted()).
577 Render(params)
573 Render(params)
578
574
579− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params))
575+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
580 } else {
576 } else {
581− prefix := styles.BaseStyle.
577+ prefix := baseStyle.
582− Foreground(styles.ForgroundDim).
578+ Foreground(t.TextMuted()).
583 Render(" └ ")
579 Render(" └ ")
584− params := styles.BaseStyle.
580+ formattedParams := baseStyle.
585− Width(width - 2 - lipgloss.Width(toolName)).
581+ Width(width - 2 - lipgloss.Width(toolNameText)).
586− Foreground(styles.ForgroundMid).
582+ Foreground(t.TextMuted()).
587 Render(params)
583 Render(params)
588− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params))
584+ parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
589 }
585 }
586+
590 if toolCall.Name == agent.AgentToolName {
587 if toolCall.Name == agent.AgentToolName {
591 taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
588 taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
592 toolCalls := []message.ToolCall{}
589 toolCalls := []message.ToolCall{}
@@ -622,3 +619,15 @@ func renderToolMessage(
622 }
619 }
623 return toolMsg
620 return toolMsg
624}
621}
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 (
14 "github.com/opencode-ai/opencode/internal/pubsub"
14 "github.com/opencode-ai/opencode/internal/pubsub"
15 "github.com/opencode-ai/opencode/internal/session"
15 "github.com/opencode-ai/opencode/internal/session"
16 "github.com/opencode-ai/opencode/internal/tui/styles"
16 "github.com/opencode-ai/opencode/internal/tui/styles"
17+ "github.com/opencode-ai/opencode/internal/tui/theme"
17)
18)
18
19
19type sidebarCmp struct {
20type sidebarCmp struct {
@@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81}
82}
82
83
83func (m *sidebarCmp) View() string {
84func (m *sidebarCmp) View() string {
84− return styles.BaseStyle.
85+ baseStyle := styles.BaseStyle()
86+
87+ return baseStyle.
85 Width(m.width).
88 Width(m.width).
86 PaddingLeft(4).
89 PaddingLeft(4).
87 PaddingRight(2).
90 PaddingRight(2).
@@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string {
101}
104}
102
105
103func (m *sidebarCmp) sessionSection() string {
106func (m *sidebarCmp) sessionSection() string {
104− sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
107+ t := theme.CurrentTheme()
108+ baseStyle := styles.BaseStyle()
109+
110+ sessionKey := baseStyle.
111+ Foreground(t.Primary()).
112+ Bold(true).
113+ Render("Session")
114+
105− sessionValue := styles.BaseStyle.
115+ sessionValue := baseStyle.
106− Foreground(styles.Forground).
116+ Foreground(t.Text()).
107 Width(m.width - lipgloss.Width(sessionKey)).
117 Width(m.width - lipgloss.Width(sessionKey)).
108 Render(fmt.Sprintf(": %s", m.session.Title))
118 Render(fmt.Sprintf(": %s", m.session.Title))
119+
109 return lipgloss.JoinHorizontal(
120 return lipgloss.JoinHorizontal(
110 lipgloss.Left,
121 lipgloss.Left,
111 sessionKey,
122 sessionKey,
@@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string {
114}
125}
115
126
116func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
127func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
128+ t := theme.CurrentTheme()
129+ baseStyle := styles.BaseStyle()
130+
117 stats := ""
131 stats := ""
118 if additions > 0 && removals > 0 {
132 if additions > 0 && removals > 0 {
133+ additionsStr := baseStyle.
134+ Foreground(t.Success()).
135+ PaddingLeft(1).
119− additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions))
136+ Render(fmt.Sprintf("+%d", additions))
137+
138+ removalsStr := baseStyle.
139+ Foreground(t.Error()).
140+ PaddingLeft(1).
120− removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals))
141+ Render(fmt.Sprintf("-%d", removals))
142+
121− content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals)
143+ content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
122− stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content)
144+ stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
123 } else if additions > 0 {
145 } else if additions > 0 {
146+ additionsStr := fmt.Sprintf(" %s", baseStyle.
147+ PaddingLeft(1).
148+ Foreground(t.Success()).
124− additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions)))
149+ Render(fmt.Sprintf("+%d", additions)))
125− stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions)
150+ stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
126 } else if removals > 0 {
151 } else if removals > 0 {
152+ removalsStr := fmt.Sprintf(" %s", baseStyle.
153+ PaddingLeft(1).
154+ Foreground(t.Error()).
127− removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals)))
155+ Render(fmt.Sprintf("-%d", removals)))
128− stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals)
156+ stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
129 }
157 }
130− filePathStr := styles.BaseStyle.Render(filePath)
131
158
132− return styles.BaseStyle.
159+ filePathStr := baseStyle.Render(filePath)
160+
161+ return baseStyle.
133 Width(m.width).
162 Width(m.width).
134 Render(
163 Render(
135 lipgloss.JoinHorizontal(
164 lipgloss.JoinHorizontal(
@@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
141}
170}
142
171
143func (m *sidebarCmp) modifiedFiles() string {
172func (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:")
145
181
146 // If no modified files, show a placeholder message
182 // If no modified files, show a placeholder message
147 if m.modFiles == nil || len(m.modFiles) == 0 {
183 if m.modFiles == nil || len(m.modFiles) == 0 {
@@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string {
150 if remainingWidth > 0 {
186 if remainingWidth > 0 {
151 message += strings.Repeat(" ", remainingWidth)
187 message += strings.Repeat(" ", remainingWidth)
152 }
188 }
153− return styles.BaseStyle.
189+ return baseStyle.
154 Width(m.width).
190 Width(m.width).
155 Render(
191 Render(
156 lipgloss.JoinVertical(
192 lipgloss.JoinVertical(
157 lipgloss.Top,
193 lipgloss.Top,
158 modifiedFiles,
194 modifiedFiles,
159− styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message),
195+ baseStyle.Foreground(t.TextMuted()).Render(message),
160 ),
196 ),
161 )
197 )
162 }
198 }
@@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string {
175 fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
211 fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
176 }
212 }
177
213
178− return styles.BaseStyle.
214+ return baseStyle.
179 Width(m.width).
215 Width(m.width).
180 Render(
216 Render(
181 lipgloss.JoinVertical(
217 lipgloss.JoinVertical(
internal/tui/components/core/status.go+64−28
@@ -15,12 +15,13 @@ import (
15 "github.com/opencode-ai/opencode/internal/session"
15 "github.com/opencode-ai/opencode/internal/session"
16 "github.com/opencode-ai/opencode/internal/tui/components/chat"
16 "github.com/opencode-ai/opencode/internal/tui/components/chat"
17 "github.com/opencode-ai/opencode/internal/tui/styles"
17 "github.com/opencode-ai/opencode/internal/tui/styles"
18+ "github.com/opencode-ai/opencode/internal/tui/theme"
18 "github.com/opencode-ai/opencode/internal/tui/util"
19 "github.com/opencode-ai/opencode/internal/tui/util"
19)
20)
20
21
21type StatusCmp interface {
22type StatusCmp interface {
22 tea.Model
23 tea.Model
23− SetHelpMsg(string)
24+ SetHelpWidgetMsg(string)
24}
25}
25
26
26type statusCmp struct {
27type statusCmp struct {
@@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
70 return m, nil
71 return m, nil
71}
72}
72
73
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+}
74
89
75func formatTokensAndCost(tokens int64, cost float64) string {
90func formatTokensAndCost(tokens int64, cost float64) string {
76 // Format tokens in human-readable format (e.g., 110K, 1.2M)
91 // Format tokens in human-readable format (e.g., 110K, 1.2M)
@@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string {
99}
114}
100
115
101func (m statusCmp) View() string {
116func (m statusCmp) View() string {
102− status := helpWidget
117+ t := theme.CurrentTheme()
118+
119+ // Initialize the help widget
120+ status := getHelpWidget("")
121+
103 if m.session.ID != "" {
122 if m.session.ID != "" {
104 tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
123 tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
105− tokensStyle := styles.Padded.
124+ tokensStyle := styles.Padded().
106− Background(styles.Forground).
125+ Background(t.Text()).
107− Foreground(styles.BackgroundDim).
126+ Foreground(t.BackgroundSecondary()).
108 Render(tokens)
127 Render(tokens)
109 status += tokensStyle
128 status += tokensStyle
110 }
129 }
111
130
131+ diagnostics := styles.Padded().
112− diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
132+ Background(t.BackgroundDarker()).
133+ Render(m.projectDiagnostics())
134+
113 if m.info.Msg != "" {
135 if m.info.Msg != "" {
114− infoStyle := styles.Padded.
136+ infoStyle := styles.Padded().
115− Foreground(styles.Base).
137+ Foreground(t.Background()).
116 Width(m.availableFooterMsgWidth(diagnostics))
138 Width(m.availableFooterMsgWidth(diagnostics))
139+
117 switch m.info.Type {
140 switch m.info.Type {
118 case util.InfoTypeInfo:
141 case util.InfoTypeInfo:
119− infoStyle = infoStyle.Background(styles.BorderColor)
142+ infoStyle = infoStyle.Background(t.Info())
120 case util.InfoTypeWarn:
143 case util.InfoTypeWarn:
121− infoStyle = infoStyle.Background(styles.Peach)
144+ infoStyle = infoStyle.Background(t.Warning())
122 case util.InfoTypeError:
145 case util.InfoTypeError:
123− infoStyle = infoStyle.Background(styles.Red)
146+ infoStyle = infoStyle.Background(t.Error())
124 }
147 }
148+
125 // Truncate message if it's longer than available width
149 // Truncate message if it's longer than available width
126 msg := m.info.Msg
150 msg := m.info.Msg
127 availWidth := m.availableFooterMsgWidth(diagnostics) - 10
151 availWidth := m.availableFooterMsgWidth(diagnostics) - 10
@@ -130,9 +154,9 @@ func (m statusCmp) View() string {
130 }
154 }
131 status += infoStyle.Render(msg)
155 status += infoStyle.Render(msg)
132 } else {
156 } else {
133− status += styles.Padded.
157+ status += styles.Padded().
134− Foreground(styles.Base).
158+ Foreground(t.Text()).
135− Background(styles.BackgroundDim).
159+ Background(t.BackgroundSecondary()).
136 Width(m.availableFooterMsgWidth(diagnostics)).
160 Width(m.availableFooterMsgWidth(diagnostics)).
137 Render("")
161 Render("")
138 }
162 }
@@ -143,6 +167,8 @@ func (m statusCmp) View() string {
143}
167}
144
168
145func (m *statusCmp) projectDiagnostics() string {
169func (m *statusCmp) projectDiagnostics() string {
170+ t := theme.CurrentTheme()
171+
146 // Check if any LSP server is still initializing
172 // Check if any LSP server is still initializing
147 initializing := false
173 initializing := false
148 for _, client := range m.lspClients {
174 for _, client := range m.lspClients {
@@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string {
155 // If any server is initializing, show that status
181 // If any server is initializing, show that status
156 if initializing {
182 if initializing {
157 return lipgloss.NewStyle().
183 return lipgloss.NewStyle().
158− Background(styles.BackgroundDarker).
184+ Background(t.BackgroundDarker()).
159− Foreground(styles.Peach).
185+ Foreground(t.Warning()).
160 Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
186 Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
161 }
187 }
162
188
@@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string {
189
215
190 if len(errorDiagnostics) > 0 {
216 if len(errorDiagnostics) > 0 {
191 errStr := lipgloss.NewStyle().
217 errStr := lipgloss.NewStyle().
192− Background(styles.BackgroundDarker).
218+ Background(t.BackgroundDarker()).
193− Foreground(styles.Error).
219+ Foreground(t.Error()).
194 Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
220 Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
195 diagnostics = append(diagnostics, errStr)
221 diagnostics = append(diagnostics, errStr)
196 }
222 }
197 if len(warnDiagnostics) > 0 {
223 if len(warnDiagnostics) > 0 {
198 warnStr := lipgloss.NewStyle().
224 warnStr := lipgloss.NewStyle().
199− Background(styles.BackgroundDarker).
225+ Background(t.BackgroundDarker()).
200− Foreground(styles.Warning).
226+ Foreground(t.Warning()).
201 Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
227 Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
202 diagnostics = append(diagnostics, warnStr)
228 diagnostics = append(diagnostics, warnStr)
203 }
229 }
204 if len(hintDiagnostics) > 0 {
230 if len(hintDiagnostics) > 0 {
205 hintStr := lipgloss.NewStyle().
231 hintStr := lipgloss.NewStyle().
206− Background(styles.BackgroundDarker).
232+ Background(t.BackgroundDarker()).
207− Foreground(styles.Text).
233+ Foreground(t.Text()).
208 Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
234 Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
209 diagnostics = append(diagnostics, hintStr)
235 diagnostics = append(diagnostics, hintStr)
210 }
236 }
211 if len(infoDiagnostics) > 0 {
237 if len(infoDiagnostics) > 0 {
212 infoStr := lipgloss.NewStyle().
238 infoStr := lipgloss.NewStyle().
213− Background(styles.BackgroundDarker).
239+ Background(t.BackgroundDarker()).
214− Foreground(styles.Peach).
240+ Foreground(t.Info()).
215 Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
241 Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
216 diagnostics = append(diagnostics, infoStr)
242 diagnostics = append(diagnostics, infoStr)
217 }
243 }
@@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
230}
256}
231
257
232func (m statusCmp) model() string {
258func (m statusCmp) model() string {
259+ t := theme.CurrentTheme()
260+
233 cfg := config.Get()
261 cfg := config.Get()
234
262
235 coder, ok := cfg.Agents[config.AgentCoder]
263 coder, ok := cfg.Agents[config.AgentCoder]
@@ -237,14 +265,22 @@ func (m statusCmp) model() string {
237 return "Unknown"
265 return "Unknown"
238 }
266 }
239 model := models.SupportedModels[coder.Model]
267 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)
241}
273}
242
274
243−func (m statusCmp) SetHelpMsg(s string) {
275+func (m statusCmp) SetHelpWidgetMsg(s string) {
244− helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
276+ // Update the help widget text using the getHelpWidget function
277+ helpWidget = getHelpWidget(s)
245}
278}
246
279
247func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
280func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
281+ // Initialize the help widget with default text
282+ helpWidget = getHelpWidget("")
283+
248 return &statusCmp{
284 return &statusCmp{
249 messageTTL: 10 * time.Second,
285 messageTTL: 10 * time.Second,
250 lspClients: lspClients,
286 lspClients: lspClients,
internal/tui/components/dialog/commands.go+21−17
@@ -6,6 +6,7 @@ import (
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/opencode-ai/opencode/internal/tui/layout"
7 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9+ "github.com/opencode-ai/opencode/internal/tui/theme"
9 "github.com/opencode-ai/opencode/internal/tui/util"
10 "github.com/opencode-ai/opencode/internal/tui/util"
10)
11)
11
12
@@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112}
113}
113
114
114func (c *commandDialogCmp) View() string {
115func (c *commandDialogCmp) View() string {
116+ t := theme.CurrentTheme()
117+ baseStyle := styles.BaseStyle()
118+
115 if len(c.commands) == 0 {
119 if len(c.commands) == 0 {
116− return styles.BaseStyle.Padding(1, 2).
120+ return baseStyle.Padding(1, 2).
117 Border(lipgloss.RoundedBorder()).
121 Border(lipgloss.RoundedBorder()).
118− BorderBackground(styles.Background).
122+ BorderBackground(t.Background()).
119− BorderForeground(styles.ForgroundDim).
123+ BorderForeground(t.TextMuted()).
120 Width(40).
124 Width(40).
121 Render("No commands available")
125 Render("No commands available")
122 }
126 }
@@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string {
154
158
155 for i := startIdx; i < endIdx; i++ {
159 for i := startIdx; i < endIdx; i++ {
156 cmd := c.commands[i]
160 cmd := c.commands[i]
157− itemStyle := styles.BaseStyle.Width(maxWidth)
161+ itemStyle := baseStyle.Width(maxWidth)
158− descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim)
162+ descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
159
163
160 if i == c.selectedIdx {
164 if i == c.selectedIdx {
161 itemStyle = itemStyle.
165 itemStyle = itemStyle.
162− Background(styles.PrimaryColor).
166+ Background(t.Primary()).
163− Foreground(styles.Background).
167+ Foreground(t.Background()).
164 Bold(true)
168 Bold(true)
165 descStyle = descStyle.
169 descStyle = descStyle.
166− Background(styles.PrimaryColor).
170+ Background(t.Primary()).
167− Foreground(styles.Background)
171+ Foreground(t.Background())
168 }
172 }
169
173
170 title := itemStyle.Padding(0, 1).Render(cmd.Title)
174 title := itemStyle.Padding(0, 1).Render(cmd.Title)
@@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string {
177 }
181 }
178 }
182 }
179
183
180− title := styles.BaseStyle.
184+ title := baseStyle.
181− Foreground(styles.PrimaryColor).
185+ Foreground(t.Primary()).
182 Bold(true).
186 Bold(true).
183 Width(maxWidth).
187 Width(maxWidth).
184 Padding(0, 1).
188 Padding(0, 1).
@@ -187,15 +191,15 @@ func (c *commandDialogCmp) View() string {
187 content := lipgloss.JoinVertical(
191 content := lipgloss.JoinVertical(
188 lipgloss.Left,
192 lipgloss.Left,
189 title,
193 title,
190− styles.BaseStyle.Width(maxWidth).Render(""),
194+ baseStyle.Width(maxWidth).Render(""),
191− styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
195+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
192− styles.BaseStyle.Width(maxWidth).Render(""),
196+ baseStyle.Width(maxWidth).Render(""),
193 )
197 )
194
198
195− return styles.BaseStyle.Padding(1, 2).
199+ return baseStyle.Padding(1, 2).
196 Border(lipgloss.RoundedBorder()).
200 Border(lipgloss.RoundedBorder()).
197− BorderBackground(styles.Background).
201+ BorderBackground(t.Background()).
198− BorderForeground(styles.ForgroundDim).
202+ BorderForeground(t.TextMuted()).
199 Width(lipgloss.Width(content) + 4).
203 Width(lipgloss.Width(content) + 4).
200 Render(content)
204 Render(content)
201}
205}
internal/tui/components/dialog/help.go+33−15
@@ -7,6 +7,7 @@ import (
7 tea "github.com/charmbracelet/bubbletea"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
10+ "github.com/opencode-ai/opencode/internal/tui/theme"
10)
11)
11
12
12type helpCmp struct {
13type helpCmp struct {
@@ -53,10 +54,21 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
53}
54}
54
55
55func (h *helpCmp) render() string {
56func (h *helpCmp) render() string {
57+ t := theme.CurrentTheme()
58+ baseStyle := styles.BaseStyle()
59+
56− helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0)
60+ helpKeyStyle := styles.Bold().
61+ Background(t.Background()).
62+ Foreground(t.Text()).
63+ Padding(0, 1, 0, 0)
64+
57− helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid)
65+ helpDescStyle := styles.Regular().
66+ Background(t.Background()).
67+ Foreground(t.TextMuted())
68+
58 // Compile list of bindings to render
69 // Compile list of bindings to render
59 bindings := removeDuplicateBindings(h.keys)
70 bindings := removeDuplicateBindings(h.keys)
71+
60 // Enumerate through each group of bindings, populating a series of
72 // Enumerate through each group of bindings, populating a series of
61 // pairs of columns, one for keys, one for descriptions
73 // pairs of columns, one for keys, one for descriptions
62 var (
74 var (
@@ -64,6 +76,7 @@ func (h *helpCmp) render() string {
64 width int
76 width int
65 rows = 10 - 2
77 rows = 10 - 2
66 )
78 )
79+
67 for i := 0; i < len(bindings); i += rows {
80 for i := 0; i < len(bindings); i += rows {
68 var (
81 var (
69 keys []string
82 keys []string
@@ -73,11 +86,12 @@ func (h *helpCmp) render() string {
73 keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
86 keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
74 descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
87 descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
75 }
88 }
89+
76 // Render pair of columns; beyond the first pair, render a three space
90 // Render pair of columns; beyond the first pair, render a three space
77 // left margin, in order to visually separate the pairs.
91 // left margin, in order to visually separate the pairs.
78 var cols []string
92 var cols []string
79 if len(pairs) > 0 {
93 if len(pairs) > 0 {
80− cols = []string{styles.BaseStyle.Render(" ")}
94+ cols = []string{baseStyle.Render(" ")}
81 }
95 }
82
96
83 maxDescWidth := 0
97 maxDescWidth := 0
@@ -89,7 +103,7 @@ func (h *helpCmp) render() string {
89 for i := range descs {
103 for i := range descs {
90 remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
104 remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
91 if remainingWidth > 0 {
105 if remainingWidth > 0 {
92− descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
106+ descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
93 }
107 }
94 }
108 }
95 maxKeyWidth := 0
109 maxKeyWidth := 0
@@ -101,7 +115,7 @@ func (h *helpCmp) render() string {
101 for i := range keys {
115 for i := range keys {
102 remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
116 remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
103 if remainingWidth > 0 {
117 if remainingWidth > 0 {
104− keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
118+ keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
105 }
119 }
106 }
120 }
107
121
@@ -110,7 +124,7 @@ func (h *helpCmp) render() string {
110 strings.Join(descs, "\n"),
124 strings.Join(descs, "\n"),
111 )
125 )
112
126
113− pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
127+ pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
114 // check whether it exceeds the maximum width avail (the width of the
128 // check whether it exceeds the maximum width avail (the width of the
115 // terminal, subtracting 2 for the borders).
129 // terminal, subtracting 2 for the borders).
116 width += lipgloss.Width(pair)
130 width += lipgloss.Width(pair)
@@ -130,9 +144,9 @@ func (h *helpCmp) render() string {
130 lipgloss.Left, // x
144 lipgloss.Left, // x
131 lipgloss.Top, // y
145 lipgloss.Top, // y
132 lastPair, // content
146 lastPair, // content
133− lipgloss.WithWhitespaceBackground(styles.Background), // background
147+ lipgloss.WithWhitespaceBackground(t.Background()),
134 ))
148 ))
135− content := styles.BaseStyle.Width(h.width).Render(
149+ content := baseStyle.Width(h.width).Render(
136 lipgloss.JoinHorizontal(
150 lipgloss.JoinHorizontal(
137 lipgloss.Top,
151 lipgloss.Top,
138 prefix...,
152 prefix...,
@@ -140,8 +154,9 @@ func (h *helpCmp) render() string {
140 )
154 )
141 return content
155 return content
142 }
156 }
157+
143 // Join pairs of columns and enclose in a border
158 // Join pairs of columns and enclose in a border
144− content := styles.BaseStyle.Width(h.width).Render(
159+ content := baseStyle.Width(h.width).Render(
145 lipgloss.JoinHorizontal(
160 lipgloss.JoinHorizontal(
146 lipgloss.Top,
161 lipgloss.Top,
147 pairs...,
162 pairs...,
@@ -151,22 +166,25 @@ func (h *helpCmp) render() string {
151}
166}
152
167
153func (h *helpCmp) View() string {
168func (h *helpCmp) View() string {
169+ t := theme.CurrentTheme()
170+ baseStyle := styles.BaseStyle()
171+
154 content := h.render()
172 content := h.render()
155− header := styles.BaseStyle.
173+ header := baseStyle.
156 Bold(true).
174 Bold(true).
157 Width(lipgloss.Width(content)).
175 Width(lipgloss.Width(content)).
158− Foreground(styles.PrimaryColor).
176+ Foreground(t.Primary()).
159 Render("Keyboard Shortcuts")
177 Render("Keyboard Shortcuts")
160
178
161− return styles.BaseStyle.Padding(1).
179+ return baseStyle.Padding(1).
162 Border(lipgloss.RoundedBorder()).
180 Border(lipgloss.RoundedBorder()).
163− BorderForeground(styles.ForgroundDim).
181+ BorderForeground(t.TextMuted()).
164 Width(h.width).
182 Width(h.width).
165− BorderBackground(styles.Background).
183+ BorderBackground(t.Background()).
166 Render(
184 Render(
167 lipgloss.JoinVertical(lipgloss.Center,
185 lipgloss.JoinVertical(lipgloss.Center,
168 header,
186 header,
169− styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
187+ baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
170 content,
188 content,
171 ),
189 ),
172 )
190 )
internal/tui/components/dialog/init.go+27−23
@@ -6,6 +6,7 @@ import (
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7
7
8 "github.com/opencode-ai/opencode/internal/tui/styles"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9+ "github.com/opencode-ai/opencode/internal/tui/theme"
9 "github.com/opencode-ai/opencode/internal/tui/util"
10 "github.com/opencode-ai/opencode/internal/tui/util"
10)
11)
11
12
@@ -92,55 +93,58 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
92
93
93// View implements tea.Model.
94// View implements tea.Model.
94func (m InitDialogCmp) View() string {
95func (m InitDialogCmp) View() string {
96+ t := theme.CurrentTheme()
97+ baseStyle := styles.BaseStyle()
98+
95 // Calculate width needed for content
99 // Calculate width needed for content
96 maxWidth := 60 // Width for explanation text
100 maxWidth := 60 // Width for explanation text
97
101
98− title := styles.BaseStyle.
102+ title := baseStyle.
99− Foreground(styles.PrimaryColor).
103+ Foreground(t.Primary()).
100 Bold(true).
104 Bold(true).
101 Width(maxWidth).
105 Width(maxWidth).
102 Padding(0, 1).
106 Padding(0, 1).
103 Render("Initialize Project")
107 Render("Initialize Project")
104
108
105− explanation := styles.BaseStyle.
109+ explanation := baseStyle.
106− Foreground(styles.Forground).
110+ Foreground(t.Text()).
107 Width(maxWidth).
111 Width(maxWidth).
108 Padding(0, 1).
112 Padding(0, 1).
109 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.")
113 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.")
110
114
111− question := styles.BaseStyle.
115+ question := baseStyle.
112− Foreground(styles.Forground).
116+ Foreground(t.Text()).
113 Width(maxWidth).
117 Width(maxWidth).
114 Padding(1, 1).
118 Padding(1, 1).
115 Render("Would you like to initialize this project?")
119 Render("Would you like to initialize this project?")
116
120
117 maxWidth = min(maxWidth, m.width-10)
121 maxWidth = min(maxWidth, m.width-10)
118− yesStyle := styles.BaseStyle
122+ yesStyle := baseStyle
119− noStyle := styles.BaseStyle
123+ noStyle := baseStyle
120
124
121 if m.selected == 0 {
125 if m.selected == 0 {
122 yesStyle = yesStyle.
126 yesStyle = yesStyle.
123− Background(styles.PrimaryColor).
127+ Background(t.Primary()).
124− Foreground(styles.Background).
128+ Foreground(t.Background()).
125 Bold(true)
129 Bold(true)
126 noStyle = noStyle.
130 noStyle = noStyle.
127− Background(styles.Background).
131+ Background(t.Background()).
128− Foreground(styles.PrimaryColor)
132+ Foreground(t.Primary())
129 } else {
133 } else {
130 noStyle = noStyle.
134 noStyle = noStyle.
131− Background(styles.PrimaryColor).
135+ Background(t.Primary()).
132− Foreground(styles.Background).
136+ Foreground(t.Background()).
133 Bold(true)
137 Bold(true)
134 yesStyle = yesStyle.
138 yesStyle = yesStyle.
135− Background(styles.Background).
139+ Background(t.Background()).
136− Foreground(styles.PrimaryColor)
140+ Foreground(t.Primary())
137 }
141 }
138
142
139 yes := yesStyle.Padding(0, 3).Render("Yes")
143 yes := yesStyle.Padding(0, 3).Render("Yes")
140 no := noStyle.Padding(0, 3).Render("No")
144 no := noStyle.Padding(0, 3).Render("No")
141
145
142− buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
146+ buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
143− buttons = styles.BaseStyle.
147+ buttons = baseStyle.
144 Width(maxWidth).
148 Width(maxWidth).
145 Padding(1, 0).
149 Padding(1, 0).
146 Render(buttons)
150 Render(buttons)
@@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string {
148 content := lipgloss.JoinVertical(
152 content := lipgloss.JoinVertical(
149 lipgloss.Left,
153 lipgloss.Left,
150 title,
154 title,
151− styles.BaseStyle.Width(maxWidth).Render(""),
155+ baseStyle.Width(maxWidth).Render(""),
152 explanation,
156 explanation,
153 question,
157 question,
154 buttons,
158 buttons,
155− styles.BaseStyle.Width(maxWidth).Render(""),
159+ baseStyle.Width(maxWidth).Render(""),
156 )
160 )
157
161
158− return styles.BaseStyle.Padding(1, 2).
162+ return baseStyle.Padding(1, 2).
159 Border(lipgloss.RoundedBorder()).
163 Border(lipgloss.RoundedBorder()).
160− BorderBackground(styles.Background).
164+ BorderBackground(t.Background()).
161− BorderForeground(styles.ForgroundDim).
165+ BorderForeground(t.TextMuted()).
162 Width(lipgloss.Width(content) + 4).
166 Width(lipgloss.Width(content) + 4).
163 Render(content)
167 Render(content)
164}
168}
internal/tui/components/dialog/models.go+18−11
@@ -12,6 +12,7 @@ import (
12 "github.com/opencode-ai/opencode/internal/llm/models"
12 "github.com/opencode-ai/opencode/internal/llm/models"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
15+ "github.com/opencode-ai/opencode/internal/tui/theme"
15 "github.com/opencode-ai/opencode/internal/tui/util"
16 "github.com/opencode-ai/opencode/internal/tui/util"
16)
17)
17
18
@@ -185,10 +186,13 @@ func (m *modelDialogCmp) switchProvider(offset int) {
185}
186}
186
187
187func (m *modelDialogCmp) View() string {
188func (m *modelDialogCmp) View() string {
189+ t := theme.CurrentTheme()
190+ baseStyle := styles.BaseStyle()
191+
188 // Capitalize first letter of provider name
192 // Capitalize first letter of provider name
189 providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
193 providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
190− title := styles.BaseStyle.
194+ title := baseStyle.
191− Foreground(styles.PrimaryColor).
195+ Foreground(t.Primary()).
192 Bold(true).
196 Bold(true).
193 Width(maxDialogWidth).
197 Width(maxDialogWidth).
194 Padding(0, 0, 1).
198 Padding(0, 0, 1).
@@ -199,10 +203,10 @@ func (m *modelDialogCmp) View() string {
199 modelItems := make([]string, 0, endIdx-m.scrollOffset)
203 modelItems := make([]string, 0, endIdx-m.scrollOffset)
200
204
201 for i := m.scrollOffset; i < endIdx; i++ {
205 for i := m.scrollOffset; i < endIdx; i++ {
202− itemStyle := styles.BaseStyle.Width(maxDialogWidth)
206+ itemStyle := baseStyle.Width(maxDialogWidth)
203 if i == m.selectedIdx {
207 if i == m.selectedIdx {
204− itemStyle = itemStyle.Background(styles.PrimaryColor).
208+ itemStyle = itemStyle.Background(t.Primary()).
205− Foreground(styles.Background).Bold(true)
209+ Foreground(t.Background()).Bold(true)
206 }
210 }
207 modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
211 modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
208 }
212 }
@@ -212,14 +216,14 @@ func (m *modelDialogCmp) View() string {
212 content := lipgloss.JoinVertical(
216 content := lipgloss.JoinVertical(
213 lipgloss.Left,
217 lipgloss.Left,
214 title,
218 title,
215− styles.BaseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
219+ baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
216 scrollIndicator,
220 scrollIndicator,
217 )
221 )
218
222
219− return styles.BaseStyle.Padding(1, 2).
223+ return baseStyle.Padding(1, 2).
220 Border(lipgloss.RoundedBorder()).
224 Border(lipgloss.RoundedBorder()).
221− BorderBackground(styles.Background).
225+ BorderBackground(t.Background()).
222− BorderForeground(styles.ForgroundDim).
226+ BorderForeground(t.TextMuted()).
223 Width(lipgloss.Width(content) + 4).
227 Width(lipgloss.Width(content) + 4).
224 Render(content)
228 Render(content)
225}
229}
@@ -249,8 +253,11 @@ func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
249 return ""
253 return ""
250 }
254 }
251
255
252− return styles.BaseStyle.
256+ t := theme.CurrentTheme()
257+ baseStyle := styles.BaseStyle()
258+
259+ return baseStyle.
253− Foreground(styles.PrimaryColor).
260+ Foreground(t.Primary()).
254 Width(maxWidth).
261 Width(maxWidth).
255 Align(lipgloss.Right).
262 Align(lipgloss.Right).
256 Bold(true).
263 Bold(true).
internal/tui/components/dialog/permission.go+69−53
@@ -7,13 +7,13 @@ import (
7 "github.com/charmbracelet/bubbles/key"
7 "github.com/charmbracelet/bubbles/key"
8 "github.com/charmbracelet/bubbles/viewport"
8 "github.com/charmbracelet/bubbles/viewport"
9 tea "github.com/charmbracelet/bubbletea"
9 tea "github.com/charmbracelet/bubbletea"
10− "github.com/charmbracelet/glamour"
11 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/lipgloss"
12 "github.com/opencode-ai/opencode/internal/diff"
11 "github.com/opencode-ai/opencode/internal/diff"
13 "github.com/opencode-ai/opencode/internal/llm/tools"
12 "github.com/opencode-ai/opencode/internal/llm/tools"
14 "github.com/opencode-ai/opencode/internal/permission"
13 "github.com/opencode-ai/opencode/internal/permission"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/styles"
15 "github.com/opencode-ai/opencode/internal/tui/styles"
16+ "github.com/opencode-ai/opencode/internal/tui/theme"
17 "github.com/opencode-ai/opencode/internal/tui/util"
17 "github.com/opencode-ai/opencode/internal/tui/util"
18)
18)
19
19
@@ -149,25 +149,28 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
149}
149}
150
150
151func (p *permissionDialogCmp) renderButtons() string {
151func (p *permissionDialogCmp) renderButtons() string {
152+ t := theme.CurrentTheme()
153+ baseStyle := styles.BaseStyle()
154+
152− allowStyle := styles.BaseStyle
155+ allowStyle := baseStyle
153− allowSessionStyle := styles.BaseStyle
156+ allowSessionStyle := baseStyle
154− denyStyle := styles.BaseStyle
157+ denyStyle := baseStyle
155− spacerStyle := styles.BaseStyle.Background(styles.Background)
158+ spacerStyle := baseStyle.Background(t.Background())
156
159
157 // Style the selected button
160 // Style the selected button
158 switch p.selectedOption {
161 switch p.selectedOption {
159 case 0:
162 case 0:
160− allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
163+ allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
161− allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
164+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
162− denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
165+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
163 case 1:
166 case 1:
164− allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
167+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
165− allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
168+ allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
166− denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
169+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
167 case 2:
170 case 2:
168− allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
171+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
169− allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
172+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
170− denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
173+ denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
171 }
174 }
172
175
173 allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
176 allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
@@ -192,15 +195,18 @@ func (p *permissionDialogCmp) renderButtons() string {
192}
195}
193
196
194func (p *permissionDialogCmp) renderHeader() string {
197func (p *permissionDialogCmp) renderHeader() string {
198+ t := theme.CurrentTheme()
199+ baseStyle := styles.BaseStyle()
200+
195− toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
201+ toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
196− toolValue := styles.BaseStyle.
202+ toolValue := baseStyle.
197− Foreground(styles.Forground).
203+ Foreground(t.Text()).
198 Width(p.width - lipgloss.Width(toolKey)).
204 Width(p.width - lipgloss.Width(toolKey)).
199 Render(fmt.Sprintf(": %s", p.permission.ToolName))
205 Render(fmt.Sprintf(": %s", p.permission.ToolName))
200
206
201− pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
207+ pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
202− pathValue := styles.BaseStyle.
208+ pathValue := baseStyle.
203− Foreground(styles.Forground).
209+ Foreground(t.Text()).
204 Width(p.width - lipgloss.Width(pathKey)).
210 Width(p.width - lipgloss.Width(pathKey)).
205 Render(fmt.Sprintf(": %s", p.permission.Path))
211 Render(fmt.Sprintf(": %s", p.permission.Path))
206
212
@@ -210,45 +216,45 @@ func (p *permissionDialogCmp) renderHeader() string {
210 toolKey,
216 toolKey,
211 toolValue,
217 toolValue,
212 ),
218 ),
213− styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
219+ baseStyle.Render(strings.Repeat(" ", p.width)),
214 lipgloss.JoinHorizontal(
220 lipgloss.JoinHorizontal(
215 lipgloss.Left,
221 lipgloss.Left,
216 pathKey,
222 pathKey,
217 pathValue,
223 pathValue,
218 ),
224 ),
219− styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
225+ baseStyle.Render(strings.Repeat(" ", p.width)),
220 }
226 }
221
227
222 // Add tool-specific header information
228 // Add tool-specific header information
223 switch p.permission.ToolName {
229 switch p.permission.ToolName {
224 case tools.BashToolName:
230 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"))
226 case tools.EditToolName:
232 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"))
228 case tools.WriteToolName:
234 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"))
230 case tools.FetchToolName:
236 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"))
232 }
238 }
233
239
234− return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
240+ return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
235}
241}
236
242
237func (p *permissionDialogCmp) renderBashContent() string {
243func (p *permissionDialogCmp) renderBashContent() string {
244+ t := theme.CurrentTheme()
245+ baseStyle := styles.BaseStyle()
246+
238 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
247 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
239 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
248 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
240
249
241 // Use the cache for markdown rendering
250 // Use the cache for markdown rendering
242 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
251 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
243− r, _ := glamour.NewTermRenderer(
252+ r := styles.GetMarkdownRenderer(p.width-10)
244− glamour.WithStyles(styles.MarkdownTheme(true)),
245− glamour.WithWordWrap(p.width-10),
246− )
247 s, err := r.Render(content)
253 s, err := r.Render(content)
248− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
254+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
249 })
255 })
250
256
251− finalContent := styles.BaseStyle.
257+ finalContent := baseStyle.
252 Width(p.contentViewPort.Width).
258 Width(p.contentViewPort.Width).
253 Render(renderedContent)
259 Render(renderedContent)
254 p.contentViewPort.SetContent(finalContent)
260 p.contentViewPort.SetContent(finalContent)
@@ -295,39 +301,45 @@ func (p *permissionDialogCmp) renderWriteContent() string {
295}
301}
296
302
297func (p *permissionDialogCmp) renderFetchContent() string {
303func (p *permissionDialogCmp) renderFetchContent() string {
304+ t := theme.CurrentTheme()
305+ baseStyle := styles.BaseStyle()
306+
298 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
307 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
299 content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
308 content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
300
309
301 // Use the cache for markdown rendering
310 // Use the cache for markdown rendering
302 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
311 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
303− r, _ := glamour.NewTermRenderer(
312+ r := styles.GetMarkdownRenderer(p.width-10)
304− glamour.WithStyles(styles.MarkdownTheme(true)),
305− glamour.WithWordWrap(p.width-10),
306− )
307 s, err := r.Render(content)
313 s, err := r.Render(content)
308− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
314+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
309 })
315 })
310
316
317+ finalContent := baseStyle.
318+ Width(p.contentViewPort.Width).
319+ Render(renderedContent)
311− p.contentViewPort.SetContent(renderedContent)
320+ p.contentViewPort.SetContent(finalContent)
312 return p.styleViewport()
321 return p.styleViewport()
313 }
322 }
314 return ""
323 return ""
315}
324}
316
325
317func (p *permissionDialogCmp) renderDefaultContent() string {
326func (p *permissionDialogCmp) renderDefaultContent() string {
327+ t := theme.CurrentTheme()
328+ baseStyle := styles.BaseStyle()
329+
318 content := p.permission.Description
330 content := p.permission.Description
319
331
320 // Use the cache for markdown rendering
332 // Use the cache for markdown rendering
321 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
333 renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
322− r, _ := glamour.NewTermRenderer(
334+ r := styles.GetMarkdownRenderer(p.width-10)
323− glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
324− glamour.WithWordWrap(p.width-10),
325− )
326 s, err := r.Render(content)
335 s, err := r.Render(content)
327− return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
336+ return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
328 })
337 })
329
338
339+ finalContent := baseStyle.
340+ Width(p.contentViewPort.Width).
341+ Render(renderedContent)
330− p.contentViewPort.SetContent(renderedContent)
342+ p.contentViewPort.SetContent(finalContent)
331
343
332 if renderedContent == "" {
344 if renderedContent == "" {
333 return ""
345 return ""
@@ -337,17 +349,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
337}
349}
338
350
339func (p *permissionDialogCmp) styleViewport() string {
351func (p *permissionDialogCmp) styleViewport() string {
352+ t := theme.CurrentTheme()
340 contentStyle := lipgloss.NewStyle().
353 contentStyle := lipgloss.NewStyle().
341− Background(styles.Background)
354+ Background(t.Background())
342
355
343 return contentStyle.Render(p.contentViewPort.View())
356 return contentStyle.Render(p.contentViewPort.View())
344}
357}
345
358
346func (p *permissionDialogCmp) render() string {
359func (p *permissionDialogCmp) render() string {
360+ t := theme.CurrentTheme()
347− title := styles.BaseStyle.
361+ baseStyle := styles.BaseStyle()
362+
363+ title := baseStyle.
348 Bold(true).
364 Bold(true).
349 Width(p.width - 4).
365 Width(p.width - 4).
350− Foreground(styles.PrimaryColor).
366+ Foreground(t.Primary()).
351 Render("Permission Required")
367 Render("Permission Required")
352 // Render header
368 // Render header
353 headerContent := p.renderHeader()
369 headerContent := p.renderHeader()
@@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string {
378 content := lipgloss.JoinVertical(
394 content := lipgloss.JoinVertical(
379 lipgloss.Top,
395 lipgloss.Top,
380 title,
396 title,
381− styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
397+ baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
382 headerContent,
398 headerContent,
383 contentFinal,
399 contentFinal,
384 buttons,
400 buttons,
385− styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
401+ baseStyle.Render(strings.Repeat(" ", p.width-4)),
386 )
402 )
387
403
388− return styles.BaseStyle.
404+ return baseStyle.
389 Padding(1, 0, 0, 1).
405 Padding(1, 0, 0, 1).
390 Border(lipgloss.RoundedBorder()).
406 Border(lipgloss.RoundedBorder()).
391− BorderBackground(styles.Background).
407+ BorderBackground(t.Background()).
392− BorderForeground(styles.ForgroundDim).
408+ BorderForeground(t.TextMuted()).
393 Width(p.width).
409 Width(p.width).
394 Height(p.height).
410 Height(p.height).
395 Render(
411 Render(
internal/tui/components/dialog/quit.go+15−11
@@ -8,6 +8,7 @@ import (
8 "github.com/charmbracelet/lipgloss"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/opencode-ai/opencode/internal/tui/layout"
9 "github.com/opencode-ai/opencode/internal/tui/layout"
10 "github.com/opencode-ai/opencode/internal/tui/styles"
10 "github.com/opencode-ai/opencode/internal/tui/styles"
11+ "github.com/opencode-ai/opencode/internal/tui/theme"
11 "github.com/opencode-ai/opencode/internal/tui/util"
12 "github.com/opencode-ai/opencode/internal/tui/util"
12)
13)
13
14
@@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81}
82}
82
83
83func (q *quitDialogCmp) View() string {
84func (q *quitDialogCmp) View() string {
85+ t := theme.CurrentTheme()
86+ baseStyle := styles.BaseStyle()
87+
84− yesStyle := styles.BaseStyle
88+ yesStyle := baseStyle
85− noStyle := styles.BaseStyle
89+ noStyle := baseStyle
86− spacerStyle := styles.BaseStyle.Background(styles.Background)
90+ spacerStyle := baseStyle.Background(t.Background())
87
91
88 if q.selectedNo {
92 if q.selectedNo {
89− noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
93+ noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
90− yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
94+ yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
91 } else {
95 } else {
92− yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
96+ yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
93− noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
97+ noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
94 }
98 }
95
99
96 yesButton := yesStyle.Padding(0, 1).Render("Yes")
100 yesButton := yesStyle.Padding(0, 1).Render("Yes")
@@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string {
104 buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
108 buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
105 }
109 }
106
110
107− content := styles.BaseStyle.Render(
111+ content := baseStyle.Render(
108 lipgloss.JoinVertical(
112 lipgloss.JoinVertical(
109 lipgloss.Center,
113 lipgloss.Center,
110 question,
114 question,
@@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string {
113 ),
117 ),
114 )
118 )
115
119
116− return styles.BaseStyle.Padding(1, 2).
120+ return baseStyle.Padding(1, 2).
117 Border(lipgloss.RoundedBorder()).
121 Border(lipgloss.RoundedBorder()).
118− BorderBackground(styles.Background).
122+ BorderBackground(t.Background()).
119− BorderForeground(styles.ForgroundDim).
123+ BorderForeground(t.TextMuted()).
120 Width(lipgloss.Width(content) + 4).
124 Width(lipgloss.Width(content) + 4).
121 Render(content)
125 Render(content)
122}
126}
internal/tui/components/dialog/session.go+18−14
@@ -7,6 +7,7 @@ import (
7 "github.com/opencode-ai/opencode/internal/session"
7 "github.com/opencode-ai/opencode/internal/session"
8 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/layout"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/styles"
10+ "github.com/opencode-ai/opencode/internal/tui/theme"
10 "github.com/opencode-ai/opencode/internal/tui/util"
11 "github.com/opencode-ai/opencode/internal/tui/util"
11)
12)
12
13
@@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
105}
106}
106
107
107func (s *sessionDialogCmp) View() string {
108func (s *sessionDialogCmp) View() string {
109+ t := theme.CurrentTheme()
110+ baseStyle := styles.BaseStyle()
111+
108 if len(s.sessions) == 0 {
112 if len(s.sessions) == 0 {
109− return styles.BaseStyle.Padding(1, 2).
113+ return baseStyle.Padding(1, 2).
110 Border(lipgloss.RoundedBorder()).
114 Border(lipgloss.RoundedBorder()).
111− BorderBackground(styles.Background).
115+ BorderBackground(t.Background()).
112− BorderForeground(styles.ForgroundDim).
116+ BorderForeground(t.TextMuted()).
113 Width(40).
117 Width(40).
114 Render("No sessions available")
118 Render("No sessions available")
115 }
119 }
@@ -146,20 +150,20 @@ func (s *sessionDialogCmp) View() string {
146
150
147 for i := startIdx; i < endIdx; i++ {
151 for i := startIdx; i < endIdx; i++ {
148 sess := s.sessions[i]
152 sess := s.sessions[i]
149− itemStyle := styles.BaseStyle.Width(maxWidth)
153+ itemStyle := baseStyle.Width(maxWidth)
150
154
151 if i == s.selectedIdx {
155 if i == s.selectedIdx {
152 itemStyle = itemStyle.
156 itemStyle = itemStyle.
153− Background(styles.PrimaryColor).
157+ Background(t.Primary()).
154− Foreground(styles.Background).
158+ Foreground(t.Background()).
155 Bold(true)
159 Bold(true)
156 }
160 }
157
161
158 sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
162 sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
159 }
163 }
160
164
161− title := styles.BaseStyle.
165+ title := baseStyle.
162− Foreground(styles.PrimaryColor).
166+ Foreground(t.Primary()).
163 Bold(true).
167 Bold(true).
164 Width(maxWidth).
168 Width(maxWidth).
165 Padding(0, 1).
169 Padding(0, 1).
@@ -168,15 +172,15 @@ func (s *sessionDialogCmp) View() string {
168 content := lipgloss.JoinVertical(
172 content := lipgloss.JoinVertical(
169 lipgloss.Left,
173 lipgloss.Left,
170 title,
174 title,
171− styles.BaseStyle.Width(maxWidth).Render(""),
175+ baseStyle.Width(maxWidth).Render(""),
172− styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
176+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
173− styles.BaseStyle.Width(maxWidth).Render(""),
177+ baseStyle.Width(maxWidth).Render(""),
174 )
178 )
175
179
176− return styles.BaseStyle.Padding(1, 2).
180+ return baseStyle.Padding(1, 2).
177 Border(lipgloss.RoundedBorder()).
181 Border(lipgloss.RoundedBorder()).
178− BorderBackground(styles.Background).
182+ BorderBackground(t.Background()).
179− BorderForeground(styles.ForgroundDim).
183+ BorderForeground(t.TextMuted()).
180 Width(lipgloss.Width(content) + 4).
184 Width(lipgloss.Width(content) + 4).
181 Render(content)
185 Render(content)
182}
186}
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 (
12 "github.com/opencode-ai/opencode/internal/logging"
12 "github.com/opencode-ai/opencode/internal/logging"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
15+ "github.com/opencode-ai/opencode/internal/tui/theme"
15)
16)
16
17
17type DetailComponent interface {
18type DetailComponent interface {
@@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
49
50
50func (i *detailCmp) updateContent() {
51func (i *detailCmp) updateContent() {
51 var content strings.Builder
52 var content strings.Builder
53+ t := theme.CurrentTheme()
52
54
53 // Format the header with timestamp and level
55 // Format the header with timestamp and level
54− timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
56+ timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
55 levelStyle := getLevelStyle(i.currentLog.Level)
57 levelStyle := getLevelStyle(i.currentLog.Level)
56
58
57 header := lipgloss.JoinHorizontal(
59 header := lipgloss.JoinHorizontal(
@@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() {
65 content.WriteString("\n\n")
67 content.WriteString("\n\n")
66
68
67 // Message with styling
69 // Message with styling
68− messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
70+ messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
69 content.WriteString(messageStyle.Render("Message:"))
71 content.WriteString(messageStyle.Render("Message:"))
70 content.WriteString("\n")
72 content.WriteString("\n")
71 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
73 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
@@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() {
73
75
74 // Attributes section
76 // Attributes section
75 if len(i.currentLog.Attributes) > 0 {
77 if len(i.currentLog.Attributes) > 0 {
76− attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
78+ attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
77 content.WriteString(attrHeaderStyle.Render("Attributes:"))
79 content.WriteString(attrHeaderStyle.Render("Attributes:"))
78 content.WriteString("\n")
80 content.WriteString("\n")
79
81
80 // Create a table-like display for attributes
82 // Create a table-like display for attributes
81− keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
83+ keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
82− valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
84+ valueStyle := lipgloss.NewStyle().Foreground(t.Text())
83
85
84 for _, attr := range i.currentLog.Attributes {
86 for _, attr := range i.currentLog.Attributes {
85 attrLine := fmt.Sprintf("%s: %s",
87 attrLine := fmt.Sprintf("%s: %s",
@@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() {
96
98
97func getLevelStyle(level string) lipgloss.Style {
99func getLevelStyle(level string) lipgloss.Style {
98 style := lipgloss.NewStyle().Bold(true)
100 style := lipgloss.NewStyle().Bold(true)
99−
101+ t := theme.CurrentTheme()
102+
100 switch strings.ToLower(level) {
103 switch strings.ToLower(level) {
101 case "info":
104 case "info":
102− return style.Foreground(styles.Blue)
105+ return style.Foreground(t.Info())
103 case "warn", "warning":
106 case "warn", "warning":
104− return style.Foreground(styles.Warning)
107+ return style.Foreground(t.Warning())
105 case "error", "err":
108 case "error", "err":
106− return style.Foreground(styles.Error)
109+ return style.Foreground(t.Error())
107 case "debug":
110 case "debug":
108− return style.Foreground(styles.Green)
111+ return style.Foreground(t.Success())
109 default:
112 default:
110− return style.Foreground(styles.Text)
113+ return style.Foreground(t.Text())
111 }
114 }
112}
115}
113
116
114func (i *detailCmp) View() string {
117func (i *detailCmp) View() string {
118+ t := theme.CurrentTheme()
115− return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background)
119+ return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
116}
120}
117
121
118func (i *detailCmp) GetSize() (int, int) {
122func (i *detailCmp) GetSize() (int, int) {
internal/tui/components/logs/table.go+7−4
@@ -11,6 +11,7 @@ import (
11 "github.com/opencode-ai/opencode/internal/pubsub"
11 "github.com/opencode-ai/opencode/internal/pubsub"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/styles"
13 "github.com/opencode-ai/opencode/internal/tui/styles"
14+ "github.com/opencode-ai/opencode/internal/tui/theme"
14 "github.com/opencode-ai/opencode/internal/tui/util"
15 "github.com/opencode-ai/opencode/internal/tui/util"
15)
16)
16
17
@@ -61,7 +62,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61}
62}
62
63
63func (i *tableCmp) View() string {
64func (i *tableCmp) View() string {
65+ t := theme.CurrentTheme()
66+ defaultStyles := table.DefaultStyles()
67+ defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
68+ i.table.SetStyles(defaultStyles)
64− return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background)
69+ return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
65}
70}
66
71
67func (i *tableCmp) GetSize() (int, int) {
72func (i *tableCmp) GetSize() (int, int) {
@@ -121,11 +126,9 @@ func NewLogsTable() TableComponent {
121 {Title: "Message", Width: 10},
126 {Title: "Message", Width: 10},
122 {Title: "Attributes", Width: 10},
127 {Title: "Attributes", Width: 10},
123 }
128 }
124− defaultStyles := table.DefaultStyles()
129+
125− defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary)
126 tableModel := table.New(
130 tableModel := table.New(
127 table.WithColumns(columns),
131 table.WithColumns(columns),
128− table.WithStyles(defaultStyles),
129 )
132 )
130 tableModel.Focus()
133 tableModel.Focus()
131 return &tableCmp{
134 return &tableCmp{
internal/tui/layout/container.go+8−29
@@ -4,7 +4,7 @@ import (
4 "github.com/charmbracelet/bubbles/key"
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7− "github.com/opencode-ai/opencode/internal/tui/styles"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
8)
8)
9
9
10type Container interface {
10type Container interface {
@@ -29,9 +29,6 @@ type container struct {
29 borderBottom bool
29 borderBottom bool
30 borderLeft bool
30 borderLeft bool
31 borderStyle lipgloss.Border
31 borderStyle lipgloss.Border
32− borderColor lipgloss.TerminalColor
33−
34− backgroundColor lipgloss.TerminalColor
35}
32}
36
33
37func (c *container) Init() tea.Cmd {
34func (c *container) Init() tea.Cmd {
@@ -45,13 +42,12 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
45}
42}
46
43
47func (c *container) View() string {
44func (c *container) View() string {
45+ t := theme.CurrentTheme()
48 style := lipgloss.NewStyle()
46 style := lipgloss.NewStyle()
49 width := c.width
47 width := c.width
50 height := c.height
48 height := c.height
51− // Apply background color if specified
49+
52− if c.backgroundColor != nil {
53− style = style.Background(c.backgroundColor)
50+ style = style.Background(t.Background())
54− }
55
51
56 // Apply border if any side is enabled
52 // Apply border if any side is enabled
57 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
53 if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
@@ -69,11 +65,7 @@ func (c *container) View() string {
69 width--
65 width--
70 }
66 }
71 style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
67 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)
68+ style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
76− }
77 }
69 }
78 style = style.
70 style = style.
79 Width(width).
71 Width(width).
@@ -132,11 +124,10 @@ func (c *container) BindingKeys() []key.Binding {
132type ContainerOption func(*container)
124type ContainerOption func(*container)
133
125
134func NewContainer(content tea.Model, options ...ContainerOption) Container {
126func NewContainer(content tea.Model, options ...ContainerOption) Container {
127+
135 c := &container{
128 c := &container{
136− content: content,
129+ content: content,
137− borderColor: styles.BorderColor,
138− borderStyle: lipgloss.NormalBorder(),
130+ borderStyle: lipgloss.NormalBorder(),
139− backgroundColor: styles.Background,
140 }
131 }
141
132
142 for _, option := range options {
133 for _, option := range options {
@@ -201,12 +192,6 @@ func WithBorderStyle(style lipgloss.Border) ContainerOption {
201 }
192 }
202}
193}
203
194
204−func WithBorderColor(color lipgloss.TerminalColor) ContainerOption {
205− return func(c *container) {
206− c.borderColor = color
207− }
208−}
209−
210func WithRoundedBorder() ContainerOption {
195func WithRoundedBorder() ContainerOption {
211 return WithBorderStyle(lipgloss.RoundedBorder())
196 return WithBorderStyle(lipgloss.RoundedBorder())
212}
197}
@@ -218,9 +203,3 @@ func WithThickBorder() ContainerOption {
218func WithDoubleBorder() ContainerOption {
203func WithDoubleBorder() ContainerOption {
219 return WithBorderStyle(lipgloss.DoubleBorder())
204 return WithBorderStyle(lipgloss.DoubleBorder())
220}
205}
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 (
9 "github.com/muesli/reflow/truncate"
9 "github.com/muesli/reflow/truncate"
10 "github.com/muesli/termenv"
10 "github.com/muesli/termenv"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
11 "github.com/opencode-ai/opencode/internal/tui/styles"
12+ "github.com/opencode-ai/opencode/internal/tui/theme"
12 "github.com/opencode-ai/opencode/internal/tui/util"
13 "github.com/opencode-ai/opencode/internal/tui/util"
13)
14)
14
15
@@ -43,12 +44,15 @@ func PlaceOverlay(
43 fgHeight := len(fgLines)
44 fgHeight := len(fgLines)
44
45
45 if shadow {
46 if shadow {
47+ t := theme.CurrentTheme()
48+ baseStyle := styles.BaseStyle()
49+
46 var shadowbg string = ""
50 var shadowbg string = ""
47 shadowchar := lipgloss.NewStyle().
51 shadowchar := lipgloss.NewStyle().
48− Background(styles.BackgroundDarker).
52+ Background(t.BackgroundDarker()).
49− Foreground(styles.Background).
53+ Foreground(t.Background()).
50 Render("░")
54 Render("░")
51− bgchar := styles.BaseStyle.Render(" ")
55+ bgchar := baseStyle.Render(" ")
52 for i := 0; i <= fgHeight; i++ {
56 for i := 0; i <= fgHeight; i++ {
53 if i == 0 {
57 if i == 0 {
54 shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
58 shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
internal/tui/layout/split.go+8−14
@@ -4,7 +4,7 @@ import (
4 "github.com/charmbracelet/bubbles/key"
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7− "github.com/opencode-ai/opencode/internal/tui/styles"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
8)
8)
9
9
10type SplitPaneLayout interface {
10type SplitPaneLayout interface {
@@ -29,8 +29,6 @@ type splitPaneLayout struct {
29 rightPanel Container
29 rightPanel Container
30 leftPanel Container
30 leftPanel Container
31 bottomPanel Container
31 bottomPanel Container
32−
33− backgroundColor lipgloss.TerminalColor
34}
32}
35
33
36type SplitPaneOption func(*splitPaneLayout)
34type SplitPaneOption func(*splitPaneLayout)
@@ -113,11 +111,13 @@ func (s *splitPaneLayout) View() string {
113 finalView = topSection
111 finalView = topSection
114 }
112 }
115
113
116− if s.backgroundColor != nil && finalView != "" {
114+ if finalView != "" {
115+ t := theme.CurrentTheme()
116+
117 style := lipgloss.NewStyle().
117 style := lipgloss.NewStyle().
118 Width(s.width).
118 Width(s.width).
119 Height(s.height).
119 Height(s.height).
120− Background(s.backgroundColor)
120+ Background(t.Background())
121
121
122 return style.Render(finalView)
122 return style.Render(finalView)
123 }
123 }
@@ -241,10 +241,10 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding {
241}
241}
242
242
243func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
243func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
244+
244 layout := &splitPaneLayout{
245 layout := &splitPaneLayout{
245− ratio: 0.7,
246+ ratio: 0.7,
246− verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
247+ verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
247− backgroundColor: styles.Background,
248 }
248 }
249 for _, option := range options {
249 for _, option := range options {
250 option(layout)
250 option(layout)
@@ -270,12 +270,6 @@ func WithRatio(ratio float64) SplitPaneOption {
270 }
270 }
271}
271}
272
272
273−func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
274− return func(s *splitPaneLayout) {
275− s.backgroundColor = color
276− }
277−}
278−
279func WithBottomPanel(panel Container) SplitPaneOption {
273func WithBottomPanel(panel Container) SplitPaneOption {
280 return func(s *splitPaneLayout) {
274 return func(s *splitPaneLayout) {
281 s.bottomPanel = panel
275 s.bottomPanel = panel
internal/tui/page/chat.go−1
@@ -142,7 +142,6 @@ func NewChatPage(app *app.App) tea.Model {
142 chat.NewMessagesCmp(app),
142 chat.NewMessagesCmp(app),
143 layout.WithPadding(1, 1, 0, 1),
143 layout.WithPadding(1, 1, 0, 1),
144 )
144 )
145−
146 editorContainer := layout.NewContainer(
145 editorContainer := layout.NewContainer(
147 chat.NewEditorCmp(app),
146 chat.NewEditorCmp(app),
148 layout.WithBorder(true, false, false, false),
147 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) {
42}
42}
43
43
44func (p *logsPage) View() string {
44func (p *logsPage) View() string {
45− style := styles.BaseStyle.Width(p.width).Height(p.height)
45+ style := styles.BaseStyle().Width(p.width).Height(p.height)
46 return style.Render(lipgloss.JoinVertical(lipgloss.Top,
46 return style.Render(lipgloss.JoinVertical(lipgloss.Top,
47 p.table.View(),
47 p.table.View(),
48 p.details.View(),
48 p.details.View(),
@@ -77,7 +77,7 @@ func (p *logsPage) Init() tea.Cmd {
77
77
78func NewLogsPage() LogPage {
78func NewLogsPage() LogPage {
79 return &logsPage{
79 return &logsPage{
80− table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
80+ table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
81− details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
81+ details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
82 }
82 }
83}
83}
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 @@
1package styles
1package styles
2
2
3import (
3import (
4+ "github.com/charmbracelet/glamour"
4 "github.com/charmbracelet/glamour/ansi"
5 "github.com/charmbracelet/glamour/ansi"
5 "github.com/charmbracelet/lipgloss"
6 "github.com/charmbracelet/lipgloss"
7+ "github.com/opencode-ai/opencode/internal/tui/theme"
6)
8)
7
9
8const defaultMargin = 1
10const defaultMargin = 1
@@ -12,930 +14,271 @@ func boolPtr(b bool) *bool { return &b }
12func stringPtr(s string) *string { return &s }
14func stringPtr(s string) *string { return &s }
13func uintPtr(u uint) *uint { return &u }
15func uintPtr(u uint) *uint { return &u }
14
16
15−// CatppuccinMarkdownStyle is the Catppuccin Mocha style for Glamour markdown rendering.
17+// returns a glamour TermRenderer configured with the current theme
16−func CatppuccinMarkdownStyle() ansi.StyleConfig {
18+func GetMarkdownRenderer(width int) *glamour.TermRenderer {
17− isDark := lipgloss.HasDarkBackground()
19+ r, _ := glamour.NewTermRenderer(
18− if isDark {
20+ glamour.WithStyles(generateMarkdownStyleConfig()),
19− return catppuccinDark
21+ glamour.WithWordWrap(width),
20− }
22+ )
21− return catppuccinLight
23+ return r
22}
24}
23
25
24−var catppuccinDark = ansi.StyleConfig{
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{
25− Document: ansi.StyleBlock{
32+ 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{
156 StylePrimitive: ansi.StylePrimitive{
33 StylePrimitive: ansi.StylePrimitive{
157− Prefix: " ",
34+ BlockPrefix: "",
158− Color: stringPtr(dark.Text().Hex),
35+ BlockSuffix: "",
36+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
159 },
37 },
160−
161 Margin: uintPtr(defaultMargin),
38 Margin: uintPtr(defaultMargin),
162 },
39 },
163− Chroma: &ansi.Chroma{
40+ BlockQuote: ansi.StyleBlock{
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{
41+ StylePrimitive: ansi.StylePrimitive{
231− Color: stringPtr(dark.Yellow().Hex),
42+ Color: stringPtr(adaptiveColorToString(t.MarkdownBlockQuote())),
232 Italic: boolPtr(true),
43 Italic: boolPtr(true),
44+ Prefix: "┃ ",
233 },
45 },
234− GenericInserted: ansi.StylePrimitive{
46+ Indent: uintPtr(1),
235− Color: stringPtr(dark.Green().Hex),
47+ IndentToken: stringPtr(BaseStyle().Render(" ")),
236− },
48+ },
237− GenericStrong: ansi.StylePrimitive{
49+ List: ansi.StyleList{
238− Color: stringPtr(dark.Peach().Hex),
50+ LevelIndent: defaultMargin,
239− Bold: boolPtr(true),
51+ StyleBlock: ansi.StyleBlock{
240− },
52+ IndentToken: stringPtr(BaseStyle().Render(" ")),
241− GenericSubheading: ansi.StylePrimitive{
53+ StylePrimitive: ansi.StylePrimitive{
242− Color: stringPtr(dark.Mauve().Hex),
54+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
55+ },
243 },
56 },
244 },
57 },
245− },
246− Table: ansi.StyleTable{
247− StyleBlock: ansi.StyleBlock{
58+ Heading: ansi.StyleBlock{
248 StylePrimitive: ansi.StylePrimitive{
59 StylePrimitive: ansi.StylePrimitive{
249− BlockPrefix: "\n",
250 BlockSuffix: "\n",
60 BlockSuffix: "\n",
61+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
62+ Bold: boolPtr(true),
251 },
63 },
252 },
64 },
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{
284 StylePrimitive: ansi.StylePrimitive{
66 StylePrimitive: ansi.StylePrimitive{
285− Color: stringPtr(light.Text().Hex),
67+ Prefix: "# ",
68+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
69+ Bold: boolPtr(true),
286 },
70 },
287 },
71 },
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{
394 StylePrimitive: ansi.StylePrimitive{
73 StylePrimitive: ansi.StylePrimitive{
395− Prefix: " ",
74+ Prefix: "## ",
396− Color: stringPtr(light.Text().Hex),
75+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
76+ Bold: boolPtr(true),
397 },
77 },
398−
399− Margin: uintPtr(defaultMargin),
400 },
78 },
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{
486 StylePrimitive: ansi.StylePrimitive{
80 StylePrimitive: ansi.StylePrimitive{
487− BlockPrefix: "\n",
81+ Prefix: "### ",
488− BlockSuffix: "\n",
82+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
83+ Bold: boolPtr(true),
489 },
84 },
490 },
85 },
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{
86+ H4: ansi.StyleBlock{
537− IndentToken: stringPtr(BaseStyle.Render(" ")),
538 StylePrimitive: ansi.StylePrimitive{
87 StylePrimitive: ansi.StylePrimitive{
539− BackgroundColor: stringPtr(Background.Dark),
88+ Prefix: "#### ",
89+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
90+ Bold: boolPtr(true),
540 },
91 },
541 },
92 },
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{
633 StylePrimitive: ansi.StylePrimitive{
94 StylePrimitive: ansi.StylePrimitive{
634− BackgroundColor: stringPtr(Background.Dark),
95+ Prefix: "##### ",
96+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
97+ Bold: boolPtr(true),
635 },
98 },
636− Margin: uintPtr(defaultMargin),
637 },
99 },
638− },
639− Table: ansi.StyleTable{
640− StyleBlock: ansi.StyleBlock{
100+ H6: ansi.StyleBlock{
641 StylePrimitive: ansi.StylePrimitive{
101 StylePrimitive: ansi.StylePrimitive{
642− BackgroundColor: stringPtr(Background.Dark),
102+ Prefix: "###### ",
103+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
104+ Bold: boolPtr(true),
643 },
105 },
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),
670 },
106 },
671− Indent: uintPtr(defaultMargin),
107+ Strikethrough: ansi.StylePrimitive{
672− IndentToken: stringPtr(BaseStyle.Render(" ")),
108+ CrossedOut: boolPtr(true),
673− },
109+ Color: stringPtr(adaptiveColorToString(t.TextMuted())),
674− Paragraph: ansi.StyleBlock{
675− StylePrimitive: ansi.StylePrimitive{
676− BackgroundColor: stringPtr(Background.Dark),
677 },
110 },
678− },
111+ Emph: ansi.StylePrimitive{
679− List: ansi.StyleList{
112+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
680− LevelIndent: defaultMargin,
113+ Italic: boolPtr(true),
681− StyleBlock: ansi.StyleBlock{
682− IndentToken: stringPtr(BaseStyle.Render(" ")),
683− StylePrimitive: ansi.StylePrimitive{
684− Color: stringPtr(Forground.Dark),
685− BackgroundColor: stringPtr(Background.Dark),
686− },
687 },
114 },
688− },
689− Heading: ansi.StyleBlock{
690− StylePrimitive: ansi.StylePrimitive{
115+ Strong: ansi.StylePrimitive{
691− BlockSuffix: "\n",
692− Color: stringPtr(PrimaryColor.Dark),
693− Bold: boolPtr(true),
116+ Bold: boolPtr(true),
694− BackgroundColor: stringPtr(Background.Dark),
117+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
695 },
118 },
696− },
697− H1: ansi.StyleBlock{
698− StylePrimitive: ansi.StylePrimitive{
119+ HorizontalRule: ansi.StylePrimitive{
699− Prefix: "# ",
120+ Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())),
700− BackgroundColor: stringPtr(Background.Dark),
121+ Format: "\n─────────────────────────────────────────\n",
701 },
122 },
702− },
703− H2: ansi.StyleBlock{
704− StylePrimitive: ansi.StylePrimitive{
123+ Item: ansi.StylePrimitive{
705− Prefix: "## ",
124+ BlockPrefix: "• ",
706− BackgroundColor: stringPtr(Background.Dark),
125+ Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())),
707 },
126 },
708− },
709− H3: ansi.StyleBlock{
710− StylePrimitive: ansi.StylePrimitive{
127+ Enumeration: ansi.StylePrimitive{
711− Prefix: "### ",
128+ BlockPrefix: ". ",
712− BackgroundColor: stringPtr(Background.Dark),
129+ Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())),
713 },
130 },
714− },
131+ Task: ansi.StyleTask{
715− H4: ansi.StyleBlock{
716− StylePrimitive: ansi.StylePrimitive{
132+ StylePrimitive: ansi.StylePrimitive{},
717− Prefix: "#### ",
133+ Ticked: "[✓] ",
718− BackgroundColor: stringPtr(Background.Dark),
134+ Unticked: "[ ] ",
719 },
135 },
720− },
721− H5: ansi.StyleBlock{
722− StylePrimitive: ansi.StylePrimitive{
136+ Link: ansi.StylePrimitive{
723− Prefix: "##### ",
137+ Color: stringPtr(adaptiveColorToString(t.MarkdownLink())),
724− BackgroundColor: stringPtr(Background.Dark),
138+ Underline: boolPtr(true),
725 },
139 },
726− },
727− H6: ansi.StyleBlock{
728− StylePrimitive: ansi.StylePrimitive{
140+ LinkText: ansi.StylePrimitive{
729− Prefix: "###### ",
141+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
730− BackgroundColor: stringPtr(Background.Dark),
142+ Bold: boolPtr(true),
731 },
143 },
732− },
733− Strikethrough: ansi.StylePrimitive{
734− CrossedOut: boolPtr(true),
735− BackgroundColor: stringPtr(Background.Dark),
736− },
737− Emph: ansi.StylePrimitive{
144+ Image: ansi.StylePrimitive{
738− Color: stringPtr("#f1fa8c"),
145+ Color: stringPtr(adaptiveColorToString(t.MarkdownImage())),
739− Italic: boolPtr(true),
146+ Underline: 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",
147+ Format: "🖼 {{.text}}",
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),
764 },
148 },
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{
149+ ImageText: ansi.StylePrimitive{
783− Color: stringPtr("#ff79c6"),
150+ Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())),
784− Format: "Image: {{.text}} →",
151+ Format: "{{.text}}",
785− BackgroundColor: stringPtr(Background.Dark),
786− },
787− Code: ansi.StyleBlock{
788− StylePrimitive: ansi.StylePrimitive{
789− Color: stringPtr("#50fa7b"),
790− BackgroundColor: stringPtr(Background.Dark),
791 },
152 },
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{
799 StylePrimitive: ansi.StylePrimitive{
154 StylePrimitive: ansi.StylePrimitive{
800− Color: stringPtr(Blue.Dark),
155+ Color: stringPtr(adaptiveColorToString(t.MarkdownCode())),
801− BackgroundColor: stringPtr(Background.Dark),
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())),
802− },
165+ },
803− Margin: uintPtr(defaultMargin),
166+ Margin: uintPtr(defaultMargin),
804− },
167+ },
805− Chroma: &ansi.Chroma{
168+ Chroma: &ansi.Chroma{
806− NameOther: ansi.StylePrimitive{
169+ Text: ansi.StylePrimitive{
807− BackgroundColor: stringPtr(Background.Dark),
170+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
808− },
809− Literal: ansi.StylePrimitive{
810− BackgroundColor: stringPtr(Background.Dark),
811− },
812− NameException: ansi.StylePrimitive{
813− BackgroundColor: stringPtr(Background.Dark),
814− },
171+ },
815− LiteralDate: ansi.StylePrimitive{
816− BackgroundColor: stringPtr(Background.Dark),
817− },
818− Text: ansi.StylePrimitive{
172+ Error: ansi.StylePrimitive{
819− Color: stringPtr(Forground.Dark),
173+ Color: stringPtr(adaptiveColorToString(t.Error())),
820− BackgroundColor: stringPtr(Background.Dark),
821− },
822− Error: ansi.StylePrimitive{
823− Color: stringPtr("#f8f8f2"),
824− BackgroundColor: stringPtr("#ff5555"),
825− },
174+ },
826− Comment: ansi.StylePrimitive{
175+ Comment: ansi.StylePrimitive{
827− Color: stringPtr("#6272A4"),
176+ Color: stringPtr(adaptiveColorToString(t.SyntaxComment())),
828− BackgroundColor: stringPtr(Background.Dark),
829− },
177+ },
830− CommentPreproc: ansi.StylePrimitive{
178+ CommentPreproc: ansi.StylePrimitive{
831− Color: stringPtr("#ff79c6"),
179+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
832− BackgroundColor: stringPtr(Background.Dark),
833− },
180+ },
834− Keyword: ansi.StylePrimitive{
181+ Keyword: ansi.StylePrimitive{
835− Color: stringPtr("#ff79c6"),
182+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
836− BackgroundColor: stringPtr(Background.Dark),
837− },
183+ },
838− KeywordReserved: ansi.StylePrimitive{
184+ KeywordReserved: ansi.StylePrimitive{
839− Color: stringPtr("#ff79c6"),
185+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
840− BackgroundColor: stringPtr(Background.Dark),
841− },
186+ },
842− KeywordNamespace: ansi.StylePrimitive{
187+ KeywordNamespace: ansi.StylePrimitive{
843− Color: stringPtr("#ff79c6"),
188+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
844− BackgroundColor: stringPtr(Background.Dark),
845− },
189+ },
846− KeywordType: ansi.StylePrimitive{
190+ KeywordType: ansi.StylePrimitive{
847− Color: stringPtr("#8be9fd"),
191+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
848− BackgroundColor: stringPtr(Background.Dark),
849− },
192+ },
850− Operator: ansi.StylePrimitive{
193+ Operator: ansi.StylePrimitive{
851− Color: stringPtr("#ff79c6"),
194+ Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())),
852− BackgroundColor: stringPtr(Background.Dark),
853− },
195+ },
854− Punctuation: ansi.StylePrimitive{
196+ Punctuation: ansi.StylePrimitive{
855− Color: stringPtr(Forground.Dark),
197+ Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())),
856− BackgroundColor: stringPtr(Background.Dark),
857− },
198+ },
858− Name: ansi.StylePrimitive{
199+ Name: ansi.StylePrimitive{
859− Color: stringPtr("#8be9fd"),
200+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
860− BackgroundColor: stringPtr(Background.Dark),
861− },
201+ },
862− NameBuiltin: ansi.StylePrimitive{
202+ NameBuiltin: ansi.StylePrimitive{
863− Color: stringPtr("#8be9fd"),
203+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
864− BackgroundColor: stringPtr(Background.Dark),
865− },
204+ },
866− NameTag: ansi.StylePrimitive{
205+ NameTag: ansi.StylePrimitive{
867− Color: stringPtr("#ff79c6"),
206+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
868− BackgroundColor: stringPtr(Background.Dark),
869− },
207+ },
870− NameAttribute: ansi.StylePrimitive{
208+ NameAttribute: ansi.StylePrimitive{
871− Color: stringPtr("#50fa7b"),
209+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
872− BackgroundColor: stringPtr(Background.Dark),
873− },
210+ },
874− NameClass: ansi.StylePrimitive{
211+ NameClass: ansi.StylePrimitive{
875− Color: stringPtr("#8be9fd"),
212+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
876− BackgroundColor: stringPtr(Background.Dark),
877− },
213+ },
878− NameConstant: ansi.StylePrimitive{
214+ NameConstant: ansi.StylePrimitive{
879− Color: stringPtr("#bd93f9"),
215+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
880− BackgroundColor: stringPtr(Background.Dark),
881− },
216+ },
882− NameDecorator: ansi.StylePrimitive{
217+ NameDecorator: ansi.StylePrimitive{
883− Color: stringPtr("#50fa7b"),
218+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
884− BackgroundColor: stringPtr(Background.Dark),
885− },
219+ },
886− NameFunction: ansi.StylePrimitive{
220+ NameFunction: ansi.StylePrimitive{
887− Color: stringPtr("#50fa7b"),
221+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
888− BackgroundColor: stringPtr(Background.Dark),
889− },
222+ },
890− LiteralNumber: ansi.StylePrimitive{
223+ LiteralNumber: ansi.StylePrimitive{
891− Color: stringPtr("#6EEFC0"),
224+ Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())),
892− BackgroundColor: stringPtr(Background.Dark),
893− },
225+ },
894− LiteralString: ansi.StylePrimitive{
226+ LiteralString: ansi.StylePrimitive{
895− Color: stringPtr("#f1fa8c"),
227+ Color: stringPtr(adaptiveColorToString(t.SyntaxString())),
896− BackgroundColor: stringPtr(Background.Dark),
897− },
228+ },
898− LiteralStringEscape: ansi.StylePrimitive{
229+ LiteralStringEscape: ansi.StylePrimitive{
899− Color: stringPtr("#ff79c6"),
230+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
900− BackgroundColor: stringPtr(Background.Dark),
901− },
231+ },
902− GenericDeleted: ansi.StylePrimitive{
232+ GenericDeleted: ansi.StylePrimitive{
903− Color: stringPtr("#ff5555"),
233+ Color: stringPtr(adaptiveColorToString(t.DiffRemoved())),
904− BackgroundColor: stringPtr(Background.Dark),
905− },
234+ },
906− GenericEmph: ansi.StylePrimitive{
235+ GenericEmph: ansi.StylePrimitive{
907− Color: stringPtr("#f1fa8c"),
236+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
908− Italic: boolPtr(true),
237+ Italic: boolPtr(true),
909− BackgroundColor: stringPtr(Background.Dark),
910− },
238+ },
911− GenericInserted: ansi.StylePrimitive{
239+ GenericInserted: ansi.StylePrimitive{
912− Color: stringPtr("#50fa7b"),
240+ Color: stringPtr(adaptiveColorToString(t.DiffAdded())),
913− BackgroundColor: stringPtr(Background.Dark),
914− },
241+ },
915− GenericStrong: ansi.StylePrimitive{
242+ GenericStrong: ansi.StylePrimitive{
916− Color: stringPtr("#ffb86c"),
243+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
917− Bold: boolPtr(true),
244+ Bold: boolPtr(true),
918− BackgroundColor: stringPtr(Background.Dark),
919− },
245+ },
920− GenericSubheading: ansi.StylePrimitive{
246+ GenericSubheading: ansi.StylePrimitive{
921− Color: stringPtr("#bd93f9"),
247+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
922− BackgroundColor: stringPtr(Background.Dark),
248+ },
923− },
924− Background: ansi.StylePrimitive{
925− BackgroundColor: stringPtr(Background.Dark),
926− },
249+ },
927− },
250+ },
928− },
929− Table: ansi.StyleTable{
251+ Table: ansi.StyleTable{
930− StyleBlock: ansi.StyleBlock{
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{
931 StylePrimitive: ansi.StylePrimitive{
270 StylePrimitive: ansi.StylePrimitive{
932− BackgroundColor: stringPtr(Background.Dark),
271+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
933 },
272 },
934− IndentToken: stringPtr(BaseStyle.Render(" ")),
935 },
273 },
936− },
274+ }
937− DefinitionDescription: ansi.StylePrimitive{
275+}
938− BlockPrefix: "\n* ",
276+
939− BackgroundColor: stringPtr(Background.Dark),
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
940− },
282+ }
283+ return color.Light
941}
284}
internal/tui/styles/styles.go+145−170
@@ -1,177 +1,152 @@
1package styles
1package styles
2
2
3import (
3import (
4− catppuccin "github.com/catppuccin/go"
5 "github.com/charmbracelet/lipgloss"
4 "github.com/charmbracelet/lipgloss"
5+ "github.com/opencode-ai/opencode/internal/tui/theme"
6)
6)
7
7
8−var (
8+// Style generation functions that use the current theme
9− light = catppuccin.Latte
9+
10− dark = catppuccin.Mocha
10+// BaseStyle returns the base style with background and foreground colors
11−)
11+func BaseStyle() lipgloss.Style {
12−
12+ t := theme.CurrentTheme()
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().
13+ return lipgloss.NewStyle().
48− Background(Background).
14+ Background(t.Background()).
49− Foreground(Forground)
15+ Foreground(t.Text())
50−
16+}
51− PrimaryColor = lipgloss.AdaptiveColor{
17+
52− Dark: "#fab283",
18+// Regular returns a basic unstyled lipgloss.Style
53− Light: "#fab283",
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())
54− }
47+}
55−)
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+}
56
152
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 {
27 SwitchSession key.Binding
27 SwitchSession key.Binding
28 Commands key.Binding
28 Commands key.Binding
29 Models key.Binding
29 Models key.Binding
30+ SwitchTheme key.Binding
30}
31}
31
32
32var keys = keyMap{
33var keys = keyMap{
@@ -58,6 +59,11 @@ var keys = keyMap{
58 key.WithKeys("ctrl+o"),
59 key.WithKeys("ctrl+o"),
59 key.WithHelp("ctrl+o", "model selection"),
60 key.WithHelp("ctrl+o", "model selection"),
60 ),
61 ),
62+
63+ SwitchTheme: key.NewBinding(
64+ key.WithKeys("ctrl+t"),
65+ key.WithHelp("ctrl+t", "switch theme"),
66+ ),
61}
67}
62
68
63var helpEsc = key.NewBinding(
69var helpEsc = key.NewBinding(
@@ -105,6 +111,9 @@ type appModel struct {
105
111
106 showInitDialog bool
112 showInitDialog bool
107 initDialog dialog.InitDialogCmp
113 initDialog dialog.InitDialogCmp
114+
115+ showThemeDialog bool
116+ themeDialog dialog.ThemeDialog
108}
117}
109
118
110func (a appModel) Init() tea.Cmd {
119func (a appModel) Init() tea.Cmd {
@@ -126,6 +135,8 @@ func (a appModel) Init() tea.Cmd {
126 cmds = append(cmds, cmd)
135 cmds = append(cmds, cmd)
127 cmd = a.initDialog.Init()
136 cmd = a.initDialog.Init()
128 cmds = append(cmds, cmd)
137 cmds = append(cmds, cmd)
138+ cmd = a.themeDialog.Init()
139+ cmds = append(cmds, cmd)
129
140
130 // Check if we should show the init dialog
141 // Check if we should show the init dialog
131 cmds = append(cmds, func() tea.Msg {
142 cmds = append(cmds, func() tea.Msg {
@@ -255,6 +266,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255 a.showCommandDialog = false
266 a.showCommandDialog = false
256 return a, nil
267 return a, nil
257
268
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+
258 case dialog.CloseModelDialogMsg:
278 case dialog.CloseModelDialogMsg:
259 a.showModelDialog = false
279 a.showModelDialog = false
260 return a, nil
280 return a, nil
@@ -344,7 +364,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
344 }
364 }
345 return a, nil
365 return a, nil
346 case key.Matches(msg, keys.Commands):
366 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 {
348 // Show commands dialog
368 // Show commands dialog
349 if len(a.commands) == 0 {
369 if len(a.commands) == 0 {
350 return a, util.ReportWarn("No commands available")
370 return a, util.ReportWarn("No commands available")
@@ -359,12 +379,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
359 a.showModelDialog = false
379 a.showModelDialog = false
360 return a, nil
380 return a, nil
361 }
381 }
362−
363 if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
382 if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
364 a.showModelDialog = true
383 a.showModelDialog = true
365 return a, nil
384 return a, nil
366 }
385 }
367 return a, nil
386 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
368 case key.Matches(msg, logsKeyReturnKey):
395 case key.Matches(msg, logsKeyReturnKey):
369 if a.currentPage == page.LogsPage {
396 if a.currentPage == page.LogsPage {
370 return a, a.moveToPage(page.ChatPage)
397 return a, a.moveToPage(page.ChatPage)
@@ -465,6 +492,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
465 }
492 }
466 }
493 }
467
494
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+
468 s, _ := a.status.Update(msg)
505 s, _ := a.status.Update(msg)
469 a.status = s.(core.StatusCmp)
506 a.status = s.(core.StatusCmp)
470 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
507 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
@@ -523,9 +560,9 @@ func (a appModel) View() string {
523 }
560 }
524
561
525 if !a.app.CoderAgent.IsBusy() {
562 if !a.app.CoderAgent.IsBusy() {
526− a.status.SetHelpMsg("ctrl+? help")
563+ a.status.SetHelpWidgetMsg("ctrl+? help")
527 } else {
564 } else {
528− a.status.SetHelpMsg("? help")
565+ a.status.SetHelpWidgetMsg("? help")
529 }
566 }
530
567
531 if a.showHelp {
568 if a.showHelp {
@@ -629,6 +666,21 @@ func (a appModel) View() string {
629 )
666 )
630 }
667 }
631
668
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+
632 return appView
684 return appView
633}
685}
634
686
@@ -645,6 +697,7 @@ func New(app *app.App) tea.Model {
645 modelDialog: dialog.NewModelDialogCmp(),
697 modelDialog: dialog.NewModelDialogCmp(),
646 permissions: dialog.NewPermissionDialogCmp(),
698 permissions: dialog.NewPermissionDialogCmp(),
647 initDialog: dialog.NewInitDialogCmp(),
699 initDialog: dialog.NewInitDialogCmp(),
700+ themeDialog: dialog.NewThemeDialogCmp(),
648 app: app,
701 app: app,
649 commands: []dialog.Command{},
702 commands: []dialog.Command{},
650 pages: map[page.PageID]tea.Model{
703 pages: map[page.PageID]tea.Model{
opencode-schema.json+143−104
@@ -12,75 +12,79 @@
12 "model": {
12 "model": {
13 "description": "Model ID for the agent",
13 "description": "Model ID for the agent",
14 "enum": [
14 "enum": [
15+ "gpt-4o-mini",
16+ "o1-pro",
17+ "azure.gpt-4o-mini",
18+ "openrouter.gpt-4.1-mini",
19+ "openrouter.o1-mini",
15 "bedrock.claude-3.7-sonnet",
20 "bedrock.claude-3.7-sonnet",
16− "claude-3-haiku",
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",
17− "claude-3.7-sonnet",
25+ "openrouter.claude-3.7-sonnet",
26+ "openrouter.gpt-4.5-preview",
27+ "azure.o3-mini",
18− "claude-3.5-haiku",
28+ "openrouter.claude-3.5-haiku",
19− "o3",
29+ "azure.o1-mini",
20− "azure.o3",
30+ "openrouter.o1",
31+ "openrouter.gemini-2.5",
32+ "llama-3.3-70b-versatile",
21 "gpt-4.5-preview",
33 "gpt-4.5-preview",
22− "azure.gpt-4.5-preview",
34+ "openrouter.claude-3-opus",
23− "o1-pro",
35+ "openrouter.claude-3.5-sonnet",
24 "o4-mini",
36 "o4-mini",
37+ "gemini-2.0-flash-lite",
38+ "azure.gpt-4.5-preview",
39+ "openrouter.gpt-4o",
40+ "o1",
25− "azure.o4-mini",
41+ "azure.gpt-4o",
42+ "openrouter.gpt-4.1-nano",
43+ "o3",
26 "gpt-4.1",
44 "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",
28 "o3-mini",
52 "o3-mini",
29− "azure.o3-mini",
53+ "azure.o3",
54+ "gpt-4o",
55+ "azure.gpt-4.1",
56+ "openrouter.claude-3-haiku",
30 "gpt-4.1-nano",
57 "gpt-4.1-nano",
31 "azure.gpt-4.1-nano",
58 "azure.gpt-4.1-nano",
32− "gpt-4o-mini",
59+ "claude-3.7-sonnet",
33− "azure.gpt-4o-mini",
34− "o1",
35− "azure.o1",
36− "gemini-2.5-flash",
60+ "gemini-2.5",
61+ "azure.o4-mini",
62+ "o1-mini",
37 "qwen-qwq",
63 "qwen-qwq",
38 "meta-llama/llama-4-maverick-17b-128e-instruct",
64 "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",
55 "openrouter.gpt-4.1",
65 "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",
62 "openrouter.o1-pro",
66 "openrouter.o1-pro",
63− "openrouter.o1-mini",
64 "openrouter.o3",
67 "openrouter.o3",
65− "openrouter.o3-mini",
68+ "claude-3.5-sonnet",
66− "openrouter.o4-mini",
67− "openrouter.gemini-2.5-flash",
69+ "gemini-2.5-flash",
68− "openrouter.gemini-2.5",
70+ "azure.gpt-4.1-mini",
69− "openrouter.claude-3.5-sonnet",
70− "openrouter.claude-3-haiku",
71+ "openrouter.o3-mini"
71− "openrouter.claude-3.7-sonnet",
72− "openrouter.claude-3.5-haiku",
73− "openrouter.claude-3-opus"
74 ],
72 ],
75 "type": "string"
73 "type": "string"
76 },
74 },
77 "reasoningEffort": {
75 "reasoningEffort": {
78 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
76 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
77+ "enum": [
78+ "low",
79− "enum": ["low", "medium", "high"],
79+ "medium",
80+ "high"
81+ ],
80 "type": "string"
82 "type": "string"
81 }
83 }
82 },
84 },
83− "required": ["model"],
85+ "required": [
86+ "model"
87+ ],
84 "type": "object"
88 "type": "object"
85 }
89 }
86 },
90 },
@@ -98,75 +102,79 @@
98 "model": {
102 "model": {
99 "description": "Model ID for the agent",
103 "description": "Model ID for the agent",
100 "enum": [
104 "enum": [
105+ "gpt-4o-mini",
106+ "o1-pro",
107+ "azure.gpt-4o-mini",
108+ "openrouter.gpt-4.1-mini",
109+ "openrouter.o1-mini",
101 "bedrock.claude-3.7-sonnet",
110 "bedrock.claude-3.7-sonnet",
102− "claude-3-haiku",
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",
103− "claude-3.7-sonnet",
115+ "openrouter.claude-3.7-sonnet",
116+ "openrouter.gpt-4.5-preview",
117+ "azure.o3-mini",
104− "claude-3.5-haiku",
118+ "openrouter.claude-3.5-haiku",
105− "o3",
119+ "azure.o1-mini",
106− "azure.o3",
120+ "openrouter.o1",
121+ "openrouter.gemini-2.5",
122+ "llama-3.3-70b-versatile",
107 "gpt-4.5-preview",
123 "gpt-4.5-preview",
108− "azure.gpt-4.5-preview",
124+ "openrouter.claude-3-opus",
109− "o1-pro",
125+ "openrouter.claude-3.5-sonnet",
110 "o4-mini",
126 "o4-mini",
127+ "gemini-2.0-flash-lite",
128+ "azure.gpt-4.5-preview",
129+ "openrouter.gpt-4o",
130+ "o1",
111− "azure.o4-mini",
131+ "azure.gpt-4o",
132+ "openrouter.gpt-4.1-nano",
133+ "o3",
112 "gpt-4.1",
134 "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",
114 "o3-mini",
142 "o3-mini",
115− "azure.o3-mini",
143+ "azure.o3",
144+ "gpt-4o",
145+ "azure.gpt-4.1",
146+ "openrouter.claude-3-haiku",
116 "gpt-4.1-nano",
147 "gpt-4.1-nano",
117 "azure.gpt-4.1-nano",
148 "azure.gpt-4.1-nano",
118− "gpt-4o-mini",
149+ "claude-3.7-sonnet",
119− "azure.gpt-4o-mini",
120− "o1",
121− "azure.o1",
122− "gemini-2.5-flash",
150+ "gemini-2.5",
151+ "azure.o4-mini",
152+ "o1-mini",
123 "qwen-qwq",
153 "qwen-qwq",
124 "meta-llama/llama-4-maverick-17b-128e-instruct",
154 "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",
141 "openrouter.gpt-4.1",
155 "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",
148 "openrouter.o1-pro",
156 "openrouter.o1-pro",
149− "openrouter.o1-mini",
150 "openrouter.o3",
157 "openrouter.o3",
151− "openrouter.o3-mini",
158+ "claude-3.5-sonnet",
152− "openrouter.o4-mini",
153− "openrouter.gemini-2.5-flash",
159+ "gemini-2.5-flash",
154− "openrouter.gemini-2.5",
160+ "azure.gpt-4.1-mini",
155− "openrouter.claude-3.5-sonnet",
156− "openrouter.claude-3-haiku",
161+ "openrouter.o3-mini"
157− "openrouter.claude-3.7-sonnet",
158− "openrouter.claude-3.5-haiku",
159− "openrouter.claude-3-opus"
160 ],
162 ],
161 "type": "string"
163 "type": "string"
162 },
164 },
163 "reasoningEffort": {
165 "reasoningEffort": {
164 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
166 "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
167+ "enum": [
168+ "low",
165− "enum": ["low", "medium", "high"],
169+ "medium",
170+ "high"
171+ ],
166 "type": "string"
172 "type": "string"
167 }
173 }
168 },
174 },
169− "required": ["model"],
175+ "required": [
176+ "model"
177+ ],
170 "type": "object"
178 "type": "object"
171 },
179 },
172 "description": "Agent configurations",
180 "description": "Agent configurations",
@@ -212,7 +220,9 @@
212 "type": "string"
220 "type": "string"
213 }
221 }
214 },
222 },
223+ "required": [
215− "required": ["directory"],
224+ "directory"
225+ ],
216 "type": "object"
226 "type": "object"
217 },
227 },
218 "debug": {
228 "debug": {
@@ -250,7 +260,9 @@
250 "type": "object"
260 "type": "object"
251 }
261 }
252 },
262 },
253− "required": ["command"],
263+ "required": [
264+ "command"
265+ ],
254 "type": "object"
266 "type": "object"
255 },
267 },
256 "description": "Language Server Protocol configurations",
268 "description": "Language Server Protocol configurations",
@@ -288,7 +300,10 @@
288 "type": {
300 "type": {
289 "default": "stdio",
301 "default": "stdio",
290 "description": "Type of MCP server",
302 "description": "Type of MCP server",
303+ "enum": [
291− "enum": ["stdio", "sse"],
304+ "stdio",
305+ "sse"
306+ ],
292 "type": "string"
307 "type": "string"
293 },
308 },
294 "url": {
309 "url": {
@@ -296,7 +311,9 @@
296 "type": "string"
311 "type": "string"
297 }
312 }
298 },
313 },
299− "required": ["command"],
314+ "required": [
315+ "command"
316+ ],
300 "type": "object"
317 "type": "object"
301 },
318 },
302 "description": "Model Control Protocol server configurations",
319 "description": "Model Control Protocol server configurations",
@@ -322,9 +339,9 @@
322 "openai",
339 "openai",
323 "gemini",
340 "gemini",
324 "groq",
341 "groq",
342+ "openrouter",
325 "bedrock",
343 "bedrock",
326− "azure",
344+ "azure"
327− "openrouter"
328 ],
345 ],
329 "type": "string"
346 "type": "string"
330 }
347 }
@@ -334,6 +351,28 @@
334 "description": "LLM provider configurations",
351 "description": "LLM provider configurations",
335 "type": "object"
352 "type": "object"
336 },
353 },
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+ },
337 "wd": {
376 "wd": {
338 "description": "Working directory for the application",
377 "description": "Working directory for the application",
339 "type": "string"
378 "type": "string"