Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
progress = app.config.Config().Options.Progress == nil || *app.config.Config().Options.Progress

if !hideSpinner && stderrTTY {
t := styles.ThemeForProvider(app.config.Config().Models[config.SelectedModelTypeLarge].Provider)
t := styles.ThemeForProvider(
app.config.Config().Models[config.SelectedModelTypeLarge].Provider,
app.config.Config().Options.ThemePreference(),
)

// Detect background color to set the appropriate color for the
// spinner's 'Generating...' text. Without this, that text would be
Expand Down
6 changes: 5 additions & 1 deletion internal/cmd/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,11 @@ func outputSessionHuman(ctx context.Context, cfg *config.ConfigStore, sess sessi
if cfg != nil {
providerID = cfg.Config().Models[config.SelectedModelTypeLarge].Provider
}
styles := styles.ThemeForProvider(providerID)
themePref := "auto"
if cfg != nil {
themePref = cfg.Config().Options.ThemePreference()
}
styles := styles.ThemeForProvider(providerID, themePref)
toolResults := chat.BuildToolResultMap(msgs)

width := sessionOutputWidth
Expand Down
12 changes: 10 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,7 @@ type LSPConfig struct {
type TUIOptions struct {
CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"`
DiffMode string `json:"diff_mode,omitempty" jsonschema:"description=Diff mode for the TUI interface,enum=unified,enum=split"`
// Here we can add themes later or any TUI related options
//
Theme string `json:"theme,omitempty" jsonschema:"description=Color theme for the TUI interface,enum=auto,enum=dark,enum=light,default=auto"`

Completions Completions `json:"completions,omitzero" jsonschema:"description=Completions UI options"`
Transparent *bool `json:"transparent,omitempty" jsonschema:"description=Enable transparent background for the TUI interface,default=false"`
Expand Down Expand Up @@ -284,6 +283,15 @@ type Options struct {
DisabledSkills []string `json:"disabled_skills,omitempty" jsonschema:"description=List of skill names to disable and hide from the agent,example=crush-config"`
}

// ThemePreference returns the configured TUI theme preference, defaulting to
// "auto" when none is set.
func (o Options) ThemePreference() string {
if o.TUI != nil && o.TUI.Theme != "" {
return o.TUI.Theme
}
return "auto"
}

type MCPs map[string]MCPConfig

type MCP struct {
Expand Down
6 changes: 5 additions & 1 deletion internal/ui/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ func (c *Common) Config() *config.Config {
// workspace has a large model selected, the theme is chosen based on its
// provider; otherwise the default theme is used.
func DefaultCommon(ws workspace.Workspace) *Common {
s := styles.ThemeForProvider(largeModelProviderID(ws))
themePref := "auto"
if ws != nil && ws.Config() != nil {
themePref = ws.Config().Options.ThemePreference()
}
s := styles.ThemeForProvider(largeModelProviderID(ws), themePref)
return &Common{
Workspace: ws,
Styles: &s,
Expand Down
49 changes: 39 additions & 10 deletions internal/ui/styles/quickstyle.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package styles

import (
"fmt"
"image/color"

"charm.land/bubbles/v2/filepicker"
Expand Down Expand Up @@ -55,6 +56,15 @@ type quickStyleOpts struct {
success color.Color
successMoreSubtle color.Color
successMostSubtle color.Color

// Diff highlight colors. If left as nil, quickStyle falls back to the
// hard-coded dark-mode defaults for backwards compatibility.
diffInsertFg color.Color
diffInsertBg color.Color
diffInsertSymbolBg color.Color
diffDeleteFg color.Color
diffDeleteBg color.Color
diffDeleteSymbolBg color.Color
}

// quickStyle builds the default Styles (that is, the default theme, Charmtone
Expand Down Expand Up @@ -530,23 +540,23 @@ func quickStyle(o quickStyleOpts) Styles {
},
InsertLine: diffview.LineStyle{
LineNumber: lipgloss.NewStyle().
Foreground(lipgloss.Color("#629657")).
Background(lipgloss.Color("#2b322a")),
Foreground(lipgloss.Color(diffColor(o.diffInsertFg, "#629657"))).
Background(lipgloss.Color(diffColor(o.diffInsertBg, "#2b322a"))),
Symbol: lipgloss.NewStyle().
Foreground(lipgloss.Color("#629657")).
Background(lipgloss.Color("#323931")),
Foreground(lipgloss.Color(diffColor(o.diffInsertFg, "#629657"))).
Background(lipgloss.Color(diffColor(o.diffInsertSymbolBg, "#323931"))),
Code: lipgloss.NewStyle().
Background(lipgloss.Color("#323931")),
Background(lipgloss.Color(diffColor(o.diffInsertBg, "#323931"))),
},
DeleteLine: diffview.LineStyle{
LineNumber: lipgloss.NewStyle().
Foreground(lipgloss.Color("#a45c59")).
Background(lipgloss.Color("#312929")),
Foreground(lipgloss.Color(diffColor(o.diffDeleteFg, "#a45c59"))).
Background(lipgloss.Color(diffColor(o.diffDeleteBg, "#312929"))),
Symbol: lipgloss.NewStyle().
Foreground(lipgloss.Color("#a45c59")).
Background(lipgloss.Color("#383030")),
Foreground(lipgloss.Color(diffColor(o.diffDeleteFg, "#a45c59"))).
Background(lipgloss.Color(diffColor(o.diffDeleteSymbolBg, "#383030"))),
Code: lipgloss.NewStyle().
Background(lipgloss.Color("#383030")),
Background(lipgloss.Color(diffColor(o.diffDeleteBg, "#383030"))),
},
Filename: diffview.LineStyle{
LineNumber: lipgloss.NewStyle().
Expand Down Expand Up @@ -951,3 +961,22 @@ func quickStyle(o quickStyleOpts) Styles {

return s
}

// colorHex returns a lowercase "#rrggbb" string for a color.Color value.
// It returns an empty string when c is nil.
func colorHex(c color.Color) string {
if c == nil {
return ""
}
r, g, b, _ := c.RGBA()
return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
}

// diffColor returns the hex color for an optional diff color, falling back to
// a hard-coded dark-mode default when none is provided.
func diffColor(c color.Color, fallback string) string {
if h := colorHex(c); h != "" {
return h
}
return fallback
}
87 changes: 85 additions & 2 deletions internal/ui/styles/themes.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
package styles

import "github.com/charmbracelet/x/exp/charmtone"
import (
"image/color"
"os"
"strings"

"github.com/charmbracelet/x/exp/charmtone"
)

// ThemeForProvider returns the Styles associated with the given provider
// ID. Unknown or empty provider IDs yield the default Charmtone Pantera
// theme.
func ThemeForProvider(providerID string) Styles {
//
// An optional theme preference overrides the provider-based default. Valid
// values are "auto" (provider-based default), "dark", and "light". When no
// preference is supplied, the CRUSH_THEME environment variable is consulted.
func ThemeForProvider(providerID string, themePref ...string) Styles {
theme := "auto"
if len(themePref) > 0 && themePref[0] != "" {
theme = themePref[0]
} else if v := os.Getenv("CRUSH_THEME"); v != "" {
theme = v
}

switch strings.ToLower(theme) {
case "light":
return CharmtoneLatte()
case "dark":
// Explicit dark falls through to provider defaults below.
}

switch providerID {
case "hyper":
return HypercrushObsidiana()
Expand Down Expand Up @@ -49,6 +73,13 @@ func CharmtonePantera() Styles {
success: charmtone.Julep,
successMoreSubtle: charmtone.Bok,
successMostSubtle: charmtone.Guac,

diffInsertFg: color.RGBA{R: 0x62, G: 0x96, B: 0x57, A: 0xFF},
diffInsertBg: color.RGBA{R: 0x2b, G: 0x32, B: 0x2a, A: 0xFF},
diffInsertSymbolBg: color.RGBA{R: 0x32, G: 0x39, B: 0x31, A: 0xFF},
diffDeleteFg: color.RGBA{R: 0xa4, G: 0x5c, B: 0x59, A: 0xFF},
diffDeleteBg: color.RGBA{R: 0x31, G: 0x29, B: 0x29, A: 0xFF},
diffDeleteSymbolBg: color.RGBA{R: 0x38, G: 0x30, B: 0x30, A: 0xFF},
})
}

Expand Down Expand Up @@ -85,5 +116,57 @@ func HypercrushObsidiana() Styles {
success: charmtone.Julep,
successMoreSubtle: charmtone.Bok,
successMostSubtle: charmtone.Guac,

diffInsertFg: color.RGBA{R: 0x62, G: 0x96, B: 0x57, A: 0xFF},
diffInsertBg: color.RGBA{R: 0x2b, G: 0x32, B: 0x2a, A: 0xFF},
diffInsertSymbolBg: color.RGBA{R: 0x32, G: 0x39, B: 0x31, A: 0xFF},
diffDeleteFg: color.RGBA{R: 0xa4, G: 0x5c, B: 0x59, A: 0xFF},
diffDeleteBg: color.RGBA{R: 0x31, G: 0x29, B: 0x29, A: 0xFF},
diffDeleteSymbolBg: color.RGBA{R: 0x38, G: 0x30, B: 0x30, A: 0xFF},
})
}

// CharmtoneLatte returns a light variant of the Charmtone theme designed for
// terminals with a light background.
func CharmtoneLatte() Styles {
return quickStyle(quickStyleOpts{
primary: charmtone.Charple,
secondary: charmtone.Dolly,
accent: charmtone.Malibu,
keyword: charmtone.Blush,

fgBase: charmtone.Pepper,
fgMoreSubtle: charmtone.Char,
fgSubtle: charmtone.Iron,
fgMostSubtle: charmtone.Oyster,

onPrimary: charmtone.Butter,

bgBase: charmtone.Soda,
bgLeastVisible: charmtone.Sash,
bgLessVisible: charmtone.Steep,
bgMostVisible: charmtone.Squid,

separator: charmtone.Steam,

destructive: charmtone.Coral,
error: charmtone.Sriracha,
warningSubtle: charmtone.Yam,
warning: charmtone.Tang,
denied: charmtone.Tang,
busy: charmtone.Mustard,
info: charmtone.Malibu,
infoMoreSubtle: charmtone.Damson,
infoMostSubtle: charmtone.Sapphire,
success: charmtone.Guac,
successMoreSubtle: charmtone.Julep,
successMostSubtle: charmtone.Pickle,

diffInsertFg: color.RGBA{R: 0x00, G: 0xa4, B: 0x75, A: 0xFF},
diffInsertBg: color.RGBA{R: 0xe6, G: 0xf7, B: 0xf1, A: 0xFF},
diffInsertSymbolBg: color.RGBA{R: 0xd1, G: 0xf2, B: 0xe6, A: 0xFF},
diffDeleteFg: color.RGBA{R: 0xd7, G: 0x3a, B: 0x49, A: 0xFF},
diffDeleteBg: color.RGBA{R: 0xff, G: 0xe6, B: 0xe6, A: 0xFF},
diffDeleteSymbolBg: color.RGBA{R: 0xff, G: 0xd1, B: 0xd1, A: 0xFF},
})
}
Loading