group colors by value

This commit is contained in:
booleanmaybe 2026-04-10 16:07:34 -04:00
parent b5f2ad66fa
commit 0be985a077
39 changed files with 766 additions and 365 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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")
}
})

View file

@ -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

View file

@ -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)

113
config/color.go Normal file
View file

@ -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 + "]"
}

146
config/color_test.go Normal file
View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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"

View file

@ -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 {

View file

@ -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{

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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++ {

View file

@ -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

View file

@ -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())
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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())
}
}
}

View file

@ -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))