From 0be985a077e0a0de97caa6221c796f7e4ce1ea6f Mon Sep 17 00:00:00 2001 From: booleanmaybe Date: Fri, 10 Apr 2026 16:07:34 -0400 Subject: [PATCH] group colors by value --- component/barchart/bar_chart.go | 10 +- component/barchart/solid.go | 2 +- component/completion_prompt.go | 6 +- component/date_edit.go | 4 +- component/edit_select_list.go | 4 +- component/int_edit_select.go | 4 +- component/recurrence_edit.go | 4 +- component/task_list.go | 28 +- component/task_list_test.go | 17 +- component/word_list.go | 10 +- component/word_list_test.go | 4 +- config/color.go | 113 +++++++ config/color_test.go | 146 +++++++++ config/colors.go | 445 +++++++++++++++++----------- controller/task_edit_coordinator.go | 15 +- plugin/definition.go | 5 +- plugin/merger.go | 8 +- plugin/merger_test.go | 15 +- plugin/parser.go | 5 +- util/gradient/gradient.go | 6 +- util/gradient/gradient_test.go | 17 +- util/points.go | 18 +- util/points_test.go | 10 +- view/borders.go | 2 +- view/doki_plugin_view.go | 4 +- view/gradient_caption_row.go | 8 +- view/header/chart.go | 2 +- view/header/color_scheme.go | 4 +- view/header/context_help.go | 2 +- view/header/info.go | 12 +- view/markdown/navigable_markdown.go | 2 +- view/search_box.go | 8 +- view/statusline/statusline.go | 16 +- view/statusline/statusline_test.go | 60 ++-- view/task_box.go | 36 ++- view/taskdetail/base.go | 2 +- view/taskdetail/render_helpers.go | 60 ++-- view/taskdetail/task_edit_view.go | 10 +- view/tiki_plugin_view.go | 7 +- 39 files changed, 766 insertions(+), 365 deletions(-) create mode 100644 config/color.go create mode 100644 config/color_test.go diff --git a/component/barchart/bar_chart.go b/component/barchart/bar_chart.go index c236b29..25606ee 100644 --- a/component/barchart/bar_chart.go +++ b/component/barchart/bar_chart.go @@ -64,11 +64,11 @@ type BarChart struct { func DefaultTheme() Theme { colors := config.GetColors() return Theme{ - AxisColor: colors.BurndownChartAxisColor, - LabelColor: colors.BurndownChartLabelColor, - ValueColor: colors.BurndownChartValueColor, - BarColor: colors.BurndownChartBarColor, - BackgroundColor: config.GetColors().ContentBackgroundColor, + AxisColor: colors.BurndownChartAxisColor.TCell(), + LabelColor: colors.BurndownChartLabelColor.TCell(), + ValueColor: colors.BurndownChartValueColor.TCell(), + BarColor: colors.BurndownChartBarColor.TCell(), + BackgroundColor: config.GetColors().ContentBackgroundColor.TCell(), BarGradientFrom: colors.BurndownChartGradientFrom.Start, BarGradientTo: colors.BurndownChartGradientTo.Start, DotChar: '⣿', // braille full cell for dense dot matrix diff --git a/component/barchart/solid.go b/component/barchart/solid.go index 244d0f5..f967774 100644 --- a/component/barchart/solid.go +++ b/component/barchart/solid.go @@ -44,7 +44,7 @@ func barFillColor(bar Bar, row, total int, theme Theme) tcell.Color { // Use adaptive gradient: solid color when gradients disabled if !config.UseGradients { - return config.GetColors().FallbackBurndownColor + return config.GetColors().FallbackBurndownColor.TCell() } t := float64(row) / float64(total-1) diff --git a/component/completion_prompt.go b/component/completion_prompt.go index bf1dfb3..5b18ea2 100644 --- a/component/completion_prompt.go +++ b/component/completion_prompt.go @@ -26,12 +26,12 @@ func NewCompletionPrompt(words []string) *CompletionPrompt { // Configure the input field colors := config.GetColors() - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) cp := &CompletionPrompt{ InputField: inputField, words: words, - hintColor: colors.CompletionHintColor, + hintColor: colors.CompletionHintColor.TCell(), } return cp diff --git a/component/date_edit.go b/component/date_edit.go index c0afc7c..fb981e4 100644 --- a/component/date_edit.go +++ b/component/date_edit.go @@ -28,8 +28,8 @@ type DateEdit struct { func NewDateEdit() *DateEdit { inputField := tview.NewInputField() colors := config.GetColors() - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) de := &DateEdit{ InputField: inputField, diff --git a/component/edit_select_list.go b/component/edit_select_list.go index 87caf3b..6c15ed6 100644 --- a/component/edit_select_list.go +++ b/component/edit_select_list.go @@ -30,8 +30,8 @@ func NewEditSelectList(values []string, allowTyping bool) *EditSelectList { // Configure the input field colors := config.GetColors() - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) esl := &EditSelectList{ InputField: inputField, diff --git a/component/int_edit_select.go b/component/int_edit_select.go index e785e37..f11859a 100644 --- a/component/int_edit_select.go +++ b/component/int_edit_select.go @@ -38,8 +38,8 @@ func NewIntEditSelect(min, max int, allowTyping bool) *IntEditSelect { inputField := tview.NewInputField() colors := config.GetColors() - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) ies := &IntEditSelect{ InputField: inputField, diff --git a/component/recurrence_edit.go b/component/recurrence_edit.go index bf681f3..768a1d6 100644 --- a/component/recurrence_edit.go +++ b/component/recurrence_edit.go @@ -32,8 +32,8 @@ type RecurrenceEdit struct { func NewRecurrenceEdit() *RecurrenceEdit { inputField := tview.NewInputField() colors := config.GetColors() - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) re := &RecurrenceEdit{ InputField: inputField, diff --git a/component/task_list.go b/component/task_list.go index dac3766..36c8d63 100644 --- a/component/task_list.go +++ b/component/task_list.go @@ -23,11 +23,12 @@ type TaskList struct { selectionIndex int idColumnWidth int // computed from widest ID idGradient config.Gradient // gradient for ID text - idFallback tcell.Color // fallback solid color for ID - titleColor string // tview color tag for title, e.g. "[#b8b8b8]" - selectionColor string // tview color tag for selected row highlight - statusDoneColor string // tview color tag for done status indicator - statusPendingColor string // tview color tag for pending status indicator + idFallback config.Color // fallback solid color for ID + titleColor config.Color // color for title text + selectionColor config.Color // foreground color for selected row highlight + selectionBgColor config.Color // background color for selected row highlight + statusDoneColor config.Color // color for done status indicator + statusPendingColor config.Color // color for pending status indicator } // NewTaskList creates a new TaskList with the given maximum visible row count. @@ -39,7 +40,8 @@ func NewTaskList(maxVisibleRows int) *TaskList { idGradient: colors.TaskBoxIDColor, idFallback: colors.FallbackTaskIDColor, titleColor: colors.TaskBoxTitleColor, - selectionColor: colors.TaskListSelectionColor, + selectionColor: colors.TaskListSelectionFg, + selectionBgColor: colors.TaskListSelectionBg, statusDoneColor: colors.TaskListStatusDoneColor, statusPendingColor: colors.TaskListStatusPendingColor, } @@ -92,14 +94,14 @@ func (tl *TaskList) ScrollDown() { } // SetIDColors overrides the gradient and fallback color for the ID column. -func (tl *TaskList) SetIDColors(g config.Gradient, fallback tcell.Color) *TaskList { +func (tl *TaskList) SetIDColors(g config.Gradient, fallback config.Color) *TaskList { tl.idGradient = g tl.idFallback = fallback return tl } -// SetTitleColor overrides the tview color tag for the title column. -func (tl *TaskList) SetTitleColor(color string) *TaskList { +// SetTitleColor overrides the color for the title column. +func (tl *TaskList) SetTitleColor(color config.Color) *TaskList { tl.titleColor = color return tl } @@ -134,9 +136,9 @@ func (tl *TaskList) buildRow(t *task.Task, selected bool, width int) string { // Status indicator: done = checkmark, else circle var statusIndicator string if config.GetStatusRegistry().IsDone(string(t.Status)) { - statusIndicator = tl.statusDoneColor + "\u2713[-]" + statusIndicator = tl.statusDoneColor.Tag().String() + "\u2713[-]" } else { - statusIndicator = tl.statusPendingColor + "\u25CB[-]" + statusIndicator = tl.statusPendingColor.Tag().String() + "\u25CB[-]" } // Gradient-rendered ID, padded to idColumnWidth @@ -151,10 +153,10 @@ func (tl *TaskList) buildRow(t *task.Task, selected bool, width int) string { titleAvailable := max(width-1-1-tl.idColumnWidth-1, 0) truncatedTitle := tview.Escape(util.TruncateText(t.Title, titleAvailable)) - row := fmt.Sprintf("%s %s %s%s[-]", statusIndicator, idText, tl.titleColor, truncatedTitle) + row := fmt.Sprintf("%s %s %s%s[-]", statusIndicator, idText, tl.titleColor.Tag().String(), truncatedTitle) if selected { - row = tl.selectionColor + row + row = tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String() + row } return row diff --git a/component/task_list_test.go b/component/task_list_test.go index 5a16034..edbfb97 100644 --- a/component/task_list_test.go +++ b/component/task_list_test.go @@ -181,7 +181,7 @@ func TestFewerItemsThanViewport(t *testing.T) { func TestSetIDColors(t *testing.T) { tl := NewTaskList(10) g := config.Gradient{Start: [3]int{255, 0, 0}, End: [3]int{0, 255, 0}} - fb := tcell.ColorRed + fb := config.NewColor(tcell.ColorRed) result := tl.SetIDColors(g, fb) if result != tl { @@ -197,12 +197,13 @@ func TestSetIDColors(t *testing.T) { func TestSetTitleColor(t *testing.T) { tl := NewTaskList(10) - result := tl.SetTitleColor("[#ff0000]") + c := config.NewColor(tcell.ColorRed) + result := tl.SetTitleColor(c) if result != tl { t.Error("SetTitleColor should return self for chaining") } - if tl.titleColor != "[#ff0000]" { - t.Errorf("Expected [#ff0000], got %s", tl.titleColor) + if tl.titleColor != c { + t.Errorf("Expected color red, got %v", tl.titleColor) } } @@ -270,14 +271,16 @@ func TestBuildRow(t *testing.T) { t.Run("selected row has selection color prefix", func(t *testing.T) { row := tl.buildRow(pendingTask, true, width) - if !strings.HasPrefix(row, tl.selectionColor) { - t.Errorf("selected row should start with selection color %q", tl.selectionColor) + selTag := tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String() + if !strings.HasPrefix(row, selTag) { + t.Errorf("selected row should start with selection color %q", selTag) } }) t.Run("unselected row has no selection prefix", func(t *testing.T) { row := tl.buildRow(pendingTask, false, width) - if strings.HasPrefix(row, tl.selectionColor) { + selTag := tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String() + if strings.HasPrefix(row, selTag) { t.Error("unselected row should not start with selection color") } }) diff --git a/component/word_list.go b/component/word_list.go index e8507bd..e085204 100644 --- a/component/word_list.go +++ b/component/word_list.go @@ -14,8 +14,8 @@ import ( type WordList struct { *tview.Box words []string - fgColor tcell.Color - bgColor tcell.Color + fgColor config.Color + bgColor config.Color } // NewWordList creates a new WordList component. @@ -43,7 +43,7 @@ func (w *WordList) GetWords() []string { } // SetColors sets the foreground and background colors. -func (w *WordList) SetColors(fg, bg tcell.Color) *WordList { +func (w *WordList) SetColors(fg, bg config.Color) *WordList { w.fgColor = fg w.bgColor = bg return w @@ -58,8 +58,8 @@ func (w *WordList) Draw(screen tcell.Screen) { return } - wordStyle := tcell.StyleDefault.Foreground(w.fgColor).Background(w.bgColor) - spaceStyle := tcell.StyleDefault.Background(config.GetColors().ContentBackgroundColor) + wordStyle := tcell.StyleDefault.Foreground(w.fgColor.TCell()).Background(w.bgColor.TCell()) + spaceStyle := tcell.StyleDefault.Background(config.GetColors().ContentBackgroundColor.TCell()) currentX := x currentY := y diff --git a/component/word_list_test.go b/component/word_list_test.go index 350ff55..6f02440 100644 --- a/component/word_list_test.go +++ b/component/word_list_test.go @@ -60,8 +60,8 @@ func TestGetWords(t *testing.T) { func TestSetColors(t *testing.T) { wl := NewWordList([]string{"test"}) - fg := tcell.ColorRed - bg := tcell.ColorGreen + fg := config.NewColor(tcell.ColorRed) + bg := config.NewColor(tcell.ColorGreen) result := wl.SetColors(fg, bg) diff --git a/config/color.go b/config/color.go new file mode 100644 index 0000000..77d2f80 --- /dev/null +++ b/config/color.go @@ -0,0 +1,113 @@ +package config + +// Unified color type that stores a single color and produces tcell, hex, and tview tag forms. + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" +) + +// Color is a unified color representation backed by tcell.Color. +// Zero value wraps tcell.ColorDefault (transparent/inherit). +type Color struct { + color tcell.Color +} + +// NewColor creates a Color from a tcell.Color value. +func NewColor(c tcell.Color) Color { + return Color{color: c} +} + +// NewColorHex creates a Color from a hex string like "#rrggbb" or "rrggbb". +func NewColorHex(hex string) Color { + return Color{color: tcell.GetColor(hex)} +} + +// NewColorRGB creates a Color from individual R, G, B components (0-255). +func NewColorRGB(r, g, b int32) Color { + return Color{color: tcell.NewRGBColor(r, g, b)} +} + +// DefaultColor returns a Color wrapping tcell.ColorDefault (transparent/inherit). +func DefaultColor() Color { + return Color{color: tcell.ColorDefault} +} + +// TCell returns the underlying tcell.Color for use with tview widget APIs. +func (c Color) TCell() tcell.Color { + return c.color +} + +// RGB returns the red, green, blue components of the color. +func (c Color) RGB() (int32, int32, int32) { + return c.color.RGB() +} + +// Hex returns the color as a "#rrggbb" hex string. +// Returns "-" for ColorDefault (tview's convention for default/transparent). +func (c Color) Hex() string { + if c.color == tcell.ColorDefault { + return "-" + } + r, g, b := c.color.RGB() + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +// Tag returns a ColorTag builder for constructing tview color tags. +func (c Color) Tag() ColorTag { + return ColorTag{fg: c} +} + +// IsDefault returns true if this is the default/transparent color. +func (c Color) IsDefault() bool { + return c.color == tcell.ColorDefault +} + +// ColorTag is a composable builder for tview [fg:bg:attr] color tags. +// Use Color.Tag() to create one, then chain Bold() / WithBg() as needed. +type ColorTag struct { + fg Color + bg *Color + bold bool +} + +// Bold returns a new ColorTag with the bold attribute set. +func (t ColorTag) Bold() ColorTag { + t.bold = true + return t +} + +// WithBg returns a new ColorTag with the given background color. +func (t ColorTag) WithBg(c Color) ColorTag { + t.bg = &c + return t +} + +// String renders the tview color tag string. +// +// Examples: +// +// Color.Tag().String() → "[#rrggbb]" +// Color.Tag().Bold().String() → "[#rrggbb::b]" +// Color.Tag().WithBg(bg).String() → "[#rrggbb:#rrggbb]" +func (t ColorTag) String() string { + fg := t.fg.Hex() + + hasBg := t.bg != nil + if !hasBg && !t.bold { + return "[" + fg + "]" + } + + bg := "-" + if hasBg { + bg = t.bg.Hex() + } + + attr := "" + if t.bold { + attr = "b" + } + + return "[" + fg + ":" + bg + ":" + attr + "]" +} diff --git a/config/color_test.go b/config/color_test.go new file mode 100644 index 0000000..8691633 --- /dev/null +++ b/config/color_test.go @@ -0,0 +1,146 @@ +package config + +import ( + "testing" + + "github.com/gdamore/tcell/v2" +) + +func TestNewColor(t *testing.T) { + c := NewColor(tcell.ColorYellow) + if c.TCell() != tcell.ColorYellow { + t.Errorf("TCell() = %v, want %v", c.TCell(), tcell.ColorYellow) + } +} + +func TestNewColorHex(t *testing.T) { + c := NewColorHex("#ff8000") + r, g, b := c.RGB() + if r != 255 || g != 128 || b != 0 { + t.Errorf("RGB() = (%d, %d, %d), want (255, 128, 0)", r, g, b) + } +} + +func TestNewColorRGB(t *testing.T) { + c := NewColorRGB(10, 20, 30) + r, g, b := c.RGB() + if r != 10 || g != 20 || b != 30 { + t.Errorf("RGB() = (%d, %d, %d), want (10, 20, 30)", r, g, b) + } +} + +func TestDefaultColor(t *testing.T) { + c := DefaultColor() + if !c.IsDefault() { + t.Error("DefaultColor().IsDefault() = false, want true") + } + if c.TCell() != tcell.ColorDefault { + t.Errorf("TCell() = %v, want ColorDefault", c.TCell()) + } +} + +func TestColor_Hex(t *testing.T) { + tests := []struct { + name string + c Color + want string + }{ + {"black", NewColorRGB(0, 0, 0), "#000000"}, + {"white", NewColorRGB(255, 255, 255), "#ffffff"}, + {"red", NewColorRGB(255, 0, 0), "#ff0000"}, + {"default", DefaultColor(), "-"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.Hex(); got != tt.want { + t.Errorf("Hex() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestColor_IsDefault(t *testing.T) { + if NewColor(tcell.ColorWhite).IsDefault() { + t.Error("white.IsDefault() = true, want false") + } + if !NewColor(tcell.ColorDefault).IsDefault() { + t.Error("default.IsDefault() = false, want true") + } +} + +func TestColorTag_String(t *testing.T) { + c := NewColorRGB(255, 128, 0) + got := c.Tag().String() + want := "[#ff8000]" + if got != want { + t.Errorf("Tag().String() = %q, want %q", got, want) + } +} + +func TestColorTag_Bold(t *testing.T) { + c := NewColorRGB(255, 128, 0) + got := c.Tag().Bold().String() + want := "[#ff8000:-:b]" + if got != want { + t.Errorf("Tag().Bold().String() = %q, want %q", got, want) + } +} + +func TestColorTag_WithBg(t *testing.T) { + fg := NewColorRGB(255, 255, 255) + bg := NewColorHex("#3a5f8a") + got := fg.Tag().WithBg(bg).String() + want := "[#ffffff:#3a5f8a:]" + if got != want { + t.Errorf("Tag().WithBg().String() = %q, want %q", got, want) + } +} + +func TestColorTag_BoldWithBg(t *testing.T) { + fg := NewColorRGB(255, 128, 0) + bg := NewColorRGB(0, 0, 0) + got := fg.Tag().Bold().WithBg(bg).String() + want := "[#ff8000:#000000:b]" + if got != want { + t.Errorf("Tag().Bold().WithBg().String() = %q, want %q", got, want) + } +} + +func TestColorTag_WithBgBold(t *testing.T) { + // order shouldn't matter + fg := NewColorRGB(255, 128, 0) + bg := NewColorRGB(0, 0, 0) + got := fg.Tag().WithBg(bg).Bold().String() + want := "[#ff8000:#000000:b]" + if got != want { + t.Errorf("Tag().WithBg().Bold().String() = %q, want %q", got, want) + } +} + +func TestColorTag_DefaultFg(t *testing.T) { + c := DefaultColor() + got := c.Tag().String() + want := "[-]" + if got != want { + t.Errorf("DefaultColor().Tag().String() = %q, want %q", got, want) + } +} + +func TestColorTag_DefaultBg(t *testing.T) { + fg := NewColorRGB(255, 0, 0) + bg := DefaultColor() + got := fg.Tag().WithBg(bg).String() + want := "[#ff0000:-:]" + if got != want { + t.Errorf("Tag().WithBg(default).String() = %q, want %q", got, want) + } +} + +func TestColorHexRoundTrip(t *testing.T) { + original := "#5e81ac" + c := NewColorHex(original) + got := c.Hex() + if got != original { + t.Errorf("hex round-trip: NewColorHex(%q).Hex() = %q", original, got) + } +} diff --git a/config/colors.go b/config/colors.go index 160dfc2..ccfd37c 100644 --- a/config/colors.go +++ b/config/colors.go @@ -1,6 +1,6 @@ package config -// Color and style definitions for the UI: gradients, tcell colors, tview color tags. +// Color and style definitions for the UI: gradients, unified Color values. import ( "github.com/gdamore/tcell/v2" @@ -18,218 +18,330 @@ type ColorConfig struct { CaptionFallbackGradient Gradient // Task box colors - TaskBoxSelectedBackground tcell.Color - TaskBoxSelectedText tcell.Color - TaskBoxSelectedBorder tcell.Color - TaskBoxUnselectedBorder tcell.Color - TaskBoxUnselectedBackground tcell.Color + TaskBoxSelectedBackground Color + TaskBoxSelectedText Color + TaskBoxSelectedBorder Color + TaskBoxUnselectedBorder Color + TaskBoxUnselectedBackground Color TaskBoxIDColor Gradient - TaskBoxTitleColor string // tview color string like "[#b8b8b8]" - TaskBoxLabelColor string // tview color string like "[#767676]" - TaskBoxDescriptionColor string // tview color string like "[#767676]" - TaskBoxTagValueColor string // tview color string like "[#5a6f8f]" - TaskListSelectionColor string // tview color string for selected row highlight, e.g. "[white:#3a5f8a]" - TaskListStatusDoneColor string // tview color string for done status indicator, e.g. "[#00ff7f]" - TaskListStatusPendingColor string // tview color string for pending status indicator, e.g. "[white]" + TaskBoxTitleColor Color + TaskBoxLabelColor Color + TaskBoxDescriptionColor Color + TaskBoxTagValueColor Color + TaskListSelectionFg Color // selected row foreground + TaskListSelectionBg Color // selected row background + TaskListStatusDoneColor Color + TaskListStatusPendingColor Color // Task detail view colors TaskDetailIDColor Gradient - TaskDetailTitleText string // tview color string like "[yellow]" - TaskDetailLabelText string // tview color string like "[green]" - TaskDetailValueText string // tview color string like "[white]" - TaskDetailCommentAuthor string // tview color string like "[yellow]" - TaskDetailEditDimTextColor string // tview color string like "[#808080]" - TaskDetailEditDimLabelColor string // tview color string like "[#606060]" - TaskDetailEditDimValueColor string // tview color string like "[#909090]" - TaskDetailEditFocusMarker string // tview color string like "[yellow]" - TaskDetailEditFocusText string // tview color string like "[white]" - TaskDetailTagForeground tcell.Color - TaskDetailTagBackground tcell.Color - TaskDetailPlaceholderColor tcell.Color + TaskDetailTitleText Color + TaskDetailLabelText Color + TaskDetailValueText Color + TaskDetailCommentAuthor Color + TaskDetailEditDimTextColor Color + TaskDetailEditDimLabelColor Color + TaskDetailEditDimValueColor Color + TaskDetailEditFocusMarker Color + TaskDetailEditFocusText Color + TaskDetailTagForeground Color + TaskDetailTagBackground Color + TaskDetailPlaceholderColor Color // Content area colors (base canvas for editable/readable content) - ContentBackgroundColor tcell.Color - ContentTextColor tcell.Color + ContentBackgroundColor Color + ContentTextColor Color // Search box colors - SearchBoxLabelColor tcell.Color - SearchBoxBackgroundColor tcell.Color - SearchBoxTextColor tcell.Color + SearchBoxLabelColor Color + SearchBoxBackgroundColor Color + SearchBoxTextColor Color // Input field colors (used in task detail edit mode) - InputFieldBackgroundColor tcell.Color - InputFieldTextColor tcell.Color + InputFieldBackgroundColor Color + InputFieldTextColor Color // Completion prompt colors - CompletionHintColor tcell.Color + CompletionHintColor Color // Burndown chart colors - BurndownChartAxisColor tcell.Color - BurndownChartLabelColor tcell.Color - BurndownChartValueColor tcell.Color - BurndownChartBarColor tcell.Color + BurndownChartAxisColor Color + BurndownChartLabelColor Color + BurndownChartValueColor Color + BurndownChartBarColor Color BurndownChartGradientFrom Gradient BurndownChartGradientTo Gradient BurndownHeaderGradientFrom Gradient // Header-specific chart gradient BurndownHeaderGradientTo Gradient // Header view colors - HeaderInfoLabel string // tview color string for view name (bold) - HeaderInfoSeparator string // tview color string for horizontal rule below name - HeaderInfoDesc string // tview color string for view description - HeaderKeyBinding string // tview color string like "[yellow]" - HeaderKeyText string // tview color string like "[white]" + HeaderInfoLabel Color + HeaderInfoSeparator Color + HeaderInfoDesc Color + HeaderKeyBinding Color + HeaderKeyText Color // Points visual bar colors - PointsFilledColor string // tview color string for filled segments - PointsUnfilledColor string // tview color string for unfilled segments + PointsFilledColor Color + PointsUnfilledColor Color // Header context help action colors - HeaderActionGlobalKeyColor string // tview color string for global action keys - HeaderActionGlobalLabelColor string // tview color string for global action labels - HeaderActionPluginKeyColor string // tview color string for plugin action keys - HeaderActionPluginLabelColor string // tview color string for plugin action labels - HeaderActionViewKeyColor string // tview color string for view action keys - HeaderActionViewLabelColor string // tview color string for view action labels + HeaderActionGlobalKeyColor Color + HeaderActionGlobalLabelColor Color + HeaderActionPluginKeyColor Color + HeaderActionPluginLabelColor Color + HeaderActionViewKeyColor Color + HeaderActionViewLabelColor Color // Plugin-specific colors - DepsEditorBackground tcell.Color // muted slate for dependency editor caption + DepsEditorBackground Color // muted slate for dependency editor caption // Fallback solid colors for gradient scenarios (used when UseGradients = false) - FallbackTaskIDColor tcell.Color // Deep Sky Blue (end of task ID gradient) - FallbackBurndownColor tcell.Color // Purple (start of burndown gradient) + FallbackTaskIDColor Color // Deep Sky Blue (end of task ID gradient) + FallbackBurndownColor Color // Purple (start of burndown gradient) // Statusline colors (bottom bar, powerline style) - StatuslineBg string // hex color for stat segment background, e.g. "#3a3a5c" - StatuslineFg string // hex color for stat segment text, e.g. "#cccccc" - StatuslineAccentBg string // hex color for accent segment background (first segment), e.g. "#5f87af" - StatuslineAccentFg string // hex color for accent segment text, e.g. "#1c1c2e" - StatuslineInfoFg string // hex color for info message text - StatuslineInfoBg string // hex color for info message background - StatuslineErrorFg string // hex color for error message text - StatuslineErrorBg string // hex color for error message background - StatuslineFillBg string // hex color for empty statusline area between segments + StatuslineBg Color + StatuslineFg Color + StatuslineAccentBg Color + StatuslineAccentFg Color + StatuslineInfoFg Color + StatuslineInfoBg Color + StatuslineErrorFg Color + StatuslineErrorBg Color + StatuslineFillBg Color } -// DefaultColors returns the default color configuration -func DefaultColors() *ColorConfig { - return &ColorConfig{ - // Caption fallback gradient - CaptionFallbackGradient: Gradient{ - Start: [3]int{25, 25, 112}, // Midnight Blue (center) - End: [3]int{65, 105, 225}, // Royal Blue (edges) +// Palette defines the base color values used throughout the UI. +// Each entry is a semantic name for a unique color; ColorConfig fields reference these. +// To change a color everywhere it appears, change it here. +type Palette struct { + HighlightColor Color // yellow — accents, focus markers, key bindings, borders + TextColor Color // white — primary text on dark background + TransparentColor Color // default/transparent — inherit background + MutedColor Color // #808080 — de-emphasized text, placeholders, hints, unfocused borders + SubduedTextColor Color // #767676 — labels, descriptions + DimLabelColor Color // #606060 — dimmed labels in edit mode + DimValueColor Color // #909090 — dimmed values in edit mode + SoftTextColor Color // #b8b8b8 — titles in task boxes + 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) + SuccessColor Color // #00ff7f — spring green, done indicator + InfoLabelColor Color // #ffa500 — orange, header view name + InfoSepColor Color // #555555 — header separator + InfoDescColor Color // #888888 — header description + + // 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 + + // Tags (task box inline) + TagValueColor Color // #5a6f8f — blueish gray for inline tag values + + // Action key colors (header context help) + ActionKeyColor Color // #ff8c00 — orange for plugin action keys + ActionLabelColor Color // #b0b0b0 — light gray for plugin action labels + ViewActionKeyColor Color // #5fafff — cyan for view-specific action keys + + // Points bar + PointsFilledColor Color // #508cff — blue filled segments + PointsUnfilledColor Color // #5f6982 — gray unfilled segments + + // Chart + ChartAxisColor Color // #505050 — dark gray chart axis + ChartLabelColor Color // #c8c8c8 — light gray chart labels + ChartValueColor Color // #ebebeb — very light gray chart values + ChartBarColor Color // #78aaff — light blue chart bars + + // Gradients (not Color, but part of the palette) + IDGradient Gradient // Dodger Blue → Deep Sky Blue + CaptionFallbackGradient Gradient // Midnight Blue → Royal Blue + DeepSkyBlue Color // #00bfff — fallback for ID gradient + DeepPurple Color // #865ad6 — fallback for burndown gradient + + // Plugin-specific + DepsEditorBgColor Color // #4e5768 — muted slate + + // Statusline (Nord palette) + NordPolarNight1 Color // #2e3440 + NordPolarNight2 Color // #3b4252 + NordPolarNight3 Color // #434c5e + NordSnowStorm1 Color // #d8dee9 + NordFrostBlue Color // #5e81ac + NordAuroraGreen Color // #a3be8c +} + +// DefaultPalette returns the default color palette. +func DefaultPalette() Palette { + return Palette{ + HighlightColor: NewColorHex("#ffff00"), + TextColor: NewColorHex("#ffffff"), + TransparentColor: DefaultColor(), + MutedColor: NewColorHex("#808080"), + SubduedTextColor: NewColorHex("#767676"), + DimLabelColor: NewColorHex("#606060"), + DimValueColor: NewColorHex("#909090"), + SoftTextColor: NewColorHex("#b8b8b8"), + AccentColor: NewColor(tcell.ColorGreen), + ValueColor: NewColorHex("#8c92ac"), + TagFgColor: NewColorRGB(180, 200, 220), + TagBgColor: NewColorRGB(30, 50, 120), + SuccessColor: NewColorHex("#00ff7f"), + InfoLabelColor: NewColorHex("#ffa500"), + InfoSepColor: NewColorHex("#555555"), + InfoDescColor: NewColorHex("#888888"), + + SelectionBgColor: NewColorHex("#3a5f8a"), + SelectionFgColor: NewColor(tcell.PaletteColor(33)), + SelectionText: NewColor(tcell.PaletteColor(117)), + + TagValueColor: NewColorHex("#5a6f8f"), + + ActionKeyColor: NewColorHex("#ff8c00"), + ActionLabelColor: NewColorHex("#b0b0b0"), + ViewActionKeyColor: NewColorHex("#5fafff"), + + PointsFilledColor: NewColorHex("#508cff"), + PointsUnfilledColor: NewColorHex("#5f6982"), + + ChartAxisColor: NewColorRGB(80, 80, 80), + ChartLabelColor: NewColorRGB(200, 200, 200), + ChartValueColor: NewColorRGB(235, 235, 235), + ChartBarColor: NewColorRGB(120, 170, 255), + + IDGradient: Gradient{ + Start: [3]int{30, 144, 255}, + End: [3]int{0, 191, 255}, }, + CaptionFallbackGradient: Gradient{ + Start: [3]int{25, 25, 112}, + End: [3]int{65, 105, 225}, + }, + DeepSkyBlue: NewColorRGB(0, 191, 255), + DeepPurple: NewColorRGB(134, 90, 214), + + DepsEditorBgColor: NewColorHex("#4e5768"), + + NordPolarNight1: NewColorHex("#2e3440"), + NordPolarNight2: NewColorHex("#3b4252"), + NordPolarNight3: NewColorHex("#434c5e"), + NordSnowStorm1: NewColorHex("#d8dee9"), + NordFrostBlue: NewColorHex("#5e81ac"), + NordAuroraGreen: NewColorHex("#a3be8c"), + } +} + +// DefaultColors returns the default color configuration built from the default palette. +func DefaultColors() *ColorConfig { + return ColorsFromPalette(DefaultPalette()) +} + +// ColorsFromPalette builds a ColorConfig from a Palette. +func ColorsFromPalette(p Palette) *ColorConfig { + deepPurpleSolid := Gradient{Start: [3]int{134, 90, 214}, End: [3]int{134, 90, 214}} + blueCyanSolid := Gradient{Start: [3]int{90, 170, 255}, End: [3]int{90, 170, 255}} + headerPurpleSolid := Gradient{Start: [3]int{160, 120, 230}, End: [3]int{160, 120, 230}} + headerCyanSolid := Gradient{Start: [3]int{110, 190, 255}, End: [3]int{110, 190, 255}} + + return &ColorConfig{ + CaptionFallbackGradient: p.CaptionFallbackGradient, // Task box - TaskBoxSelectedBackground: tcell.PaletteColor(33), // Blue (ANSI 33) - TaskBoxSelectedText: tcell.PaletteColor(117), // Light Blue (ANSI 117) - TaskBoxSelectedBorder: tcell.ColorYellow, - TaskBoxUnselectedBorder: tcell.ColorGray, - TaskBoxUnselectedBackground: tcell.ColorDefault, // transparent/no background - TaskBoxIDColor: Gradient{ - Start: [3]int{30, 144, 255}, // Dodger Blue - End: [3]int{0, 191, 255}, // Deep Sky Blue - }, - TaskBoxTitleColor: "[#b8b8b8]", // Light gray - TaskBoxLabelColor: "[#767676]", // Darker gray for labels - TaskBoxDescriptionColor: "[#767676]", // Darker gray for description - TaskBoxTagValueColor: "[#5a6f8f]", // Blueish gray for tag values - TaskListSelectionColor: "[white:#3a5f8a]", // White text on steel blue background - TaskListStatusDoneColor: "[#00ff7f]", // Spring green for done checkmark - TaskListStatusPendingColor: "[white]", // White for pending circle + TaskBoxSelectedBackground: p.SelectionFgColor, + TaskBoxSelectedText: p.SelectionText, + TaskBoxSelectedBorder: p.HighlightColor, + TaskBoxUnselectedBorder: p.MutedColor, + TaskBoxUnselectedBackground: p.TransparentColor, + TaskBoxIDColor: p.IDGradient, + TaskBoxTitleColor: p.SoftTextColor, + TaskBoxLabelColor: p.SubduedTextColor, + TaskBoxDescriptionColor: p.SubduedTextColor, + TaskBoxTagValueColor: p.TagValueColor, + TaskListSelectionFg: p.TextColor, + TaskListSelectionBg: p.SelectionBgColor, + TaskListStatusDoneColor: p.SuccessColor, + TaskListStatusPendingColor: p.TextColor, // Task detail - TaskDetailIDColor: Gradient{ - Start: [3]int{30, 144, 255}, // Dodger Blue (same as task box) - End: [3]int{0, 191, 255}, // Deep Sky Blue - }, - TaskDetailTitleText: "[yellow]", - TaskDetailLabelText: "[green]", - TaskDetailValueText: "[#8c92ac]", - TaskDetailCommentAuthor: "[yellow]", - TaskDetailEditDimTextColor: "[#808080]", // Medium gray for dim text - TaskDetailEditDimLabelColor: "[#606060]", // Darker gray for dim labels - TaskDetailEditDimValueColor: "[#909090]", // Lighter gray for dim values - TaskDetailEditFocusMarker: "[yellow]", // Yellow arrow for focus - TaskDetailEditFocusText: "[white]", // White text after arrow - TaskDetailTagForeground: tcell.NewRGBColor(180, 200, 220), // Light blue-gray text - TaskDetailTagBackground: tcell.NewRGBColor(30, 50, 120), // Dark blue background (more bluish) - TaskDetailPlaceholderColor: tcell.ColorGray, // Gray for placeholder text in edit fields + TaskDetailIDColor: p.IDGradient, + TaskDetailTitleText: p.HighlightColor, + TaskDetailLabelText: p.AccentColor, + TaskDetailValueText: p.ValueColor, + TaskDetailCommentAuthor: p.HighlightColor, + TaskDetailEditDimTextColor: p.MutedColor, + TaskDetailEditDimLabelColor: p.DimLabelColor, + TaskDetailEditDimValueColor: p.DimValueColor, + TaskDetailEditFocusMarker: p.HighlightColor, + TaskDetailEditFocusText: p.TextColor, + TaskDetailTagForeground: p.TagFgColor, + TaskDetailTagBackground: p.TagBgColor, + TaskDetailPlaceholderColor: p.MutedColor, - // Content area (base canvas) - ContentBackgroundColor: tcell.ColorBlack, // dark theme: explicit black - ContentTextColor: tcell.ColorWhite, // dark theme: white text + // Content area + ContentBackgroundColor: NewColor(tcell.ColorBlack), + ContentTextColor: p.TextColor, // Search box - SearchBoxLabelColor: tcell.ColorWhite, - SearchBoxBackgroundColor: tcell.ColorDefault, // Transparent - SearchBoxTextColor: tcell.ColorWhite, + SearchBoxLabelColor: p.TextColor, + SearchBoxBackgroundColor: p.TransparentColor, + SearchBoxTextColor: p.TextColor, - // Input field colors - InputFieldBackgroundColor: tcell.ColorDefault, // Transparent - InputFieldTextColor: tcell.ColorWhite, + // Input field + InputFieldBackgroundColor: p.TransparentColor, + InputFieldTextColor: p.TextColor, // Completion prompt - CompletionHintColor: tcell.NewRGBColor(128, 128, 128), // Medium gray for hint text + CompletionHintColor: p.MutedColor, // Burndown chart - BurndownChartAxisColor: tcell.NewRGBColor(80, 80, 80), // Dark gray - BurndownChartLabelColor: tcell.NewRGBColor(200, 200, 200), // Light gray - BurndownChartValueColor: tcell.NewRGBColor(235, 235, 235), // Very light gray - BurndownChartBarColor: tcell.NewRGBColor(120, 170, 255), // Light blue - BurndownChartGradientFrom: Gradient{ - Start: [3]int{134, 90, 214}, // Deep purple - End: [3]int{134, 90, 214}, // Deep purple (solid, not gradient) - }, - BurndownChartGradientTo: Gradient{ - Start: [3]int{90, 170, 255}, // Blue/cyan - End: [3]int{90, 170, 255}, // Blue/cyan (solid, not gradient) - }, - BurndownHeaderGradientFrom: Gradient{ - Start: [3]int{160, 120, 230}, // Purple base for header chart - End: [3]int{160, 120, 230}, // Purple base (solid) - }, - BurndownHeaderGradientTo: Gradient{ - Start: [3]int{110, 190, 255}, // Cyan top for header chart - End: [3]int{110, 190, 255}, // Cyan top (solid) - }, + BurndownChartAxisColor: p.ChartAxisColor, + BurndownChartLabelColor: p.ChartLabelColor, + BurndownChartValueColor: p.ChartValueColor, + BurndownChartBarColor: p.ChartBarColor, + BurndownChartGradientFrom: deepPurpleSolid, + BurndownChartGradientTo: blueCyanSolid, + BurndownHeaderGradientFrom: headerPurpleSolid, + BurndownHeaderGradientTo: headerCyanSolid, - // Points visual bar - PointsFilledColor: "[#508cff]", // Blue for filled segments - PointsUnfilledColor: "[#5f6982]", // Gray for unfilled segments + // Points bar + PointsFilledColor: p.PointsFilledColor, + PointsUnfilledColor: p.PointsUnfilledColor, // Header - HeaderInfoLabel: "[orange]", - HeaderInfoSeparator: "[#555555]", - HeaderInfoDesc: "[#888888]", - HeaderKeyBinding: "[yellow]", - HeaderKeyText: "[white]", + HeaderInfoLabel: p.InfoLabelColor, + HeaderInfoSeparator: p.InfoSepColor, + HeaderInfoDesc: p.InfoDescColor, + HeaderKeyBinding: p.HighlightColor, + HeaderKeyText: p.TextColor, // Header context help actions - HeaderActionGlobalKeyColor: "#ffff00", // yellow for global actions - HeaderActionGlobalLabelColor: "#ffffff", // white for global action labels - HeaderActionPluginKeyColor: "#ff8c00", // orange for plugin actions - HeaderActionPluginLabelColor: "#b0b0b0", // light gray for plugin labels - HeaderActionViewKeyColor: "#5fafff", // cyan for view-specific actions - HeaderActionViewLabelColor: "#808080", // gray for view-specific labels + HeaderActionGlobalKeyColor: p.HighlightColor, + HeaderActionGlobalLabelColor: p.TextColor, + HeaderActionPluginKeyColor: p.ActionKeyColor, + HeaderActionPluginLabelColor: p.ActionLabelColor, + HeaderActionViewKeyColor: p.ViewActionKeyColor, + HeaderActionViewLabelColor: p.MutedColor, // Plugin-specific - DepsEditorBackground: tcell.NewHexColor(0x4e5768), // Muted slate + DepsEditorBackground: p.DepsEditorBgColor, - // Fallback solid colors (no-gradient terminals) - FallbackTaskIDColor: tcell.NewRGBColor(0, 191, 255), // Deep Sky Blue - FallbackBurndownColor: tcell.NewRGBColor(134, 90, 214), // Purple + // Fallback solid colors + FallbackTaskIDColor: p.DeepSkyBlue, + FallbackBurndownColor: p.DeepPurple, - // Statusline (Nord theme) - StatuslineBg: "#434c5e", // Nord polar night 3 - StatuslineFg: "#d8dee9", // Nord snow storm 1 - StatuslineAccentBg: "#5e81ac", // Nord frost blue - StatuslineAccentFg: "#2e3440", // Nord polar night 1 - StatuslineInfoFg: "#a3be8c", // Nord aurora green - StatuslineInfoBg: "#3b4252", // Nord polar night 2 - StatuslineErrorFg: "#ffff00", // yellow, matches header global key color - StatuslineErrorBg: "#3b4252", // Nord polar night 2 - StatuslineFillBg: "#3b4252", // Nord polar night 2 + // Statusline + StatuslineBg: p.NordPolarNight3, + StatuslineFg: p.NordSnowStorm1, + StatuslineAccentBg: p.NordFrostBlue, + StatuslineAccentFg: p.NordPolarNight1, + StatuslineInfoFg: p.NordAuroraGreen, + StatuslineInfoBg: p.NordPolarNight2, + StatuslineErrorFg: p.HighlightColor, + StatuslineErrorBg: p.NordPolarNight2, + StatuslineFillBg: p.NordPolarNight2, } } @@ -251,13 +363,14 @@ func GetColors() *ColorConfig { globalColors = DefaultColors() // Apply theme-aware overrides for critical text colors if GetEffectiveTheme() == "light" { - globalColors.ContentBackgroundColor = tcell.ColorDefault - globalColors.ContentTextColor = tcell.ColorBlack - globalColors.SearchBoxLabelColor = tcell.ColorBlack - globalColors.SearchBoxTextColor = tcell.ColorBlack - globalColors.InputFieldTextColor = tcell.ColorBlack - globalColors.TaskDetailEditFocusText = "[black]" - globalColors.HeaderKeyText = "[black]" + 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 } colorsInitialized = true } diff --git a/controller/task_edit_coordinator.go b/controller/task_edit_coordinator.go index f8de930..0492233 100644 --- a/controller/task_edit_coordinator.go +++ b/controller/task_edit_coordinator.go @@ -381,9 +381,10 @@ func (c *TaskEditCoordinator) prepareView(activeView View, focus model.EditField _ = c.FocusNextField(activeView) } -// rejectionMessage extracts clean rejection reasons from an error. -// If the error wraps a RejectionError, returns just the reasons without -// the "failed to update task:" / "validation failed:" prefixes. +// rejectionMessage extracts a clean user-facing message from an error. +// For RejectionError: returns just the rejection reasons. +// For other errors: unwraps to the root cause to strip wrapper prefixes +// like "failed to update task: failed to save task:". func rejectionMessage(err error) string { var re *service.RejectionError if errors.As(err, &re) { @@ -393,5 +394,13 @@ func rejectionMessage(err error) string { } return strings.Join(reasons, "; ") } + // unwrap to the innermost error for a clean message + for { + inner := errors.Unwrap(err) + if inner == nil { + break + } + err = inner + } return err.Error() } diff --git a/plugin/definition.go b/plugin/definition.go index 9dbe857..168fe89 100644 --- a/plugin/definition.go +++ b/plugin/definition.go @@ -3,6 +3,7 @@ package plugin import ( "github.com/gdamore/tcell/v2" + "github.com/boolean-maybe/tiki/config" "github.com/boolean-maybe/tiki/ruki" ) @@ -24,8 +25,8 @@ type BasePlugin struct { Key tcell.Key // tcell key constant (e.g. KeyCtrlH) Rune rune // printable character (e.g. 'L') Modifier tcell.ModMask // modifier keys (Alt, Shift, Ctrl, etc.) - Foreground tcell.Color // caption text color - Background tcell.Color // caption background color + Foreground config.Color // caption text color + Background config.Color // caption background color FilePath string // source file path (for error messages) ConfigIndex int // index in workflow.yaml views array (-1 if not from a config file) Type string // plugin type: "tiki" or "doki" diff --git a/plugin/merger.go b/plugin/merger.go index 1767696..d139e57 100644 --- a/plugin/merger.go +++ b/plugin/merger.go @@ -1,9 +1,5 @@ package plugin -import ( - "github.com/gdamore/tcell/v2" -) - // pluginFileConfig represents the YAML structure of a plugin file type pluginFileConfig struct { Name string `yaml:"name"` @@ -58,10 +54,10 @@ func mergePluginDefinitions(base Plugin, override Plugin) Plugin { result.Rune = overrideTiki.Rune result.Modifier = overrideTiki.Modifier } - if overrideTiki.Foreground != tcell.ColorDefault { + if !overrideTiki.Foreground.IsDefault() { result.Foreground = overrideTiki.Foreground } - if overrideTiki.Background != tcell.ColorDefault { + if !overrideTiki.Background.IsDefault() { result.Background = overrideTiki.Background } if len(overrideTiki.Lanes) > 0 { diff --git a/plugin/merger_test.go b/plugin/merger_test.go index d05d68f..e7534a4 100644 --- a/plugin/merger_test.go +++ b/plugin/merger_test.go @@ -5,6 +5,7 @@ import ( "github.com/gdamore/tcell/v2" + "github.com/boolean-maybe/tiki/config" rukiRuntime "github.com/boolean-maybe/tiki/internal/ruki/runtime" "github.com/boolean-maybe/tiki/ruki" ) @@ -30,8 +31,8 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { Key: tcell.KeyRune, Rune: 'B', Modifier: 0, - Foreground: tcell.ColorRed, - Background: tcell.ColorBlue, + Foreground: config.NewColor(tcell.ColorRed), + Background: config.NewColor(tcell.ColorBlue), Type: "tiki", }, Lanes: []TikiLane{ @@ -47,8 +48,8 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { Key: tcell.KeyRune, Rune: 'O', Modifier: tcell.ModAlt, - Foreground: tcell.ColorGreen, - Background: tcell.ColorDefault, + Foreground: config.NewColor(tcell.ColorGreen), + Background: config.DefaultColor(), FilePath: "override.yaml", ConfigIndex: 1, Type: "tiki", @@ -72,7 +73,7 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) { if resultTiki.Modifier != tcell.ModAlt { t.Errorf("expected ModAlt, got %v", resultTiki.Modifier) } - if resultTiki.Foreground != tcell.ColorGreen { + if resultTiki.Foreground.TCell() != tcell.ColorGreen { t.Errorf("expected green foreground, got %v", resultTiki.Foreground) } if resultTiki.ViewMode != "expanded" { @@ -92,8 +93,8 @@ func TestMergePluginDefinitions_PreservesModifier(t *testing.T) { Key: tcell.KeyRune, Rune: 'M', Modifier: tcell.ModAlt, // this should be preserved - Foreground: tcell.ColorWhite, - Background: tcell.ColorDefault, + Foreground: config.NewColor(tcell.ColorWhite), + Background: config.DefaultColor(), Type: "tiki", }, Lanes: []TikiLane{ diff --git a/plugin/parser.go b/plugin/parser.go index 696d17d..4993395 100644 --- a/plugin/parser.go +++ b/plugin/parser.go @@ -9,6 +9,7 @@ import ( "github.com/gdamore/tcell/v2" "gopkg.in/yaml.v3" + "github.com/boolean-maybe/tiki/config" "github.com/boolean-maybe/tiki/ruki" ) @@ -20,8 +21,8 @@ func parsePluginConfig(cfg pluginFileConfig, source string, schema ruki.Schema) // Common fields // Use ColorDefault as sentinel so views can detect "not specified" and use theme-appropriate colors - fg := parseColor(cfg.Foreground, tcell.ColorDefault) - bg := parseColor(cfg.Background, tcell.ColorDefault) + fg := config.NewColor(parseColor(cfg.Foreground, tcell.ColorDefault)) + bg := config.NewColor(parseColor(cfg.Background, tcell.ColorDefault)) key, r, mod, err := parseKey(cfg.Key) if err != nil { diff --git a/util/gradient/gradient.go b/util/gradient/gradient.go index 087246e..baeaac4 100644 --- a/util/gradient/gradient.go +++ b/util/gradient/gradient.go @@ -120,7 +120,7 @@ func RenderGradientText(text string, gradient config.Gradient) string { // RenderAdaptiveGradientText renders text with gradient or solid color based on config.UseGradients. // When gradients are disabled, uses the gradient's end color as a solid color fallback. -func RenderAdaptiveGradientText(text string, gradient config.Gradient, fallbackColor tcell.Color) string { +func RenderAdaptiveGradientText(text string, gradient config.Gradient, fallbackColor config.Color) string { if len(text) == 0 { return "" } @@ -136,7 +136,7 @@ func RenderAdaptiveGradientText(text string, gradient config.Gradient, fallbackC } // GradientFromColor derives a gradient by lightening the base color. -func GradientFromColor(primary tcell.Color, ratio float64, fallback config.Gradient) config.Gradient { +func GradientFromColor(primary config.Color, ratio float64, fallback config.Gradient) config.Gradient { r, g, b := primary.RGB() if r == 0 && g == 0 && b == 0 { return fallback @@ -152,7 +152,7 @@ func GradientFromColor(primary tcell.Color, ratio float64, fallback config.Gradi } // GradientFromColorVibrant derives a vibrant gradient by boosting RGB values. -func GradientFromColorVibrant(primary tcell.Color, boost float64, fallback config.Gradient) config.Gradient { +func GradientFromColorVibrant(primary config.Color, boost float64, fallback config.Gradient) config.Gradient { r, g, b := primary.RGB() if r == 0 && g == 0 && b == 0 { return fallback diff --git a/util/gradient/gradient_test.go b/util/gradient/gradient_test.go index 58a3f6f..8ec9d09 100644 --- a/util/gradient/gradient_test.go +++ b/util/gradient/gradient_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/boolean-maybe/tiki/config" - "github.com/gdamore/tcell/v2" ) func TestInterpolateRGB(t *testing.T) { @@ -227,7 +226,7 @@ func TestGradientFromColor(t *testing.T) { } t.Run("black color uses fallback", func(t *testing.T) { - black := tcell.NewRGBColor(0, 0, 0) + black := config.NewColorRGB(0, 0, 0) got := GradientFromColor(black, 0.5, fallback) if got != fallback { t.Errorf("GradientFromColor(black) should return fallback, got %v", got) @@ -235,7 +234,7 @@ func TestGradientFromColor(t *testing.T) { }) t.Run("non-black color creates gradient", func(t *testing.T) { - blue := tcell.NewRGBColor(0, 0, 200) + blue := config.NewColorRGB(0, 0, 200) got := GradientFromColor(blue, 0.5, fallback) // Should have base color and lighter version @@ -257,7 +256,7 @@ func TestGradientFromColorVibrant(t *testing.T) { } t.Run("black color uses fallback", func(t *testing.T) { - black := tcell.NewRGBColor(0, 0, 0) + black := config.NewColorRGB(0, 0, 0) got := GradientFromColorVibrant(black, 1.5, fallback) if got != fallback { t.Errorf("GradientFromColorVibrant(black) should return fallback, got %v", got) @@ -265,7 +264,7 @@ func TestGradientFromColorVibrant(t *testing.T) { }) t.Run("non-black color creates boosted gradient", func(t *testing.T) { - blue := tcell.NewRGBColor(0, 0, 100) + blue := config.NewColorRGB(0, 0, 100) got := GradientFromColorVibrant(blue, 1.5, fallback) // Should have base color and boosted version @@ -301,7 +300,7 @@ func TestRenderAdaptiveGradientText(t *testing.T) { Start: [3]int{30, 144, 255}, // Dodger Blue End: [3]int{0, 191, 255}, // Deep Sky Blue } - fallback := tcell.NewRGBColor(0, 191, 255) // Deep Sky Blue + fallback := config.NewColorRGB(0, 191, 255) // Deep Sky Blue tests := []struct { name string @@ -427,15 +426,15 @@ func TestAdaptiveGradientRespectConfig(t *testing.T) { Start: [3]int{100, 100, 100}, End: [3]int{200, 200, 200}, } - fallback := tcell.NewRGBColor(200, 200, 200) + fallbackColor := config.NewColorRGB(200, 200, 200) text := "Test" // Test toggle behavior config.UseGradients = true - resultWithGradients := RenderAdaptiveGradientText(text, gradient, fallback) + resultWithGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor) config.UseGradients = false - resultWithoutGradients := RenderAdaptiveGradientText(text, gradient, fallback) + resultWithoutGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor) // Results should be different if resultWithGradients == resultWithoutGradients { diff --git a/util/points.go b/util/points.go index 3da7bb0..52d1925 100644 --- a/util/points.go +++ b/util/points.go @@ -1,6 +1,10 @@ package util -import "strings" +import ( + "strings" + + "github.com/boolean-maybe/tiki/config" +) // GeneratePointsVisual formats points as a visual representation using a styled bar. // Points are scaled to a 0-10 display range based on maxPoints configuration. @@ -8,8 +12,8 @@ import "strings" // Parameters: // - points: The task's point value // - maxPoints: The configured maximum points value (for scaling) -// - filledColor: tview color tag for filled segments (e.g. "[#508cff]") -// - unfilledColor: tview color tag for unfilled segments (e.g. "[#5f6982]") +// - filledColor: Color for filled segments +// - unfilledColor: Color for unfilled segments // // Returns: A string with colored filled (❚) and unfilled (❘) segments representing the points value. // @@ -17,8 +21,8 @@ import "strings" // // Example: // -// GeneratePointsVisual(7, 10, "[#508cff]", "[#5f6982]") returns a bar with 7 blue segments and 3 gray segments -func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfilledColor string) string { +// GeneratePointsVisual(7, 10, filledColor, unfilledColor) returns a bar with 7 blue segments and 3 gray segments +func GeneratePointsVisual(points int, maxPoints int, filledColor config.Color, unfilledColor config.Color) string { const displaySegments = 10 const filledChar = "❚" const unfilledChar = "❘" @@ -36,7 +40,7 @@ func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfille // Add filled segments if displayPoints > 0 { - result.WriteString(filledColor) + result.WriteString(filledColor.Tag().String()) for i := 0; i < displayPoints; i++ { result.WriteString(filledChar) } @@ -44,7 +48,7 @@ func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfille // Add unfilled segments if displayPoints < displaySegments { - result.WriteString(unfilledColor) + result.WriteString(unfilledColor.Tag().String()) for i := displayPoints; i < displaySegments; i++ { result.WriteString(unfilledChar) } diff --git a/util/points_test.go b/util/points_test.go index 4e77122..bf176b5 100644 --- a/util/points_test.go +++ b/util/points_test.go @@ -1,8 +1,14 @@ package util -import "testing" +import ( + "testing" + + "github.com/boolean-maybe/tiki/config" +) func TestGeneratePointsVisual(t *testing.T) { + blue := config.NewColorHex("#508cff") + gray := config.NewColorHex("#5f6982") const blueColor = "[#508cff]" const grayColor = "[#5f6982]" const resetColor = "[-]" @@ -65,7 +71,7 @@ func TestGeneratePointsVisual(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := GeneratePointsVisual(tt.points, tt.maxPoints, blueColor, grayColor) + got := GeneratePointsVisual(tt.points, tt.maxPoints, blue, gray) if got != tt.want { t.Errorf("GeneratePointsVisual(%d, %d) = %q, want %q", tt.points, tt.maxPoints, got, tt.want) } diff --git a/view/borders.go b/view/borders.go index 2436add..cec283c 100644 --- a/view/borders.go +++ b/view/borders.go @@ -25,7 +25,7 @@ func DrawSingleLineBorder(screen tcell.Screen, x, y, width, height int) { } colors := config.GetColors() - style := tcell.StyleDefault.Foreground(colors.TaskBoxUnselectedBorder).Background(colors.ContentBackgroundColor) + style := tcell.StyleDefault.Foreground(colors.TaskBoxUnselectedBorder.TCell()).Background(colors.ContentBackgroundColor.TCell()) DrawSingleLineBorderWithStyle(screen, x, y, width, height, style) } diff --git a/view/doki_plugin_view.go b/view/doki_plugin_view.go index f8dd59f..a20db29 100644 --- a/view/doki_plugin_view.go +++ b/view/doki_plugin_view.go @@ -57,8 +57,8 @@ func NewDokiView( func (dv *DokiView) build() { // title bar with gradient background using plugin color - textColor := tcell.ColorDefault - if dv.pluginDef.Foreground != tcell.ColorDefault { + textColor := config.DefaultColor() + if !dv.pluginDef.Foreground.IsDefault() { textColor = dv.pluginDef.Foreground } dv.titleBar = NewGradientCaptionRow([]string{dv.pluginDef.Name}, nil, dv.pluginDef.Background, textColor) diff --git a/view/gradient_caption_row.go b/view/gradient_caption_row.go index 5708b3e..5393cb4 100644 --- a/view/gradient_caption_row.go +++ b/view/gradient_caption_row.go @@ -15,12 +15,12 @@ type GradientCaptionRow struct { laneNames []string laneWidths []int // proportional widths (same values used in tview.Flex) gradient config.Gradient // computed gradient (for truecolor/256-color terminals) - textColor tcell.Color + textColor config.Color } // NewGradientCaptionRow creates a new gradient caption row widget. // laneWidths should match the flex proportions used for lane layout (nil = equal). -func NewGradientCaptionRow(laneNames []string, laneWidths []int, bgColor tcell.Color, textColor tcell.Color) *GradientCaptionRow { +func NewGradientCaptionRow(laneNames []string, laneWidths []int, bgColor config.Color, textColor config.Color) *GradientCaptionRow { return &GradientCaptionRow{ Box: tview.NewBox(), laneNames: laneNames, @@ -106,7 +106,7 @@ func (gcr *GradientCaptionRow) Draw(screen tcell.Screen) { } // Render the cell with gradient background - style := tcell.StyleDefault.Foreground(gcr.textColor).Background(bgColor) + style := tcell.StyleDefault.Foreground(gcr.textColor.TCell()).Background(bgColor) for row := 0; row < height; row++ { screen.SetContent(x+col, y+row, char, nil, style) } @@ -154,7 +154,7 @@ const ( ) // computeCaptionGradient computes the gradient for caption background from a base color. -func computeCaptionGradient(primary tcell.Color) config.Gradient { +func computeCaptionGradient(primary config.Color) config.Gradient { fallback := config.GetColors().CaptionFallbackGradient if useVibrantPluginGradient { return gradient.GradientFromColorVibrant(primary, vibrantBoost, fallback) diff --git a/view/header/chart.go b/view/header/chart.go index 049d51c..3d8e680 100644 --- a/view/header/chart.go +++ b/view/header/chart.go @@ -17,7 +17,7 @@ type ChartWidget struct { func NewChartWidgetSimple() *ChartWidget { colors := config.GetColors() chartTheme := barchart.DefaultTheme() - chartTheme.AxisColor = colors.BurndownChartAxisColor + chartTheme.AxisColor = colors.BurndownChartAxisColor.TCell() chartTheme.BarGradientFrom = colors.BurndownHeaderGradientFrom.Start // Use header-specific gradient chartTheme.BarGradientTo = colors.BurndownHeaderGradientTo.Start // Use header-specific gradient chartTheme.DotChar = '⣿' // braille full cell for compact dots diff --git a/view/header/color_scheme.go b/view/header/color_scheme.go index d215b3b..0cb0b11 100644 --- a/view/header/color_scheme.go +++ b/view/header/color_scheme.go @@ -4,8 +4,8 @@ import "github.com/boolean-maybe/tiki/config" // ColorScheme defines color pairs for different action categories type ColorScheme struct { - KeyColor string - LabelColor string + KeyColor config.Color + LabelColor config.Color } // getColorScheme returns the color scheme for the given action type. diff --git a/view/header/context_help.go b/view/header/context_help.go index 2209d8f..18fb67a 100644 --- a/view/header/context_help.go +++ b/view/header/context_help.go @@ -237,7 +237,7 @@ func buildGridRow(rowData []cellData, maxKeyLenPerCol, maxLabelLenPerCol []int, // Render cell with colors scheme := getColorScheme(cell.colorType) - fmt.Fprintf(&line, "[%s]<%s>[%s]", scheme.KeyColor, cell.key, scheme.LabelColor) + fmt.Fprintf(&line, "%s<%s>%s", scheme.KeyColor.Tag().String(), cell.key, scheme.LabelColor.Tag().String()) // Add key padding if keyPadding := maxKeyLenPerCol[col] - cell.keyLen; keyPadding > 0 { diff --git a/view/header/info.go b/view/header/info.go index 193fd82..f30e008 100644 --- a/view/header/info.go +++ b/view/header/info.go @@ -30,14 +30,15 @@ func NewInfoWidget() *InfoWidget { func (iw *InfoWidget) SetViewInfo(name, description string) { colors := config.GetColors() - // convert "[orange]" to "[orange::b]" for bold name - boldColor := makeBold(colors.HeaderInfoLabel) + boldColor := colors.HeaderInfoLabel.Tag().Bold().String() + separatorTag := colors.HeaderInfoSeparator.Tag().String() + descTag := colors.HeaderInfoDesc.Tag().String() separator := strings.Repeat("─", InfoWidth) var text string if description != "" { - text = fmt.Sprintf("%s%s[-::-]\n%s%s[-]\n%s%s", boldColor, name, colors.HeaderInfoSeparator, separator, colors.HeaderInfoDesc, description) + text = fmt.Sprintf("%s%s[-::-]\n%s%s[-]\n%s%s", boldColor, name, separatorTag, separator, descTag, description) } else { text = fmt.Sprintf("%s%s[-::-]", boldColor, name) } @@ -45,11 +46,6 @@ func (iw *InfoWidget) SetViewInfo(name, description string) { iw.SetText(text) } -// makeBold converts a tview color tag like "[orange]" to "[orange::b]" -func makeBold(colorTag string) string { - return strings.TrimSuffix(colorTag, "]") + "::b]" -} - // Primitive returns the underlying tview primitive func (iw *InfoWidget) Primitive() tview.Primitive { return iw.TextView diff --git a/view/markdown/navigable_markdown.go b/view/markdown/navigable_markdown.go index a9e68ff..112b435 100644 --- a/view/markdown/navigable_markdown.go +++ b/view/markdown/navigable_markdown.go @@ -47,7 +47,7 @@ func NewNavigableMarkdown(cfg NavigableMarkdownConfig) *NavigableMarkdown { renderer = renderer.WithCodeBorder(b) } nm.viewer.SetRenderer(renderer) - nm.viewer.SetBackgroundColor(config.GetColors().ContentBackgroundColor) + nm.viewer.SetBackgroundColor(config.GetColors().ContentBackgroundColor.TCell()) if cfg.ImageManager != nil && cfg.ImageManager.Supported() { nm.viewer.SetImageManager(cfg.ImageManager) } diff --git a/view/search_box.go b/view/search_box.go index 29acda1..3b64244 100644 --- a/view/search_box.go +++ b/view/search_box.go @@ -21,9 +21,9 @@ func NewSearchBox() *SearchBox { // Configure the input field (border drawn manually in Draw) inputField.SetLabel("> ") - inputField.SetLabelColor(colors.SearchBoxLabelColor) - inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) - inputField.SetFieldTextColor(colors.ContentTextColor) + inputField.SetLabelColor(colors.SearchBoxLabelColor.TCell()) + inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + inputField.SetFieldTextColor(colors.ContentTextColor.TCell()) inputField.SetBorder(false) sb := &SearchBox{ @@ -60,7 +60,7 @@ func (sb *SearchBox) Draw(screen tcell.Screen) { } // Fill interior with theme-aware background color - bgColor := config.GetColors().ContentBackgroundColor + bgColor := config.GetColors().ContentBackgroundColor.TCell() bgStyle := tcell.StyleDefault.Background(bgColor) for row := y; row < y+height; row++ { for col := x; col < x+width; col++ { diff --git a/view/statusline/statusline.go b/view/statusline/statusline.go index 4557378..e8b3a90 100644 --- a/view/statusline/statusline.go +++ b/view/statusline/statusline.go @@ -102,7 +102,7 @@ func (sw *StatuslineWidget) render(width int) { if padLen < 1 { padLen = 1 } - padding := fmt.Sprintf("[-:%s]%s[-:-]", colors.StatuslineFillBg, strings.Repeat(" ", padLen)) + padding := fmt.Sprintf("[-:%s]%s[-:-]", colors.StatuslineFillBg.Hex(), strings.Repeat(" ", padLen)) sw.SetText(left + msgRendered + padding + rightStats) } @@ -121,14 +121,14 @@ func (sw *StatuslineWidget) renderLeftSegments(segments []statSegment, colors *c bg, fg := segmentColors(i, colors) // segment text: " value " - fmt.Fprintf(&b, "[%s:%s] %s ", fg, bg, seg.value) + fmt.Fprintf(&b, "[%s:%s] %s ", fg.Hex(), bg.Hex(), seg.value) // separator: fg = current bg (creates the arrow), bg = next segment's bg or fill nextBg := colors.StatuslineFillBg if i < len(segments)-1 { nextBg, _ = segmentColors(i+1, colors) } - fmt.Fprintf(&b, "[%s:%s]%s", bg, nextBg, separatorRight) + fmt.Fprintf(&b, "[%s:%s]%s", bg.Hex(), nextBg.Hex(), separatorRight) } // reset colors @@ -153,10 +153,10 @@ func (sw *StatuslineWidget) renderRightSegments(segments []statSegment, colors * if i > 0 { prevBg, _ = segmentColors(i-1, colors) } - fmt.Fprintf(&b, "[%s:%s]%s", bg, prevBg, separatorLeft) + fmt.Fprintf(&b, "[%s:%s]%s", bg.Hex(), prevBg.Hex(), separatorLeft) // segment text - fmt.Fprintf(&b, "[%s:%s] %s ", fg, bg, seg.value) + fmt.Fprintf(&b, "[%s:%s] %s ", fg.Hex(), bg.Hex(), seg.value) } // reset colors @@ -166,7 +166,7 @@ func (sw *StatuslineWidget) renderRightSegments(segments []statSegment, colors * // segmentColors returns (bg, fg) for a segment at the given index. // Even indices use accent colors, odd indices use normal colors. -func segmentColors(index int, colors *config.ColorConfig) (string, string) { +func segmentColors(index int, colors *config.ColorConfig) (config.Color, config.Color) { if index%2 == 0 { return colors.StatuslineAccentBg, colors.StatuslineAccentFg } @@ -179,11 +179,11 @@ func (sw *StatuslineWidget) renderMessage(msg string, level model.MessageLevel, return "" } fg, bg := messageColors(level, colors) - return fmt.Sprintf("[%s:%s] %s [-:-]", fg, bg, msg) + return fmt.Sprintf("[%s:%s] %s [-:-]", fg.Hex(), bg.Hex(), msg) } // messageColors returns (fg, bg) for the given message level -func messageColors(level model.MessageLevel, colors *config.ColorConfig) (string, string) { +func messageColors(level model.MessageLevel, colors *config.ColorConfig) (config.Color, config.Color) { switch level { case model.MessageLevelError: return colors.StatuslineErrorFg, colors.StatuslineErrorBg diff --git a/view/statusline/statusline_test.go b/view/statusline/statusline_test.go index 4c0e746..770d2f3 100644 --- a/view/statusline/statusline_test.go +++ b/view/statusline/statusline_test.go @@ -11,15 +11,15 @@ import ( func testColors() *config.ColorConfig { return &config.ColorConfig{ - StatuslineBg: "#normal_bg", - StatuslineFg: "#normal_fg", - StatuslineAccentBg: "#accent_bg", - StatuslineAccentFg: "#accent_fg", - StatuslineInfoFg: "#info_fg", - StatuslineInfoBg: "#info_bg", - StatuslineErrorFg: "#error_fg", - StatuslineErrorBg: "#error_bg", - StatuslineFillBg: "#fill_bg", + StatuslineBg: config.NewColorHex("#111111"), + StatuslineFg: config.NewColorHex("#222222"), + StatuslineAccentBg: config.NewColorHex("#333333"), + StatuslineAccentFg: config.NewColorHex("#444444"), + StatuslineInfoFg: config.NewColorHex("#555555"), + StatuslineInfoBg: config.NewColorHex("#666666"), + StatuslineErrorFg: config.NewColorHex("#777777"), + StatuslineErrorBg: config.NewColorHex("#888888"), + StatuslineFillBg: config.NewColorHex("#999999"), } } @@ -108,11 +108,11 @@ func TestRenderLeftSegments_singleSegment(t *testing.T) { result := sw.renderLeftSegments(segments, colors) // first segment (index 0) uses accent colors - if !strings.Contains(result, "[#accent_fg:#accent_bg] v1.0 ") { + if !strings.Contains(result, "[#444444:#333333] v1.0 ") { t.Errorf("first segment should use accent colors, got %q", result) } // separator: fg=accent_bg (current), bg=fill (last segment) - if !strings.Contains(result, "[#accent_bg:#fill_bg]"+separatorRight) { + if !strings.Contains(result, "[#333333:#999999]"+separatorRight) { t.Errorf("separator should transition to fill background, got %q", result) } // ends with color reset @@ -132,15 +132,15 @@ func TestRenderLeftSegments_twoSegments(t *testing.T) { result := sw.renderLeftSegments(segments, colors) // first segment (index 0): accent - if !strings.Contains(result, "[#accent_fg:#accent_bg] v1.0 ") { + if !strings.Contains(result, "[#444444:#333333] v1.0 ") { t.Errorf("first segment should use accent, got %q", result) } // separator between 1st and 2nd: fg=accent_bg, bg=normal_bg - if !strings.Contains(result, "[#accent_bg:#normal_bg]"+separatorRight) { + if !strings.Contains(result, "[#333333:#111111]"+separatorRight) { t.Errorf("separator should transition accent→normal, got %q", result) } // second segment (index 1): normal - if !strings.Contains(result, "[#normal_fg:#normal_bg] main ") { + if !strings.Contains(result, "[#222222:#111111] main ") { t.Errorf("second segment should use normal colors, got %q", result) } } @@ -161,11 +161,11 @@ func TestRenderRightSegments_singleSegment(t *testing.T) { result := sw.renderRightSegments(segments, colors) // index 0 (even) → accent colors - if !strings.Contains(result, "[#accent_fg:#accent_bg] 42 ") { + if !strings.Contains(result, "[#444444:#333333] 42 ") { t.Errorf("segment 0 should use accent colors, got %q", result) } // separator: fg=accent_bg, bg=fill (first right segment) - if !strings.Contains(result, "[#accent_bg:#fill_bg]"+separatorLeft) { + if !strings.Contains(result, "[#333333:#999999]"+separatorLeft) { t.Errorf("separator should be accent→fill, got %q", result) } } @@ -181,15 +181,15 @@ func TestRenderRightSegments_twoSegments(t *testing.T) { result := sw.renderRightSegments(segments, colors) // index 0 (even) → accent - if !strings.Contains(result, "[#accent_fg:#accent_bg] 42 ") { + if !strings.Contains(result, "[#444444:#333333] 42 ") { t.Errorf("segment 0 should use accent, got %q", result) } // index 1 (odd) → normal - if !strings.Contains(result, "[#normal_fg:#normal_bg] 10 ") { + if !strings.Contains(result, "[#222222:#111111] 10 ") { t.Errorf("segment 1 should use normal, got %q", result) } // separator between 0→1: fg=normal_bg, bg=accent_bg (prev segment) - if !strings.Contains(result, "[#normal_bg:#accent_bg]"+separatorLeft) { + if !strings.Contains(result, "[#111111:#333333]"+separatorLeft) { t.Errorf("separator between segments should show normal→accent transition, got %q", result) } } @@ -206,13 +206,13 @@ func TestRenderRightSegments_threeSegments(t *testing.T) { result := sw.renderRightSegments(segments, colors) // index 0 → accent, index 1 → normal, index 2 → accent - if !strings.Contains(result, "[#accent_fg:#accent_bg] a ") { + if !strings.Contains(result, "[#444444:#333333] a ") { t.Error("segment 0 should use accent") } - if !strings.Contains(result, "[#normal_fg:#normal_bg] b ") { + if !strings.Contains(result, "[#222222:#111111] b ") { t.Error("segment 1 should use normal") } - if !strings.Contains(result, "[#accent_fg:#accent_bg] c ") { + if !strings.Contains(result, "[#444444:#333333] c ") { t.Error("segment 2 should use accent") } } @@ -230,7 +230,7 @@ func TestRenderMessage_info(t *testing.T) { colors := testColors() result := sw.renderMessage("task saved", model.MessageLevelInfo, colors) - if !strings.Contains(result, "[#info_fg:#info_bg] task saved ") { + if !strings.Contains(result, "[#555555:#666666] task saved ") { t.Errorf("info message should use info colors, got %q", result) } if !strings.HasSuffix(result, "[-:-]") { @@ -243,7 +243,7 @@ func TestRenderMessage_error(t *testing.T) { colors := testColors() result := sw.renderMessage("validation failed", model.MessageLevelError, colors) - if !strings.Contains(result, "[#error_fg:#error_bg] validation failed ") { + if !strings.Contains(result, "[#777777:#888888] validation failed ") { t.Errorf("error message should use error colors, got %q", result) } if !strings.HasSuffix(result, "[-:-]") { @@ -255,18 +255,18 @@ func TestRightSegmentColors(t *testing.T) { colors := testColors() bg0, fg0 := segmentColors(0, colors) - if bg0 != "#accent_bg" || fg0 != "#accent_fg" { - t.Errorf("index 0: got (%s, %s), want accent", bg0, fg0) + if bg0.Hex() != colors.StatuslineAccentBg.Hex() || fg0.Hex() != colors.StatuslineAccentFg.Hex() { + t.Errorf("index 0: got (%s, %s), want accent", bg0.Hex(), fg0.Hex()) } bg1, fg1 := segmentColors(1, colors) - if bg1 != "#normal_bg" || fg1 != "#normal_fg" { - t.Errorf("index 1: got (%s, %s), want normal", bg1, fg1) + if bg1.Hex() != colors.StatuslineBg.Hex() || fg1.Hex() != colors.StatuslineFg.Hex() { + t.Errorf("index 1: got (%s, %s), want normal", bg1.Hex(), fg1.Hex()) } bg2, fg2 := segmentColors(2, colors) - if bg2 != "#accent_bg" || fg2 != "#accent_fg" { - t.Errorf("index 2: got (%s, %s), want accent", bg2, fg2) + if bg2.Hex() != colors.StatuslineAccentBg.Hex() || fg2.Hex() != colors.StatuslineAccentFg.Hex() { + t.Errorf("index 2: got (%s, %s), want accent", bg2.Hex(), fg2.Hex()) } } diff --git a/view/task_box.go b/view/task_box.go index 27333a6..beab26e 100644 --- a/view/task_box.go +++ b/view/task_box.go @@ -18,11 +18,11 @@ import ( // applyFrameStyle applies selected/unselected styling to a frame func applyFrameStyle(frame *tview.Frame, selected bool, colors *config.ColorConfig) { if selected { - frame.SetBorderColor(colors.TaskBoxSelectedBorder) + frame.SetBorderColor(colors.TaskBoxSelectedBorder.TCell()) } else { - frame.SetBorderColor(colors.TaskBoxUnselectedBorder) - if colors.TaskBoxUnselectedBackground != tcell.ColorDefault { - frame.SetBackgroundColor(colors.TaskBoxUnselectedBackground) + frame.SetBorderColor(colors.TaskBoxUnselectedBorder.TCell()) + if !colors.TaskBoxUnselectedBackground.IsDefault() { + frame.SetBackgroundColor(colors.TaskBoxUnselectedBackground.TCell()) } } } @@ -35,11 +35,14 @@ func buildCompactTaskContent(task *taskpkg.Task, colors *config.ColorConfig, ava priorityEmoji := taskpkg.PriorityLabel(task.Priority) pointsVisual := util.GeneratePointsVisual(task.Points, config.GetMaxPoints(), colors.PointsFilledColor, colors.PointsUnfilledColor) + titleTag := colors.TaskBoxTitleColor.Tag().String() + labelTag := colors.TaskBoxLabelColor.Tag().String() + return fmt.Sprintf("%s %s\n%s%s[-]\n%spriority[-] %s %spoints[-] %s%s[-]", emoji, idGradient, - colors.TaskBoxTitleColor, truncatedTitle, - colors.TaskBoxLabelColor, priorityEmoji, - colors.TaskBoxLabelColor, colors.TaskBoxLabelColor, pointsVisual) + titleTag, truncatedTitle, + labelTag, priorityEmoji, + labelTag, labelTag, pointsVisual) } // buildExpandedTaskContent builds the content string for expanded task display @@ -64,25 +67,30 @@ func buildExpandedTaskContent(task *taskpkg.Task, colors *config.ColorConfig, av descLine3 = tview.Escape(util.TruncateText(descLines[2], availableWidth)) } + titleTag := colors.TaskBoxTitleColor.Tag().String() + labelTag := colors.TaskBoxLabelColor.Tag().String() + descTag := colors.TaskBoxDescriptionColor.Tag().String() + tagValueTag := colors.TaskBoxTagValueColor.Tag().String() + // Build tags string tagsStr := "" if len(task.Tags) > 0 { - tagsStr = colors.TaskBoxLabelColor + "Tags:[-] " + colors.TaskBoxTagValueColor + tview.Escape(util.TruncateText(strings.Join(task.Tags, ", "), availableWidth-6)) + "[-]" + tagsStr = labelTag + "Tags:[-] " + tagValueTag + tview.Escape(util.TruncateText(strings.Join(task.Tags, ", "), availableWidth-6)) + "[-]" } // Build priority/points line priorityEmoji := taskpkg.PriorityLabel(task.Priority) pointsVisual := util.GeneratePointsVisual(task.Points, config.GetMaxPoints(), colors.PointsFilledColor, colors.PointsUnfilledColor) priorityPointsStr := fmt.Sprintf("%spriority[-] %s %spoints[-] %s%s[-]", - colors.TaskBoxLabelColor, priorityEmoji, - colors.TaskBoxLabelColor, colors.TaskBoxLabelColor, pointsVisual) + labelTag, priorityEmoji, + labelTag, labelTag, pointsVisual) return fmt.Sprintf("%s %s\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s\n%s", emoji, idGradient, - colors.TaskBoxTitleColor, truncatedTitle, - colors.TaskBoxDescriptionColor, descLine1, - colors.TaskBoxDescriptionColor, descLine2, - colors.TaskBoxDescriptionColor, descLine3, + titleTag, truncatedTitle, + descTag, descLine1, + descTag, descLine2, + descTag, descLine3, tagsStr, priorityPointsStr) } diff --git a/view/taskdetail/base.go b/view/taskdetail/base.go index 982d757..46bda79 100644 --- a/view/taskdetail/base.go +++ b/view/taskdetail/base.go @@ -115,7 +115,7 @@ func (b *Base) assembleMetadataBox( metadataBox := tview.NewFrame(metadataContainer).SetBorders(0, 0, 0, 0, 0, 0) metadataBox.SetBorder(true).SetTitle( fmt.Sprintf(" %s ", gradient.RenderAdaptiveGradientText(task.ID, colors.TaskDetailIDColor, colors.FallbackTaskIDColor)), - ).SetBorderColor(colors.TaskBoxUnselectedBorder) + ).SetBorderColor(colors.TaskBoxUnselectedBorder.TCell()) metadataBox.SetBorderPadding(1, 0, 2, 2) return metadataBox diff --git a/view/taskdetail/render_helpers.go b/view/taskdetail/render_helpers.go index 76ace37..8e46404 100644 --- a/view/taskdetail/render_helpers.go +++ b/view/taskdetail/render_helpers.go @@ -29,7 +29,7 @@ type FieldRenderContext struct { } // getDimOrFullColor returns dim color if in edit mode and not focused, otherwise full color -func getDimOrFullColor(mode RenderMode, focused bool, fullColor string, dimColor string) string { +func getDimOrFullColor(mode RenderMode, focused bool, fullColor config.Color, dimColor config.Color) config.Color { if mode == RenderModeEdit && !focused { return dimColor } @@ -38,7 +38,7 @@ func getDimOrFullColor(mode RenderMode, focused bool, fullColor string, dimColor // getFocusMarker returns the focus marker string (arrow + text color) from colors config func getFocusMarker(colors *config.ColorConfig) string { - return colors.TaskDetailEditFocusMarker + "► " + colors.TaskDetailEditFocusText + return colors.TaskDetailEditFocusMarker.Tag().String() + "► " + colors.TaskDetailEditFocusText.Tag().String() } // RenderStatusText renders a status field as read-only text @@ -46,15 +46,15 @@ func RenderStatusText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitiv focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldStatus statusDisplay := taskpkg.StatusDisplay(task.Status) - labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) - valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) + labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() + valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String() focusMarker := "" if focused && ctx.Mode == RenderModeEdit { focusMarker = getFocusMarker(ctx.Colors) } - text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelColor, "Status:", valueColor, statusDisplay) + text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelTag, "Status:", valueTag, statusDisplay) textView := tview.NewTextView().SetDynamicColors(true).SetText(text) textView.SetBorderPadding(0, 0, 0, 0) @@ -69,15 +69,15 @@ func RenderTypeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive typeDisplay = "[gray](none)[-]" } - labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) - valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) + labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() + valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String() focusMarker := "" if focused && ctx.Mode == RenderModeEdit { focusMarker = getFocusMarker(ctx.Colors) } - text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelColor, "Type:", valueColor, typeDisplay) + text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelTag, "Type:", valueTag, typeDisplay) textView := tview.NewTextView().SetDynamicColors(true).SetText(text) textView.SetBorderPadding(0, 0, 0, 0) @@ -88,15 +88,15 @@ func RenderTypeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive func RenderPriorityText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive { focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPriority - labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) - valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) + labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() + valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String() focusMarker := "" if focused && ctx.Mode == RenderModeEdit { focusMarker = getFocusMarker(ctx.Colors) } - text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelColor, "Priority:", valueColor, taskpkg.PriorityDisplay(task.Priority)) + text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelTag, "Priority:", valueTag, taskpkg.PriorityDisplay(task.Priority)) textView := tview.NewTextView().SetDynamicColors(true).SetText(text) textView.SetBorderPadding(0, 0, 0, 0) @@ -107,15 +107,15 @@ func RenderPriorityText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primit func RenderAssigneeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive { focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldAssignee - labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) - valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) + labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() + valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String() focusMarker := "" if focused && ctx.Mode == RenderModeEdit { focusMarker = getFocusMarker(ctx.Colors) } - text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelColor, "Assignee:", valueColor, tview.Escape(defaultString(task.Assignee, "Unassigned"))) + text := fmt.Sprintf("%s%s%-10s%s%s", focusMarker, labelTag, "Assignee:", valueTag, tview.Escape(defaultString(task.Assignee, "Unassigned"))) textView := tview.NewTextView().SetDynamicColors(true).SetText(text) textView.SetBorderPadding(0, 0, 0, 0) @@ -126,15 +126,15 @@ func RenderAssigneeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primit func RenderPointsText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive { focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPoints - labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) - valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) + labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String() + valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String() focusMarker := "" if focused && ctx.Mode == RenderModeEdit { focusMarker = getFocusMarker(ctx.Colors) } - text := fmt.Sprintf("%s%s%-10s%s%d", focusMarker, labelColor, "Points:", valueColor, task.Points) + text := fmt.Sprintf("%s%s%-10s%s%d", focusMarker, labelTag, "Points:", valueTag, task.Points) textView := tview.NewTextView().SetDynamicColors(true).SetText(text) textView.SetBorderPadding(0, 0, 0, 0) @@ -144,8 +144,14 @@ func RenderPointsText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitiv // RenderTitleText renders a title as read-only text func RenderTitleText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive { focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldTitle - titleColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailTitleText[:len(ctx.Colors.TaskDetailTitleText)-1]+"::b]", ctx.Colors.TaskDetailEditDimTextColor) - titleText := fmt.Sprintf("%s%s%s", titleColor, tview.Escape(task.Title), ctx.Colors.TaskDetailValueText) + var titleTag string + if ctx.Mode == RenderModeEdit && !focused { + titleTag = ctx.Colors.TaskDetailEditDimTextColor.Tag().String() + } else { + titleTag = ctx.Colors.TaskDetailTitleText.Tag().Bold().String() + } + valueTag := ctx.Colors.TaskDetailValueText.Tag().String() + titleText := fmt.Sprintf("%s%s%s", titleTag, tview.Escape(task.Title), valueTag) titleBox := tview.NewTextView(). SetDynamicColors(true). SetText(titleText) @@ -159,7 +165,7 @@ func RenderTagsColumn(task *taskpkg.Task) tview.Primitive { return tview.NewBox() } colors := config.GetColors() - label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sTags", colors.TaskDetailLabelText)) + label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sTags", colors.TaskDetailLabelText.Tag().String())) label.SetBorderPadding(0, 0, 0, 0) col := tview.NewFlex().SetDirection(tview.FlexRow) @@ -186,7 +192,7 @@ func RenderDependsOnColumn(task *taskpkg.Task, taskStore store.Store) tview.Prim } colors := config.GetColors() - label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sDepends On", colors.TaskDetailLabelText)) + label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sDepends On", colors.TaskDetailLabelText.Tag().String())) label.SetBorderPadding(0, 0, 0, 0) col := tview.NewFlex().SetDirection(tview.FlexRow) @@ -204,7 +210,7 @@ func RenderBlocksColumn(blocked []*taskpkg.Task) tview.Primitive { } colors := config.GetColors() - label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sBlocks", colors.TaskDetailLabelText)) + label := tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf("%sBlocks", colors.TaskDetailLabelText.Tag().String())) label.SetBorderPadding(0, 0, 0, 0) col := tview.NewFlex().SetDirection(tview.FlexRow) @@ -217,7 +223,7 @@ func RenderBlocksColumn(blocked []*taskpkg.Task) tview.Primitive { // RenderAuthorText renders the author field as read-only text func RenderAuthorText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primitive { text := fmt.Sprintf("%s%-10s%s%s", - colors.TaskDetailEditDimLabelColor, "Author:", colors.TaskDetailValueText, tview.Escape(defaultString(task.CreatedBy, "Unknown"))) + colors.TaskDetailEditDimLabelColor.Tag().String(), "Author:", colors.TaskDetailValueText.Tag().String(), tview.Escape(defaultString(task.CreatedBy, "Unknown"))) view := tview.NewTextView().SetDynamicColors(true).SetText(text) view.SetBorderPadding(0, 0, 0, 0) return view @@ -230,7 +236,7 @@ func RenderCreatedText(task *taskpkg.Task, colors *config.ColorConfig) tview.Pri createdAtStr = task.CreatedAt.Format("2006-01-02 15:04") } text := fmt.Sprintf("%s%-10s%s%s", - colors.TaskDetailEditDimLabelColor, "Created:", colors.TaskDetailValueText, createdAtStr) + colors.TaskDetailEditDimLabelColor.Tag().String(), "Created:", colors.TaskDetailValueText.Tag().String(), createdAtStr) view := tview.NewTextView().SetDynamicColors(true).SetText(text) view.SetBorderPadding(0, 0, 0, 0) return view @@ -243,7 +249,7 @@ func RenderUpdatedText(task *taskpkg.Task, colors *config.ColorConfig) tview.Pri updatedAtStr = task.UpdatedAt.Format("2006-01-02 15:04") } text := fmt.Sprintf("%s%-10s%s%s", - colors.TaskDetailEditDimLabelColor, "Updated:", colors.TaskDetailValueText, updatedAtStr) + colors.TaskDetailEditDimLabelColor.Tag().String(), "Updated:", colors.TaskDetailValueText.Tag().String(), updatedAtStr) view := tview.NewTextView().SetDynamicColors(true).SetText(text) view.SetBorderPadding(0, 0, 0, 0) return view @@ -331,7 +337,7 @@ func RenderDueText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primiti dueDisplay = task.Due.Format("2006-01-02") } text := fmt.Sprintf("%s%-12s%s%s", - colors.TaskDetailEditDimLabelColor, "Due:", colors.TaskDetailValueText, dueDisplay) + colors.TaskDetailEditDimLabelColor.Tag().String(), "Due:", colors.TaskDetailValueText.Tag().String(), dueDisplay) view := tview.NewTextView().SetDynamicColors(true).SetText(text) view.SetBorderPadding(0, 0, 0, 0) return view @@ -341,7 +347,7 @@ func RenderDueText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primiti func RenderRecurrenceText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primitive { display := taskpkg.RecurrenceDisplay(task.Recurrence) text := fmt.Sprintf("%s%-12s%s%s", - colors.TaskDetailEditDimLabelColor, "Recurrence:", colors.TaskDetailValueText, display) + colors.TaskDetailEditDimLabelColor.Tag().String(), "Recurrence:", colors.TaskDetailValueText.Tag().String(), display) view := tview.NewTextView().SetDynamicColors(true).SetText(text) view.SetBorderPadding(0, 0, 0, 0) return view diff --git a/view/taskdetail/task_edit_view.go b/view/taskdetail/task_edit_view.go index 34931ff..fa47793 100644 --- a/view/taskdetail/task_edit_view.go +++ b/view/taskdetail/task_edit_view.go @@ -381,7 +381,7 @@ func (ev *TaskEditView) ensureTagsTextArea(task *taskpkg.Task) *tview.TextArea { ev.tagsTextArea.SetBorder(false) ev.tagsTextArea.SetBorderPadding(1, 1, 2, 2) ev.tagsTextArea.SetPlaceholder("Enter tags separated by spaces") - ev.tagsTextArea.SetPlaceholderStyle(tcell.StyleDefault.Foreground(config.GetColors().TaskDetailPlaceholderColor)) + ev.tagsTextArea.SetPlaceholderStyle(tcell.StyleDefault.Foreground(config.GetColors().TaskDetailPlaceholderColor.TCell())) ev.tagsTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyCtrlS { @@ -448,8 +448,8 @@ func (ev *TaskEditView) ensureTitleInput(task *taskpkg.Task) *tview.InputField { if ev.titleInput == nil { colors := config.GetColors() ev.titleInput = tview.NewInputField() - ev.titleInput.SetFieldBackgroundColor(colors.ContentBackgroundColor) - ev.titleInput.SetFieldTextColor(colors.InputFieldTextColor) + ev.titleInput.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell()) + ev.titleInput.SetFieldTextColor(colors.InputFieldTextColor.TCell()) ev.titleInput.SetBorder(false) ev.titleInput.SetChangedFunc(func(text string) { @@ -501,9 +501,9 @@ func (ev *TaskEditView) updateValidationState() { if ev.metadataBox != nil { colors := config.DefaultColors() if len(ev.validationErrors) > 0 { - ev.metadataBox.SetBorderColor(colors.TaskBoxSelectedBorder) + ev.metadataBox.SetBorderColor(colors.TaskBoxSelectedBorder.TCell()) } else { - ev.metadataBox.SetBorderColor(colors.TaskBoxUnselectedBorder) + ev.metadataBox.SetBorderColor(colors.TaskBoxUnselectedBorder.TCell()) } } } diff --git a/view/tiki_plugin_view.go b/view/tiki_plugin_view.go index deb1d1d..211d35f 100644 --- a/view/tiki_plugin_view.go +++ b/view/tiki_plugin_view.go @@ -10,12 +10,9 @@ import ( "github.com/boolean-maybe/tiki/store" "github.com/boolean-maybe/tiki/task" - "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) -// Note: tcell import is still used for pv.pluginDef.Background/Foreground checks - // PluginView renders a filtered/sorted list of tasks across lanes type PluginView struct { root *tview.Flex @@ -61,8 +58,8 @@ func NewPluginView( func (pv *PluginView) build() { // title bar with gradient background using plugin color - textColor := tcell.ColorDefault - if pv.pluginDef.Foreground != tcell.ColorDefault { + textColor := config.DefaultColor() + if !pv.pluginDef.Foreground.IsDefault() { textColor = pv.pluginDef.Foreground } laneNames := make([]string, len(pv.pluginDef.Lanes))