mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
light theme and termenv-based auto-detection
This commit is contained in:
parent
e93675c34f
commit
7656ae91ac
7 changed files with 108 additions and 104 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
104
config/colors.go
104
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue