diff --git a/config/art.go b/config/art.go index 6bf1557..b901409 100644 --- a/config/art.go +++ b/config/art.go @@ -7,68 +7,29 @@ import ( "strings" ) -//nolint:unused -const artFire = "▓▓▓▓▓▓╗ ▓▓ ▓▓ ▓▓ ▓▓\n╚═▒▒═╝ ▒▒ ▒▒ ▒▒ ▒▒\n ▒▒ ▒▒ ▒▒▒▒ ▒▒\n ░░ ░░ ░░ ░░ ░░\n ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝" - const artDots = "▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒\n▒ ● ● ● ▓ ● ▓ ● ▓ ● ▓ ● ▒\n▒ ▓ ● ▓ ▓ ● ▓ ● ● ▓ ▓ ● ▒\n▒ ▓ ● ▓ ▓ ● ▓ ● ▓ ● ▓ ● ▒\n▒ ▓ ● ▓ ▓ ● ▓ ● ▓ ● ▓ ● ▒\n▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒" -// fireGradient is the color scheme for artFire (yellow → orange → red) -// -//nolint:unused -var fireGradient = []string{"#FFDC00", "#FFAA00", "#FF7800", "#FF5000", "#B42800"} - -// dotsGradient is the color scheme for artDots (bright cyan → blue gradient) -// Each character type gets a different color: -// ● (dot) = bright cyan (text) -// ▓ (dark shade) = medium blue (near) -// ▒ (medium shade) = dark blue (far) -var dotsGradient = []string{"#40E0D0", "#4682B4", "#324664"} - -// var currentArt = artFire -// var currentGradient = fireGradient -var currentArt = artDots -var currentGradient = dotsGradient - -// GetArtTView returns the art logo formatted for tview (with tview color codes) -// uses the current gradient colors +// GetArtTView returns the art logo formatted for tview (with tview color codes). +// Colors are sourced from the palette via ColorConfig. func GetArtTView() string { - if currentArt == artDots { - // For dots art, color by character type, not by row - return getDotsArtTView() - } + colors := GetColors() + dotColor := colors.LogoDotColor.Hex() + shadeColor := colors.LogoShadeColor.Hex() + borderColor := colors.LogoBorderColor.Hex() - // For other art, color by row - lines := strings.Split(currentArt, "\n") - var result strings.Builder - - for i, line := range lines { - // pick color based on line index (cycle if more lines than colors) - colorIdx := i - if colorIdx >= len(currentGradient) { - colorIdx = len(currentGradient) - 1 - } - color := currentGradient[colorIdx] - fmt.Fprintf(&result, "[%s]%s[white]\n", color, line) - } - return result.String() -} - -// getDotsArtTView colors the dots art by character type -func getDotsArtTView() string { lines := strings.Split(artDots, "\n") var result strings.Builder - // dotsGradient: [0]=● (text), [1]=▓ (near), [2]=▒ (far) for _, line := range lines { for _, char := range line { var color string switch char { case '●': - color = dotsGradient[0] // bright cyan + color = dotColor case '▓': - color = dotsGradient[1] // medium blue + color = shadeColor case '▒': - color = dotsGradient[2] // dark blue + color = borderColor default: result.WriteRune(char) continue @@ -79,8 +40,3 @@ func getDotsArtTView() string { } return result.String() } - -// GetFireIcon returns fire icon with tview color codes -func GetFireIcon() string { - return "[#FFDC00] ░ ▒ ░ \n[#FFAA00] ▒▓██▓█▒░ \n[#FF7800] ░▓████▓██▒░ \n[#FF5000] ▒▓██▓▓▒░ \n[#B42800] ▒▓░ \n[white]\n" -} diff --git a/config/colors.go b/config/colors.go index 1f76181..44b4c56 100644 --- a/config/colors.go +++ b/config/colors.go @@ -18,8 +18,6 @@ type ColorConfig struct { CaptionFallbackGradient Gradient // Task box colors - TaskBoxSelectedBackground Color - TaskBoxSelectedText Color TaskBoxSelectedBorder Color TaskBoxUnselectedBorder Color TaskBoxUnselectedBackground Color @@ -100,6 +98,11 @@ type ColorConfig struct { FallbackTaskIDColor Color // Deep Sky Blue (end of task ID gradient) FallbackBurndownColor Color // Purple (start of burndown gradient) + // Logo colors (header art) + LogoDotColor Color // bright turquoise (● dots) + LogoShadeColor Color // medium blue (▓ shade) + LogoBorderColor Color // dark blue (▒ border) + // Statusline colors (bottom bar, powerline style) StatuslineBg Color StatuslineFg Color @@ -123,24 +126,28 @@ type Palette struct { SoftTextColor Color // #b4b4b4 — secondary readable text (task box titles, action labels) AccentColor Color // #008000 — label text (green) ValueColor Color // #8c92ac — field values (cool gray) - TagFgColor Color // #b4c8dc — tag chip foreground (light blue-gray) - TagBgColor Color // #1e3278 — tag chip background (dark blue) InfoLabelColor Color // #ffa500 — orange, header view name // Selection SelectionBgColor Color // #3a5f8a — steel blue selection row background - SelectionFgColor Color // ANSI 33 blue — selected task box background - SelectionText Color // ANSI 117 — selected task box text // Action key / accent blue AccentBlue Color // #5fafff — cyan-blue (action keys, points bar, chart bars) SlateColor Color // #5f6982 — muted blue-gray (tag values, unfilled bar segments) + // Logo + LogoDotColor Color // #40e0d0 — bright turquoise (● in header art) + LogoShadeColor Color // #4682b4 — steel blue (▓ in header art) + LogoBorderColor Color // #324664 — dark navy (▒ in header art) + // Gradients CaptionFallbackGradient Gradient // Midnight Blue → Royal Blue DeepSkyBlue Color // #00bfff — task ID base color + gradient fallback DeepPurple Color // #865ad6 — fallback for burndown gradient + // Content area + ContentBackgroundColor Color // canvas background (dark: black, light: transparent/default) + // Statusline (Nord palette) NordPolarNight1 Color // #2e3440 NordPolarNight2 Color // #3b4252 @@ -150,8 +157,8 @@ type Palette struct { NordAuroraGreen Color // #a3be8c } -// DefaultPalette returns the default color palette. -func DefaultPalette() Palette { +// DarkPalette returns the color palette for dark backgrounds. +func DarkPalette() Palette { return Palette{ HighlightColor: NewColorHex("#ffff00"), TextColor: NewColorHex("#ffffff"), @@ -160,17 +167,17 @@ func DefaultPalette() Palette { SoftTextColor: NewColorHex("#b4b4b4"), AccentColor: NewColor(tcell.ColorGreen), ValueColor: NewColorHex("#8c92ac"), - TagFgColor: NewColorRGB(180, 200, 220), - TagBgColor: NewColorRGB(30, 50, 120), InfoLabelColor: NewColorHex("#ffa500"), SelectionBgColor: NewColorHex("#3a5f8a"), - SelectionFgColor: NewColor(tcell.PaletteColor(33)), - SelectionText: NewColor(tcell.PaletteColor(117)), AccentBlue: NewColorHex("#5fafff"), SlateColor: NewColorHex("#5f6982"), + LogoDotColor: NewColorHex("#40e0d0"), + LogoShadeColor: NewColorHex("#4682b4"), + LogoBorderColor: NewColorHex("#324664"), + CaptionFallbackGradient: Gradient{ Start: [3]int{25, 25, 112}, End: [3]int{65, 105, 225}, @@ -178,6 +185,8 @@ func DefaultPalette() Palette { DeepSkyBlue: NewColorRGB(0, 191, 255), DeepPurple: NewColorRGB(134, 90, 214), + ContentBackgroundColor: NewColor(tcell.ColorBlack), + NordPolarNight1: NewColorHex("#2e3440"), NordPolarNight2: NewColorHex("#3b4252"), NordPolarNight3: NewColorHex("#434c5e"), @@ -187,9 +196,48 @@ func DefaultPalette() Palette { } } -// DefaultColors returns the default color configuration built from the default palette. +// LightPalette returns the color palette for light backgrounds. +func LightPalette() Palette { + return Palette{ + HighlightColor: NewColorHex("#0055dd"), // vivid blue — accents, focus markers, key bindings + TextColor: NewColor(tcell.ColorBlack), + TransparentColor: DefaultColor(), + MutedColor: NewColorHex("#808080"), // medium gray — de-emphasized text, placeholders + SoftTextColor: NewColorHex("#404040"), // dark gray — secondary readable text + AccentColor: NewColorHex("#006400"), // dark green — labels + ValueColor: NewColorHex("#4a4e6a"), // dark cool gray — field values + InfoLabelColor: NewColorHex("#b85c00"), // darker orange — header view name + + SelectionBgColor: NewColorHex("#b8d4f0"), // light blue — selection background + + AccentBlue: NewColorHex("#0060c0"), // darker blue — action keys, points bar + SlateColor: NewColorHex("#7080a0"), // blue-gray — tag values, unfilled bar segments + + LogoDotColor: NewColorHex("#20a090"), // darker turquoise + LogoShadeColor: NewColorHex("#3060a0"), // medium blue + LogoBorderColor: NewColorHex("#6080a0"), // lighter blue-gray (visible on light bg) + + CaptionFallbackGradient: Gradient{ + Start: [3]int{100, 140, 200}, + End: [3]int{60, 100, 180}, + }, + DeepSkyBlue: NewColorRGB(0, 100, 180), + DeepPurple: NewColorRGB(90, 50, 160), + + ContentBackgroundColor: DefaultColor(), // transparent — inherit terminal background + + NordPolarNight1: NewColorHex("#eceff4"), // inverted: light background + NordPolarNight2: NewColorHex("#e5e9f0"), + NordPolarNight3: NewColorHex("#d8dee9"), + NordSnowStorm1: NewColorHex("#2e3440"), // inverted: dark text + NordFrostBlue: NewColorHex("#5e81ac"), // stays — good contrast on light + NordAuroraGreen: NewColorHex("#4c7a5a"), // darker green for light bg + } +} + +// DefaultColors returns the default color configuration built from the dark palette. func DefaultColors() *ColorConfig { - return ColorsFromPalette(DefaultPalette()) + return ColorsFromPalette(DarkPalette()) } // darkenRGB returns a darkened version of an RGB triple. ratio 0 = no change, 1 = black. @@ -220,8 +268,6 @@ func ColorsFromPalette(p Palette) *ColorConfig { CaptionFallbackGradient: p.CaptionFallbackGradient, // Task box - TaskBoxSelectedBackground: p.SelectionFgColor, - TaskBoxSelectedText: p.SelectionText, TaskBoxSelectedBorder: p.HighlightColor, TaskBoxUnselectedBorder: p.MutedColor, TaskBoxUnselectedBackground: p.TransparentColor, @@ -246,12 +292,12 @@ func ColorsFromPalette(p Palette) *ColorConfig { TaskDetailEditDimValueColor: p.SoftTextColor, TaskDetailEditFocusMarker: p.HighlightColor, TaskDetailEditFocusText: p.TextColor, - TaskDetailTagForeground: p.TagFgColor, - TaskDetailTagBackground: p.TagBgColor, + TaskDetailTagForeground: p.SoftTextColor, + TaskDetailTagBackground: p.SelectionBgColor, TaskDetailPlaceholderColor: p.MutedColor, // Content area - ContentBackgroundColor: NewColor(tcell.ColorBlack), + ContentBackgroundColor: p.ContentBackgroundColor, ContentTextColor: p.TextColor, // Search box @@ -302,6 +348,11 @@ func ColorsFromPalette(p Palette) *ColorConfig { FallbackTaskIDColor: p.DeepSkyBlue, FallbackBurndownColor: p.DeepPurple, + // Logo + LogoDotColor: p.LogoDotColor, + LogoShadeColor: p.LogoShadeColor, + LogoBorderColor: p.LogoBorderColor, + // Statusline StatuslineBg: p.NordPolarNight3, StatuslineFg: p.NordSnowStorm1, @@ -327,20 +378,13 @@ var UseGradients bool // Screen-wide gradients show more banding on 256-color terminals, so require truecolor var UseWideGradients bool -// GetColors returns the global color configuration with theme-aware overrides +// GetColors returns the global color configuration for the effective theme func GetColors() *ColorConfig { if !colorsInitialized { - globalColors = DefaultColors() - // Apply theme-aware overrides for critical text colors if GetEffectiveTheme() == "light" { - black := NewColor(tcell.ColorBlack) - globalColors.ContentBackgroundColor = DefaultColor() - globalColors.ContentTextColor = black - globalColors.SearchBoxLabelColor = black - globalColors.SearchBoxTextColor = black - globalColors.InputFieldTextColor = black - globalColors.TaskDetailEditFocusText = black - globalColors.HeaderKeyText = black + globalColors = ColorsFromPalette(LightPalette()) + } else { + globalColors = ColorsFromPalette(DarkPalette()) } colorsInitialized = true } diff --git a/config/loader.go b/config/loader.go index 97682c8..0ba1ffc 100644 --- a/config/loader.go +++ b/config/loader.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/muesli/termenv" "github.com/spf13/pflag" "github.com/spf13/viper" "gopkg.in/yaml.v3" @@ -354,24 +355,19 @@ func GetTheme() string { return theme } -// GetEffectiveTheme resolves "auto" to actual theme based on terminal detection +// GetEffectiveTheme resolves "auto" to actual theme based on terminal detection. +// Uses termenv OSC 11 query to detect the terminal's actual background color, +// falling back to COLORFGBG env var, then dark. func GetEffectiveTheme() string { theme := GetTheme() if theme != "auto" { return theme } - // Detect via COLORFGBG env var (format: "fg;bg") - if colorfgbg := os.Getenv("COLORFGBG"); colorfgbg != "" { - parts := strings.Split(colorfgbg, ";") - if len(parts) >= 2 { - bg := parts[len(parts)-1] - // 0-7 = dark colors, 8+ = light colors - if bg >= "8" { - return "light" - } - } + output := termenv.NewOutput(os.Stdout) + if output.HasDarkBackground() { + return "dark" } - return "dark" // default fallback + return "light" } // GetGradientThreshold returns the minimum color count required for gradients diff --git a/internal/bootstrap/init.go b/internal/bootstrap/init.go index 83f2fa5..c9ac552 100644 --- a/internal/bootstrap/init.go +++ b/internal/bootstrap/init.go @@ -297,5 +297,9 @@ func InitColorAndGradientSupport(cfg *config.Config) *sysinfo.SystemInfo { "wideGradients", config.UseWideGradients) } + // set tview global background so all primitives inherit the theme background + colors := config.GetColors() + tview.Styles.PrimitiveBackgroundColor = colors.ContentBackgroundColor.TCell() + return systemInfo } diff --git a/internal/viewer/markdown_viewer.go b/internal/viewer/markdown_viewer.go index 35adf2b..f090ab1 100644 --- a/internal/viewer/markdown_viewer.go +++ b/internal/viewer/markdown_viewer.go @@ -201,21 +201,25 @@ func updateStatusBar(statusBar *tview.TextView, v *navtview.TextViewViewer) { canBack := core.CanGoBack() canForward := core.CanGoForward() - keyColor := "gray" - activeColor := "white" - status := fmt.Sprintf(" [yellow]%s[-] | Link:[%s]Tab/Shift-Tab[-] | Back:", fileName, keyColor) + colors := config.GetColors() + labelColor := colors.TaskBoxTitleColor.Hex() + keyColor := colors.CompletionHintColor.Hex() + activeColor := colors.ContentTextColor.Hex() + mutedColor := colors.CompletionHintColor.Hex() + accentColor := colors.HeaderInfoLabel.Tag().Bold().String() + status := fmt.Sprintf(" %s%s[-] | [%s]Link:[-][%s]Tab/Shift-Tab[-] | [%s]Back:[-]", accentColor, fileName, labelColor, keyColor, labelColor) if canBack { status += fmt.Sprintf("[%s]◀[-]", activeColor) } else { - status += "[gray]◀[-]" + status += fmt.Sprintf("[%s]◀[-]", mutedColor) } - status += " Fwd:" + status += fmt.Sprintf(" [%s]Fwd:[-]", labelColor) if canForward { status += fmt.Sprintf("[%s]▶[-]", activeColor) } else { - status += "[gray]▶[-]" + status += fmt.Sprintf("[%s]▶[-]", mutedColor) } - status += fmt.Sprintf(" | Scroll:[%s]j/k[-] Top/End:[%s]g/G[-] Refresh:[%s]r[-] Edit:[%s]e[-] Quit:[%s]q[-]", keyColor, keyColor, keyColor, keyColor, keyColor) + status += fmt.Sprintf(" | [%s]Scroll:[-][%s]j/k[-] [%s]Top/End:[-][%s]g/G[-] [%s]Refresh:[-][%s]r[-] [%s]Edit:[-][%s]e[-] [%s]Quit:[-][%s]q[-]", labelColor, keyColor, labelColor, keyColor, labelColor, keyColor, labelColor, keyColor, labelColor, keyColor) statusBar.SetText(status) } diff --git a/view/taskdetail/render_helpers.go b/view/taskdetail/render_helpers.go index 8e46404..a8f6e7d 100644 --- a/view/taskdetail/render_helpers.go +++ b/view/taskdetail/render_helpers.go @@ -66,7 +66,7 @@ func RenderTypeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldType typeDisplay := taskpkg.TypeDisplay(task.Type) if task.Type == "" { - typeDisplay = "[gray](none)[-]" + typeDisplay = ctx.Colors.TaskDetailPlaceholderColor.Tag().String() + "(none)[-]" } labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() diff --git a/view/taskdetail/task_edit_view.go b/view/taskdetail/task_edit_view.go index fa47793..3f0a162 100644 --- a/view/taskdetail/task_edit_view.go +++ b/view/taskdetail/task_edit_view.go @@ -499,7 +499,7 @@ func (ev *TaskEditView) updateValidationState() { // Update border color based on validation if ev.metadataBox != nil { - colors := config.DefaultColors() + colors := config.GetColors() if len(ev.validationErrors) > 0 { ev.metadataBox.SetBorderColor(colors.TaskBoxSelectedBorder.TCell()) } else {