@@ -21,6 +21,7 @@ import (
"git.secluded.site/crush/internal/projects"
"git.secluded.site/crush/internal/stringext"
"git.secluded.site/crush/internal/tui"
+ "git.secluded.site/crush/internal/tui/styles"
"git.secluded.site/crush/internal/version"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/fang"
@@ -35,6 +36,7 @@ func init() {
rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
+ rootCmd.PersistentFlags().BoolP("light", "l", false, "Use light theme")
rootCmd.Flags().BoolP("help", "h", false, "Help")
rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
@@ -177,9 +179,17 @@ func setupAppWithProgressBar(cmd *cobra.Command) (*app.App, error) {
func setupApp(cmd *cobra.Command) (*app.App, error) {
debug, _ := cmd.Flags().GetBool("debug")
yolo, _ := cmd.Flags().GetBool("yolo")
+ light, _ := cmd.Flags().GetBool("light")
dataDir, _ := cmd.Flags().GetString("data-dir")
ctx := cmd.Context()
+ // Set light theme if requested.
+ if light {
+ if err := styles.DefaultManager().SetTheme("light"); err != nil {
+ slog.Warn("Failed to set light theme", "error", err)
+ }
+ }
+
cwd, err := ResolveCwd(cmd)
if err != nil {
return nil, err
@@ -56,6 +56,51 @@ func NewCharmtoneTheme() *Theme {
RedDark: charmtone.Sriracha,
RedLight: charmtone.Salmon,
Cherry: charmtone.Cherry,
+
+ // Markdown colors (dark theme).
+ MdText: charmtone.Smoke,
+ MdHeading: charmtone.Malibu,
+ MdH6: charmtone.Guac,
+ MdHRule: charmtone.Charcoal,
+ MdLink: charmtone.Zinc,
+ MdLinkText: charmtone.Guac,
+ MdImage: charmtone.Cheeky,
+ MdImageText: charmtone.Squid,
+ MdCodeFg: charmtone.Coral,
+ MdCodeBg: charmtone.Charcoal,
+ MdCodeBlockFg: charmtone.Charcoal,
+ MdCodeBlockBg: charmtone.Charcoal,
+ MdComment: charmtone.Oyster,
+ MdKeyword: charmtone.Malibu,
+ MdKeywordAlt: charmtone.Pony,
+ MdKeywordType: charmtone.Guppy,
+ MdOperator: charmtone.Salmon,
+ MdPunctuation: charmtone.Zest,
+ MdName: charmtone.Smoke,
+ MdNameBuiltin: charmtone.Cheeky,
+ MdNameTag: charmtone.Mauve,
+ MdNameAttr: charmtone.Hazy,
+ MdNameClass: charmtone.Salt,
+ MdNameDecorator: charmtone.Citron,
+ MdNameFunc: charmtone.Guac,
+ MdNumber: charmtone.Julep,
+ MdString: charmtone.Cumin,
+ MdStringEscape: charmtone.Bok,
+ MdDeleted: charmtone.Coral,
+ MdInserted: charmtone.Guac,
+ MdSubheading: charmtone.Squid,
+ MdError: charmtone.Butter,
+ MdErrorBg: charmtone.Sriracha,
+
+ // Diff colors (dark theme).
+ DiffInsertFg: lipgloss.Color("#629657"),
+ DiffInsertBg: lipgloss.Color("#2b322a"),
+ DiffInsertSymBg: lipgloss.Color("#323931"),
+ DiffInsertCodeBg: lipgloss.Color("#323931"),
+ DiffDeleteFg: lipgloss.Color("#a45c59"),
+ DiffDeleteBg: lipgloss.Color("#312929"),
+ DiffDeleteSymBg: lipgloss.Color("#383030"),
+ DiffDeleteCodeBg: lipgloss.Color("#383030"),
}
// Text selection.
@@ -0,0 +1,121 @@
+package styles
+
+import (
+ "charm.land/lipgloss/v2"
+ "github.com/charmbracelet/x/exp/charmtone"
+)
+
+func NewLightTheme() *Theme {
+ t := &Theme{
+ Name: "light",
+ IsDark: false,
+
+ Primary: charmtone.Charple,
+ Secondary: charmtone.Dolly,
+ Tertiary: charmtone.Bok,
+ Accent: charmtone.Zest,
+
+ // Backgrounds (improved for accessibility)
+ BgBase: charmtone.Salt, // #F1EFEF - very light background
+ BgBaseLighter: lipgloss.Color("#FFFFFF"), // Pure white for areas needing more contrast
+ BgSubtle: charmtone.Ash, // #DFDBDD - light gray for subtle areas
+ BgOverlay: charmtone.Oyster, // #605F6B - darker overlay, better contrast
+
+ // Foregrounds (darker for better accessibility on light backgrounds)
+ FgBase: charmtone.Pepper, // #201F26 - darkest, good contrast
+ FgMuted: charmtone.Charcoal, // #3A3943 - still quite dark
+ FgHalfMuted: charmtone.Iron, // #4D4C57 - medium dark
+ FgSubtle: charmtone.Oyster, // #605F6B - much better than Squid
+ FgSelected: charmtone.Pepper, // #201F26 - same as base for consistency
+
+ // Borders (improved contrast)
+ Border: charmtone.Oyster, // #605F6B - better contrast than previous
+ BorderFocus: charmtone.Charple, // #6B50FF - same purple for focus
+
+ // Status (same as dark, these work on both)
+ Success: charmtone.Turtle,
+ Error: charmtone.Cherry,
+ Warning: charmtone.Zest,
+ Info: charmtone.Sapphire,
+
+ // Colors
+ White: charmtone.Salt,
+
+ BlueLight: charmtone.Malibu,
+ BlueDark: charmtone.Sapphire,
+ Blue: charmtone.Guppy,
+
+ Yellow: charmtone.Mustard,
+ Citron: charmtone.Citron,
+
+ Green: charmtone.Turtle,
+ GreenDark: charmtone.Guac,
+ GreenLight: charmtone.Bok,
+
+ Red: charmtone.Coral,
+ RedDark: charmtone.Cherry,
+ RedLight: charmtone.Salmon,
+ Cherry: charmtone.Cherry,
+
+ // Markdown colors (light theme - accessible on Salt background).
+ MdText: charmtone.Pepper, // 14.26:1 contrast
+ MdHeading: lipgloss.Color("#0066CC"), // Dark blue, 4.86:1
+ MdH6: lipgloss.Color("#006644"), // Forest green, 6.14:1
+ MdHRule: charmtone.Oyster, // 5.48:1
+ MdLink: lipgloss.Color("#007777"), // Dark teal, 4.69:1
+ MdLinkText: lipgloss.Color("#006644"), // Forest green, 6.14:1
+ MdImage: lipgloss.Color("#CC2255"), // Dark red, 4.67:1
+ MdImageText: charmtone.Iron, // 7.37:1
+ MdCodeFg: charmtone.Charcoal, // 9.93:1 on Ash
+ MdCodeBg: charmtone.Ash, // Light gray background
+ MdCodeBlockFg: charmtone.Pepper, // Dark text
+ MdCodeBlockBg: charmtone.Ash, // Light gray background
+ MdComment: charmtone.Oyster, // 5.48:1
+ MdKeyword: lipgloss.Color("#0066CC"), // Dark blue, 4.86:1
+ MdKeywordAlt: lipgloss.Color("#5533CC"), // Dark purple, 6.71:1
+ MdKeywordType: charmtone.Sapphire, // 4.98:1
+ MdOperator: lipgloss.Color("#CC2255"), // Dark red, 4.67:1
+ MdPunctuation: charmtone.Charcoal, // 9.93:1 (can't use bright yellow)
+ MdName: charmtone.Charcoal, // 9.93:1
+ MdNameBuiltin: lipgloss.Color("#CC2255"), // Dark red, 4.67:1
+ MdNameTag: lipgloss.Color("#8B008B"), // Dark magenta, 7.42:1
+ MdNameAttr: lipgloss.Color("#4B0082"), // Indigo, 11.31:1
+ MdNameClass: charmtone.Pepper, // Darkest, bold
+ MdNameDecorator: lipgloss.Color("#B7410E"), // Rust, 4.86:1
+ MdNameFunc: lipgloss.Color("#006644"), // Forest green, 6.14:1
+ MdNumber: lipgloss.Color("#800080"), // Purple, 8.22:1
+ MdString: lipgloss.Color("#8B4513"), // Saddle brown, 6.20:1
+ MdStringEscape: lipgloss.Color("#006644"), // Forest green, 6.14:1
+ MdDeleted: lipgloss.Color("#CC2255"), // Dark red, 4.67:1
+ MdInserted: lipgloss.Color("#006644"), // Forest green, 6.14:1
+ MdSubheading: charmtone.Iron, // 7.37:1
+ MdError: charmtone.Pepper, // Dark text
+ MdErrorBg: lipgloss.Color("#FFCCCC"), // Light red background
+
+ // Diff colors (light theme - light backgrounds with dark text).
+ DiffInsertFg: lipgloss.Color("#006644"), // Dark green text
+ DiffInsertBg: lipgloss.Color("#D4EDDA"), // Light green bg
+ DiffInsertSymBg: lipgloss.Color("#C3E6CB"), // Slightly darker green
+ DiffInsertCodeBg: lipgloss.Color("#D4EDDA"), // Light green bg
+ DiffDeleteFg: lipgloss.Color("#CC2255"), // Dark red text
+ DiffDeleteBg: lipgloss.Color("#F8D7DA"), // Light red bg
+ DiffDeleteSymBg: lipgloss.Color("#F5C6CB"), // Slightly darker red
+ DiffDeleteCodeBg: lipgloss.Color("#F8D7DA"), // Light red bg
+ }
+
+ // Text selection.
+ t.TextSelection = lipgloss.NewStyle().Foreground(charmtone.Salt).Background(charmtone.Charple)
+
+ // LSP and MCP status (improved accessibility).
+ t.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Oyster).SetString("●")
+ t.ItemBusyIcon = t.ItemOfflineIcon.Foreground(charmtone.Citron)
+ t.ItemErrorIcon = t.ItemOfflineIcon.Foreground(charmtone.Cherry)
+ t.ItemOnlineIcon = t.ItemOfflineIcon.Foreground(charmtone.Turtle)
+
+ t.YoloIconFocused = lipgloss.NewStyle().Foreground(charmtone.Pepper).Background(charmtone.Citron).Bold(true).SetString(" ! ")
+ t.YoloIconBlurred = t.YoloIconFocused.Foreground(charmtone.Salt).Background(charmtone.Iron)
+ t.YoloDotsFocused = lipgloss.NewStyle().Foreground(charmtone.Zest).SetString(":::")
+ t.YoloDotsBlurred = t.YoloDotsFocused.Foreground(charmtone.Oyster)
+
+ return t
+}
@@ -76,6 +76,51 @@ type Theme struct {
RedLight color.Color
Cherry color.Color
+ // Markdown colors (theme-aware for accessibility).
+ MdText color.Color // Document text
+ MdHeading color.Color // Heading color
+ MdH6 color.Color // H6 color
+ MdHRule color.Color // Horizontal rule
+ MdLink color.Color // Link URL
+ MdLinkText color.Color // Link text
+ MdImage color.Color // Image URL
+ MdImageText color.Color // Image alt text
+ MdCodeFg color.Color // Inline code foreground
+ MdCodeBg color.Color // Inline code background
+ MdCodeBlockFg color.Color // Code block text
+ MdCodeBlockBg color.Color // Code block background
+ MdComment color.Color // Code comment
+ MdKeyword color.Color // Keyword
+ MdKeywordAlt color.Color // Keyword reserved/namespace
+ MdKeywordType color.Color // Type keyword
+ MdOperator color.Color // Operator
+ MdPunctuation color.Color // Punctuation
+ MdName color.Color // Name/identifier
+ MdNameBuiltin color.Color // Builtin name
+ MdNameTag color.Color // Tag name
+ MdNameAttr color.Color // Attribute name
+ MdNameClass color.Color // Class name
+ MdNameDecorator color.Color // Decorator
+ MdNameFunc color.Color // Function name
+ MdNumber color.Color // Literal number
+ MdString color.Color // Literal string
+ MdStringEscape color.Color // String escape
+ MdDeleted color.Color // Deleted text
+ MdInserted color.Color // Inserted text
+ MdSubheading color.Color // Subheading
+ MdError color.Color // Error foreground
+ MdErrorBg color.Color // Error background
+
+ // Diff colors (theme-aware).
+ DiffInsertFg color.Color // Insert line number fg
+ DiffInsertBg color.Color // Insert line number bg
+ DiffInsertSymBg color.Color // Insert symbol bg
+ DiffInsertCodeBg color.Color // Insert code bg
+ DiffDeleteFg color.Color // Delete line number fg
+ DiffDeleteBg color.Color // Delete line number bg
+ DiffDeleteSymBg color.Color // Delete symbol bg
+ DiffDeleteCodeBg color.Color // Delete code bg
+
// Text selection.
TextSelection lipgloss.Style
@@ -221,7 +266,7 @@ func (t *Theme) buildStyles() *Styles {
StylePrimitive: ansi.StylePrimitive{
// BlockPrefix: "\n",
// BlockSuffix: "\n",
- Color: stringPtr(charmtone.Smoke.Hex()),
+ Color: stringPtr(ToHex(t.MdText)),
},
// Margin: uintPtr(defaultMargin),
},
@@ -236,7 +281,7 @@ func (t *Theme) buildStyles() *Styles {
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
- Color: stringPtr(charmtone.Malibu.Hex()),
+ Color: stringPtr(ToHex(t.MdHeading)),
Bold: boolPtr(true),
},
},
@@ -272,7 +317,7 @@ func (t *Theme) buildStyles() *Styles {
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ",
- Color: stringPtr(charmtone.Guac.Hex()),
+ Color: stringPtr(ToHex(t.MdH6)),
Bold: boolPtr(false),
},
},
@@ -286,7 +331,7 @@ func (t *Theme) buildStyles() *Styles {
Bold: boolPtr(true),
},
HorizontalRule: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Charcoal.Hex()),
+ Color: stringPtr(ToHex(t.MdHRule)),
Format: "\n--------\n",
},
Item: ansi.StylePrimitive{
@@ -301,117 +346,117 @@ func (t *Theme) buildStyles() *Styles {
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Zinc.Hex()),
+ Color: stringPtr(ToHex(t.MdLink)),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Guac.Hex()),
+ Color: stringPtr(ToHex(t.MdLinkText)),
Bold: boolPtr(true),
},
Image: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Cheeky.Hex()),
+ Color: stringPtr(ToHex(t.MdImage)),
Underline: boolPtr(true),
},
ImageText: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Squid.Hex()),
+ Color: stringPtr(ToHex(t.MdImageText)),
Format: "Image: {{.text}} →",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: " ",
Suffix: " ",
- Color: stringPtr(charmtone.Coral.Hex()),
- BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
+ Color: stringPtr(ToHex(t.MdCodeFg)),
+ BackgroundColor: stringPtr(ToHex(t.MdCodeBg)),
},
},
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Charcoal.Hex()),
+ Color: stringPtr(ToHex(t.MdCodeBlockFg)),
},
Margin: uintPtr(defaultMargin),
},
Chroma: &ansi.Chroma{
Text: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Smoke.Hex()),
+ Color: stringPtr(ToHex(t.MdText)),
},
Error: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Butter.Hex()),
- BackgroundColor: stringPtr(charmtone.Sriracha.Hex()),
+ Color: stringPtr(ToHex(t.MdError)),
+ BackgroundColor: stringPtr(ToHex(t.MdErrorBg)),
},
Comment: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Oyster.Hex()),
+ Color: stringPtr(ToHex(t.MdComment)),
},
CommentPreproc: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Bengal.Hex()),
+ Color: stringPtr(ToHex(t.MdComment)),
},
Keyword: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Malibu.Hex()),
+ Color: stringPtr(ToHex(t.MdKeyword)),
},
KeywordReserved: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Pony.Hex()),
+ Color: stringPtr(ToHex(t.MdKeywordAlt)),
},
KeywordNamespace: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Pony.Hex()),
+ Color: stringPtr(ToHex(t.MdKeywordAlt)),
},
KeywordType: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Guppy.Hex()),
+ Color: stringPtr(ToHex(t.MdKeywordType)),
},
Operator: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Salmon.Hex()),
+ Color: stringPtr(ToHex(t.MdOperator)),
},
Punctuation: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Zest.Hex()),
+ Color: stringPtr(ToHex(t.MdPunctuation)),
},
Name: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Smoke.Hex()),
+ Color: stringPtr(ToHex(t.MdName)),
},
NameBuiltin: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Cheeky.Hex()),
+ Color: stringPtr(ToHex(t.MdNameBuiltin)),
},
NameTag: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Mauve.Hex()),
+ Color: stringPtr(ToHex(t.MdNameTag)),
},
NameAttribute: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Hazy.Hex()),
+ Color: stringPtr(ToHex(t.MdNameAttr)),
},
NameClass: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Salt.Hex()),
+ Color: stringPtr(ToHex(t.MdNameClass)),
Underline: boolPtr(true),
Bold: boolPtr(true),
},
NameDecorator: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Citron.Hex()),
+ Color: stringPtr(ToHex(t.MdNameDecorator)),
},
NameFunction: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Guac.Hex()),
+ Color: stringPtr(ToHex(t.MdNameFunc)),
},
LiteralNumber: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Julep.Hex()),
+ Color: stringPtr(ToHex(t.MdNumber)),
},
LiteralString: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Cumin.Hex()),
+ Color: stringPtr(ToHex(t.MdString)),
},
LiteralStringEscape: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Bok.Hex()),
+ Color: stringPtr(ToHex(t.MdStringEscape)),
},
GenericDeleted: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Coral.Hex()),
+ Color: stringPtr(ToHex(t.MdDeleted)),
},
GenericEmph: ansi.StylePrimitive{
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Guac.Hex()),
+ Color: stringPtr(ToHex(t.MdInserted)),
},
GenericStrong: ansi.StylePrimitive{
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
- Color: stringPtr(charmtone.Squid.Hex()),
+ Color: stringPtr(ToHex(t.MdSubheading)),
},
Background: ansi.StylePrimitive{
- BackgroundColor: stringPtr(charmtone.Charcoal.Hex()),
+ BackgroundColor: stringPtr(ToHex(t.MdCodeBlockBg)),
},
},
},
@@ -460,23 +505,23 @@ func (t *Theme) buildStyles() *Styles {
},
InsertLine: diffview.LineStyle{
LineNumber: lipgloss.NewStyle().
- Foreground(lipgloss.Color("#629657")).
- Background(lipgloss.Color("#2b322a")),
+ Foreground(t.DiffInsertFg).
+ Background(t.DiffInsertBg),
Symbol: lipgloss.NewStyle().
- Foreground(lipgloss.Color("#629657")).
- Background(lipgloss.Color("#323931")),
+ Foreground(t.DiffInsertFg).
+ Background(t.DiffInsertSymBg),
Code: lipgloss.NewStyle().
- Background(lipgloss.Color("#323931")),
+ Background(t.DiffInsertCodeBg),
},
DeleteLine: diffview.LineStyle{
LineNumber: lipgloss.NewStyle().
- Foreground(lipgloss.Color("#a45c59")).
- Background(lipgloss.Color("#312929")),
+ Foreground(t.DiffDeleteFg).
+ Background(t.DiffDeleteBg),
Symbol: lipgloss.NewStyle().
- Foreground(lipgloss.Color("#a45c59")).
- Background(lipgloss.Color("#383030")),
+ Foreground(t.DiffDeleteFg).
+ Background(t.DiffDeleteSymBg),
Code: lipgloss.NewStyle().
- Background(lipgloss.Color("#383030")),
+ Background(t.DiffDeleteCodeBg),
},
},
FilePicker: filepicker.Styles{
@@ -525,9 +570,11 @@ func NewManager() *Manager {
themes: make(map[string]*Theme),
}
- t := NewCharmtoneTheme() // default theme
- m.Register(t)
- m.current = m.themes[t.Name]
+ dark := NewCharmtoneTheme()
+ light := NewLightTheme()
+ m.Register(dark)
+ m.Register(light)
+ m.current = dark // default to dark theme
return m
}
@@ -563,6 +610,12 @@ func ParseHex(hex string) color.Color {
return color.RGBA{R: r, G: g, B: b, A: 255}
}
+// ToHex converts a color.Color to hex string.
+func ToHex(c color.Color) string {
+ r, g, b, _ := c.RGBA()
+ return fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)
+}
+
// Alpha returns a color with transparency
func Alpha(c color.Color, alpha uint8) color.Color {
r, g, b, _ := c.RGBA()