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 { func DefaultTheme() Theme {
colors := config.GetColors() colors := config.GetColors()
return Theme{ return Theme{
AxisColor: colors.BurndownChartAxisColor, AxisColor: colors.BurndownChartAxisColor.TCell(),
LabelColor: colors.BurndownChartLabelColor, LabelColor: colors.BurndownChartLabelColor.TCell(),
ValueColor: colors.BurndownChartValueColor, ValueColor: colors.BurndownChartValueColor.TCell(),
BarColor: colors.BurndownChartBarColor, BarColor: colors.BurndownChartBarColor.TCell(),
BackgroundColor: config.GetColors().ContentBackgroundColor, BackgroundColor: config.GetColors().ContentBackgroundColor.TCell(),
BarGradientFrom: colors.BurndownChartGradientFrom.Start, BarGradientFrom: colors.BurndownChartGradientFrom.Start,
BarGradientTo: colors.BurndownChartGradientTo.Start, BarGradientTo: colors.BurndownChartGradientTo.Start,
DotChar: '⣿', // braille full cell for dense dot matrix 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 // Use adaptive gradient: solid color when gradients disabled
if !config.UseGradients { if !config.UseGradients {
return config.GetColors().FallbackBurndownColor return config.GetColors().FallbackBurndownColor.TCell()
} }
t := float64(row) / float64(total-1) t := float64(row) / float64(total-1)

View file

@ -26,12 +26,12 @@ func NewCompletionPrompt(words []string) *CompletionPrompt {
// Configure the input field // Configure the input field
colors := config.GetColors() colors := config.GetColors()
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
cp := &CompletionPrompt{ cp := &CompletionPrompt{
InputField: inputField, InputField: inputField,
words: words, words: words,
hintColor: colors.CompletionHintColor, hintColor: colors.CompletionHintColor.TCell(),
} }
return cp return cp

View file

@ -28,8 +28,8 @@ type DateEdit struct {
func NewDateEdit() *DateEdit { func NewDateEdit() *DateEdit {
inputField := tview.NewInputField() inputField := tview.NewInputField()
colors := config.GetColors() colors := config.GetColors()
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
de := &DateEdit{ de := &DateEdit{
InputField: inputField, InputField: inputField,

View file

@ -30,8 +30,8 @@ func NewEditSelectList(values []string, allowTyping bool) *EditSelectList {
// Configure the input field // Configure the input field
colors := config.GetColors() colors := config.GetColors()
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
esl := &EditSelectList{ esl := &EditSelectList{
InputField: inputField, InputField: inputField,

View file

@ -38,8 +38,8 @@ func NewIntEditSelect(min, max int, allowTyping bool) *IntEditSelect {
inputField := tview.NewInputField() inputField := tview.NewInputField()
colors := config.GetColors() colors := config.GetColors()
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
ies := &IntEditSelect{ ies := &IntEditSelect{
InputField: inputField, InputField: inputField,

View file

@ -32,8 +32,8 @@ type RecurrenceEdit struct {
func NewRecurrenceEdit() *RecurrenceEdit { func NewRecurrenceEdit() *RecurrenceEdit {
inputField := tview.NewInputField() inputField := tview.NewInputField()
colors := config.GetColors() colors := config.GetColors()
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
re := &RecurrenceEdit{ re := &RecurrenceEdit{
InputField: inputField, InputField: inputField,

View file

@ -23,11 +23,12 @@ type TaskList struct {
selectionIndex int selectionIndex int
idColumnWidth int // computed from widest ID idColumnWidth int // computed from widest ID
idGradient config.Gradient // gradient for ID text idGradient config.Gradient // gradient for ID text
idFallback tcell.Color // fallback solid color for ID idFallback config.Color // fallback solid color for ID
titleColor string // tview color tag for title, e.g. "[#b8b8b8]" titleColor config.Color // color for title text
selectionColor string // tview color tag for selected row highlight selectionColor config.Color // foreground color for selected row highlight
statusDoneColor string // tview color tag for done status indicator selectionBgColor config.Color // background color for selected row highlight
statusPendingColor string // tview color tag for pending status indicator 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. // NewTaskList creates a new TaskList with the given maximum visible row count.
@ -39,7 +40,8 @@ func NewTaskList(maxVisibleRows int) *TaskList {
idGradient: colors.TaskBoxIDColor, idGradient: colors.TaskBoxIDColor,
idFallback: colors.FallbackTaskIDColor, idFallback: colors.FallbackTaskIDColor,
titleColor: colors.TaskBoxTitleColor, titleColor: colors.TaskBoxTitleColor,
selectionColor: colors.TaskListSelectionColor, selectionColor: colors.TaskListSelectionFg,
selectionBgColor: colors.TaskListSelectionBg,
statusDoneColor: colors.TaskListStatusDoneColor, statusDoneColor: colors.TaskListStatusDoneColor,
statusPendingColor: colors.TaskListStatusPendingColor, statusPendingColor: colors.TaskListStatusPendingColor,
} }
@ -92,14 +94,14 @@ func (tl *TaskList) ScrollDown() {
} }
// SetIDColors overrides the gradient and fallback color for the ID column. // 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.idGradient = g
tl.idFallback = fallback tl.idFallback = fallback
return tl return tl
} }
// SetTitleColor overrides the tview color tag for the title column. // SetTitleColor overrides the color for the title column.
func (tl *TaskList) SetTitleColor(color string) *TaskList { func (tl *TaskList) SetTitleColor(color config.Color) *TaskList {
tl.titleColor = color tl.titleColor = color
return tl return tl
} }
@ -134,9 +136,9 @@ func (tl *TaskList) buildRow(t *task.Task, selected bool, width int) string {
// Status indicator: done = checkmark, else circle // Status indicator: done = checkmark, else circle
var statusIndicator string var statusIndicator string
if config.GetStatusRegistry().IsDone(string(t.Status)) { if config.GetStatusRegistry().IsDone(string(t.Status)) {
statusIndicator = tl.statusDoneColor + "\u2713[-]" statusIndicator = tl.statusDoneColor.Tag().String() + "\u2713[-]"
} else { } else {
statusIndicator = tl.statusPendingColor + "\u25CB[-]" statusIndicator = tl.statusPendingColor.Tag().String() + "\u25CB[-]"
} }
// Gradient-rendered ID, padded to idColumnWidth // 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) titleAvailable := max(width-1-1-tl.idColumnWidth-1, 0)
truncatedTitle := tview.Escape(util.TruncateText(t.Title, titleAvailable)) 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 { if selected {
row = tl.selectionColor + row row = tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String() + row
} }
return row return row

View file

@ -181,7 +181,7 @@ func TestFewerItemsThanViewport(t *testing.T) {
func TestSetIDColors(t *testing.T) { func TestSetIDColors(t *testing.T) {
tl := NewTaskList(10) tl := NewTaskList(10)
g := config.Gradient{Start: [3]int{255, 0, 0}, End: [3]int{0, 255, 0}} 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) result := tl.SetIDColors(g, fb)
if result != tl { if result != tl {
@ -197,12 +197,13 @@ func TestSetIDColors(t *testing.T) {
func TestSetTitleColor(t *testing.T) { func TestSetTitleColor(t *testing.T) {
tl := NewTaskList(10) tl := NewTaskList(10)
result := tl.SetTitleColor("[#ff0000]") c := config.NewColor(tcell.ColorRed)
result := tl.SetTitleColor(c)
if result != tl { if result != tl {
t.Error("SetTitleColor should return self for chaining") t.Error("SetTitleColor should return self for chaining")
} }
if tl.titleColor != "[#ff0000]" { if tl.titleColor != c {
t.Errorf("Expected [#ff0000], got %s", tl.titleColor) 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) { t.Run("selected row has selection color prefix", func(t *testing.T) {
row := tl.buildRow(pendingTask, true, width) row := tl.buildRow(pendingTask, true, width)
if !strings.HasPrefix(row, tl.selectionColor) { selTag := tl.selectionColor.Tag().WithBg(tl.selectionBgColor).String()
t.Errorf("selected row should start with selection color %q", tl.selectionColor) 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) { t.Run("unselected row has no selection prefix", func(t *testing.T) {
row := tl.buildRow(pendingTask, false, width) 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") t.Error("unselected row should not start with selection color")
} }
}) })

View file

@ -14,8 +14,8 @@ import (
type WordList struct { type WordList struct {
*tview.Box *tview.Box
words []string words []string
fgColor tcell.Color fgColor config.Color
bgColor tcell.Color bgColor config.Color
} }
// NewWordList creates a new WordList component. // NewWordList creates a new WordList component.
@ -43,7 +43,7 @@ func (w *WordList) GetWords() []string {
} }
// SetColors sets the foreground and background colors. // 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.fgColor = fg
w.bgColor = bg w.bgColor = bg
return w return w
@ -58,8 +58,8 @@ func (w *WordList) Draw(screen tcell.Screen) {
return return
} }
wordStyle := tcell.StyleDefault.Foreground(w.fgColor).Background(w.bgColor) wordStyle := tcell.StyleDefault.Foreground(w.fgColor.TCell()).Background(w.bgColor.TCell())
spaceStyle := tcell.StyleDefault.Background(config.GetColors().ContentBackgroundColor) spaceStyle := tcell.StyleDefault.Background(config.GetColors().ContentBackgroundColor.TCell())
currentX := x currentX := x
currentY := y currentY := y

View file

@ -60,8 +60,8 @@ func TestGetWords(t *testing.T) {
func TestSetColors(t *testing.T) { func TestSetColors(t *testing.T) {
wl := NewWordList([]string{"test"}) wl := NewWordList([]string{"test"})
fg := tcell.ColorRed fg := config.NewColor(tcell.ColorRed)
bg := tcell.ColorGreen bg := config.NewColor(tcell.ColorGreen)
result := wl.SetColors(fg, bg) 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 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 ( import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -18,218 +18,330 @@ type ColorConfig struct {
CaptionFallbackGradient Gradient CaptionFallbackGradient Gradient
// Task box colors // Task box colors
TaskBoxSelectedBackground tcell.Color TaskBoxSelectedBackground Color
TaskBoxSelectedText tcell.Color TaskBoxSelectedText Color
TaskBoxSelectedBorder tcell.Color TaskBoxSelectedBorder Color
TaskBoxUnselectedBorder tcell.Color TaskBoxUnselectedBorder Color
TaskBoxUnselectedBackground tcell.Color TaskBoxUnselectedBackground Color
TaskBoxIDColor Gradient TaskBoxIDColor Gradient
TaskBoxTitleColor string // tview color string like "[#b8b8b8]" TaskBoxTitleColor Color
TaskBoxLabelColor string // tview color string like "[#767676]" TaskBoxLabelColor Color
TaskBoxDescriptionColor string // tview color string like "[#767676]" TaskBoxDescriptionColor Color
TaskBoxTagValueColor string // tview color string like "[#5a6f8f]" TaskBoxTagValueColor Color
TaskListSelectionColor string // tview color string for selected row highlight, e.g. "[white:#3a5f8a]" TaskListSelectionFg Color // selected row foreground
TaskListStatusDoneColor string // tview color string for done status indicator, e.g. "[#00ff7f]" TaskListSelectionBg Color // selected row background
TaskListStatusPendingColor string // tview color string for pending status indicator, e.g. "[white]" TaskListStatusDoneColor Color
TaskListStatusPendingColor Color
// Task detail view colors // Task detail view colors
TaskDetailIDColor Gradient TaskDetailIDColor Gradient
TaskDetailTitleText string // tview color string like "[yellow]" TaskDetailTitleText Color
TaskDetailLabelText string // tview color string like "[green]" TaskDetailLabelText Color
TaskDetailValueText string // tview color string like "[white]" TaskDetailValueText Color
TaskDetailCommentAuthor string // tview color string like "[yellow]" TaskDetailCommentAuthor Color
TaskDetailEditDimTextColor string // tview color string like "[#808080]" TaskDetailEditDimTextColor Color
TaskDetailEditDimLabelColor string // tview color string like "[#606060]" TaskDetailEditDimLabelColor Color
TaskDetailEditDimValueColor string // tview color string like "[#909090]" TaskDetailEditDimValueColor Color
TaskDetailEditFocusMarker string // tview color string like "[yellow]" TaskDetailEditFocusMarker Color
TaskDetailEditFocusText string // tview color string like "[white]" TaskDetailEditFocusText Color
TaskDetailTagForeground tcell.Color TaskDetailTagForeground Color
TaskDetailTagBackground tcell.Color TaskDetailTagBackground Color
TaskDetailPlaceholderColor tcell.Color TaskDetailPlaceholderColor Color
// Content area colors (base canvas for editable/readable content) // Content area colors (base canvas for editable/readable content)
ContentBackgroundColor tcell.Color ContentBackgroundColor Color
ContentTextColor tcell.Color ContentTextColor Color
// Search box colors // Search box colors
SearchBoxLabelColor tcell.Color SearchBoxLabelColor Color
SearchBoxBackgroundColor tcell.Color SearchBoxBackgroundColor Color
SearchBoxTextColor tcell.Color SearchBoxTextColor Color
// Input field colors (used in task detail edit mode) // Input field colors (used in task detail edit mode)
InputFieldBackgroundColor tcell.Color InputFieldBackgroundColor Color
InputFieldTextColor tcell.Color InputFieldTextColor Color
// Completion prompt colors // Completion prompt colors
CompletionHintColor tcell.Color CompletionHintColor Color
// Burndown chart colors // Burndown chart colors
BurndownChartAxisColor tcell.Color BurndownChartAxisColor Color
BurndownChartLabelColor tcell.Color BurndownChartLabelColor Color
BurndownChartValueColor tcell.Color BurndownChartValueColor Color
BurndownChartBarColor tcell.Color BurndownChartBarColor Color
BurndownChartGradientFrom Gradient BurndownChartGradientFrom Gradient
BurndownChartGradientTo Gradient BurndownChartGradientTo Gradient
BurndownHeaderGradientFrom Gradient // Header-specific chart gradient BurndownHeaderGradientFrom Gradient // Header-specific chart gradient
BurndownHeaderGradientTo Gradient BurndownHeaderGradientTo Gradient
// Header view colors // Header view colors
HeaderInfoLabel string // tview color string for view name (bold) HeaderInfoLabel Color
HeaderInfoSeparator string // tview color string for horizontal rule below name HeaderInfoSeparator Color
HeaderInfoDesc string // tview color string for view description HeaderInfoDesc Color
HeaderKeyBinding string // tview color string like "[yellow]" HeaderKeyBinding Color
HeaderKeyText string // tview color string like "[white]" HeaderKeyText Color
// Points visual bar colors // Points visual bar colors
PointsFilledColor string // tview color string for filled segments PointsFilledColor Color
PointsUnfilledColor string // tview color string for unfilled segments PointsUnfilledColor Color
// Header context help action colors // Header context help action colors
HeaderActionGlobalKeyColor string // tview color string for global action keys HeaderActionGlobalKeyColor Color
HeaderActionGlobalLabelColor string // tview color string for global action labels HeaderActionGlobalLabelColor Color
HeaderActionPluginKeyColor string // tview color string for plugin action keys HeaderActionPluginKeyColor Color
HeaderActionPluginLabelColor string // tview color string for plugin action labels HeaderActionPluginLabelColor Color
HeaderActionViewKeyColor string // tview color string for view action keys HeaderActionViewKeyColor Color
HeaderActionViewLabelColor string // tview color string for view action labels HeaderActionViewLabelColor Color
// Plugin-specific colors // 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) // Fallback solid colors for gradient scenarios (used when UseGradients = false)
FallbackTaskIDColor tcell.Color // Deep Sky Blue (end of task ID gradient) FallbackTaskIDColor Color // Deep Sky Blue (end of task ID gradient)
FallbackBurndownColor tcell.Color // Purple (start of burndown gradient) FallbackBurndownColor Color // Purple (start of burndown gradient)
// Statusline colors (bottom bar, powerline style) // Statusline colors (bottom bar, powerline style)
StatuslineBg string // hex color for stat segment background, e.g. "#3a3a5c" StatuslineBg Color
StatuslineFg string // hex color for stat segment text, e.g. "#cccccc" StatuslineFg Color
StatuslineAccentBg string // hex color for accent segment background (first segment), e.g. "#5f87af" StatuslineAccentBg Color
StatuslineAccentFg string // hex color for accent segment text, e.g. "#1c1c2e" StatuslineAccentFg Color
StatuslineInfoFg string // hex color for info message text StatuslineInfoFg Color
StatuslineInfoBg string // hex color for info message background StatuslineInfoBg Color
StatuslineErrorFg string // hex color for error message text StatuslineErrorFg Color
StatuslineErrorBg string // hex color for error message background StatuslineErrorBg Color
StatuslineFillBg string // hex color for empty statusline area between segments StatuslineFillBg Color
} }
// DefaultColors returns the default color configuration // Palette defines the base color values used throughout the UI.
func DefaultColors() *ColorConfig { // Each entry is a semantic name for a unique color; ColorConfig fields reference these.
return &ColorConfig{ // To change a color everywhere it appears, change it here.
// Caption fallback gradient type Palette struct {
CaptionFallbackGradient: Gradient{ HighlightColor Color // yellow — accents, focus markers, key bindings, borders
Start: [3]int{25, 25, 112}, // Midnight Blue (center) TextColor Color // white — primary text on dark background
End: [3]int{65, 105, 225}, // Royal Blue (edges) 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 // Task box
TaskBoxSelectedBackground: tcell.PaletteColor(33), // Blue (ANSI 33) TaskBoxSelectedBackground: p.SelectionFgColor,
TaskBoxSelectedText: tcell.PaletteColor(117), // Light Blue (ANSI 117) TaskBoxSelectedText: p.SelectionText,
TaskBoxSelectedBorder: tcell.ColorYellow, TaskBoxSelectedBorder: p.HighlightColor,
TaskBoxUnselectedBorder: tcell.ColorGray, TaskBoxUnselectedBorder: p.MutedColor,
TaskBoxUnselectedBackground: tcell.ColorDefault, // transparent/no background TaskBoxUnselectedBackground: p.TransparentColor,
TaskBoxIDColor: Gradient{ TaskBoxIDColor: p.IDGradient,
Start: [3]int{30, 144, 255}, // Dodger Blue TaskBoxTitleColor: p.SoftTextColor,
End: [3]int{0, 191, 255}, // Deep Sky Blue TaskBoxLabelColor: p.SubduedTextColor,
}, TaskBoxDescriptionColor: p.SubduedTextColor,
TaskBoxTitleColor: "[#b8b8b8]", // Light gray TaskBoxTagValueColor: p.TagValueColor,
TaskBoxLabelColor: "[#767676]", // Darker gray for labels TaskListSelectionFg: p.TextColor,
TaskBoxDescriptionColor: "[#767676]", // Darker gray for description TaskListSelectionBg: p.SelectionBgColor,
TaskBoxTagValueColor: "[#5a6f8f]", // Blueish gray for tag values TaskListStatusDoneColor: p.SuccessColor,
TaskListSelectionColor: "[white:#3a5f8a]", // White text on steel blue background TaskListStatusPendingColor: p.TextColor,
TaskListStatusDoneColor: "[#00ff7f]", // Spring green for done checkmark
TaskListStatusPendingColor: "[white]", // White for pending circle
// Task detail // Task detail
TaskDetailIDColor: Gradient{ TaskDetailIDColor: p.IDGradient,
Start: [3]int{30, 144, 255}, // Dodger Blue (same as task box) TaskDetailTitleText: p.HighlightColor,
End: [3]int{0, 191, 255}, // Deep Sky Blue TaskDetailLabelText: p.AccentColor,
}, TaskDetailValueText: p.ValueColor,
TaskDetailTitleText: "[yellow]", TaskDetailCommentAuthor: p.HighlightColor,
TaskDetailLabelText: "[green]", TaskDetailEditDimTextColor: p.MutedColor,
TaskDetailValueText: "[#8c92ac]", TaskDetailEditDimLabelColor: p.DimLabelColor,
TaskDetailCommentAuthor: "[yellow]", TaskDetailEditDimValueColor: p.DimValueColor,
TaskDetailEditDimTextColor: "[#808080]", // Medium gray for dim text TaskDetailEditFocusMarker: p.HighlightColor,
TaskDetailEditDimLabelColor: "[#606060]", // Darker gray for dim labels TaskDetailEditFocusText: p.TextColor,
TaskDetailEditDimValueColor: "[#909090]", // Lighter gray for dim values TaskDetailTagForeground: p.TagFgColor,
TaskDetailEditFocusMarker: "[yellow]", // Yellow arrow for focus TaskDetailTagBackground: p.TagBgColor,
TaskDetailEditFocusText: "[white]", // White text after arrow TaskDetailPlaceholderColor: p.MutedColor,
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
// Content area (base canvas) // Content area
ContentBackgroundColor: tcell.ColorBlack, // dark theme: explicit black ContentBackgroundColor: NewColor(tcell.ColorBlack),
ContentTextColor: tcell.ColorWhite, // dark theme: white text ContentTextColor: p.TextColor,
// Search box // Search box
SearchBoxLabelColor: tcell.ColorWhite, SearchBoxLabelColor: p.TextColor,
SearchBoxBackgroundColor: tcell.ColorDefault, // Transparent SearchBoxBackgroundColor: p.TransparentColor,
SearchBoxTextColor: tcell.ColorWhite, SearchBoxTextColor: p.TextColor,
// Input field colors // Input field
InputFieldBackgroundColor: tcell.ColorDefault, // Transparent InputFieldBackgroundColor: p.TransparentColor,
InputFieldTextColor: tcell.ColorWhite, InputFieldTextColor: p.TextColor,
// Completion prompt // Completion prompt
CompletionHintColor: tcell.NewRGBColor(128, 128, 128), // Medium gray for hint text CompletionHintColor: p.MutedColor,
// Burndown chart // Burndown chart
BurndownChartAxisColor: tcell.NewRGBColor(80, 80, 80), // Dark gray BurndownChartAxisColor: p.ChartAxisColor,
BurndownChartLabelColor: tcell.NewRGBColor(200, 200, 200), // Light gray BurndownChartLabelColor: p.ChartLabelColor,
BurndownChartValueColor: tcell.NewRGBColor(235, 235, 235), // Very light gray BurndownChartValueColor: p.ChartValueColor,
BurndownChartBarColor: tcell.NewRGBColor(120, 170, 255), // Light blue BurndownChartBarColor: p.ChartBarColor,
BurndownChartGradientFrom: Gradient{ BurndownChartGradientFrom: deepPurpleSolid,
Start: [3]int{134, 90, 214}, // Deep purple BurndownChartGradientTo: blueCyanSolid,
End: [3]int{134, 90, 214}, // Deep purple (solid, not gradient) BurndownHeaderGradientFrom: headerPurpleSolid,
}, BurndownHeaderGradientTo: headerCyanSolid,
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)
},
// Points visual bar // Points bar
PointsFilledColor: "[#508cff]", // Blue for filled segments PointsFilledColor: p.PointsFilledColor,
PointsUnfilledColor: "[#5f6982]", // Gray for unfilled segments PointsUnfilledColor: p.PointsUnfilledColor,
// Header // Header
HeaderInfoLabel: "[orange]", HeaderInfoLabel: p.InfoLabelColor,
HeaderInfoSeparator: "[#555555]", HeaderInfoSeparator: p.InfoSepColor,
HeaderInfoDesc: "[#888888]", HeaderInfoDesc: p.InfoDescColor,
HeaderKeyBinding: "[yellow]", HeaderKeyBinding: p.HighlightColor,
HeaderKeyText: "[white]", HeaderKeyText: p.TextColor,
// Header context help actions // Header context help actions
HeaderActionGlobalKeyColor: "#ffff00", // yellow for global actions HeaderActionGlobalKeyColor: p.HighlightColor,
HeaderActionGlobalLabelColor: "#ffffff", // white for global action labels HeaderActionGlobalLabelColor: p.TextColor,
HeaderActionPluginKeyColor: "#ff8c00", // orange for plugin actions HeaderActionPluginKeyColor: p.ActionKeyColor,
HeaderActionPluginLabelColor: "#b0b0b0", // light gray for plugin labels HeaderActionPluginLabelColor: p.ActionLabelColor,
HeaderActionViewKeyColor: "#5fafff", // cyan for view-specific actions HeaderActionViewKeyColor: p.ViewActionKeyColor,
HeaderActionViewLabelColor: "#808080", // gray for view-specific labels HeaderActionViewLabelColor: p.MutedColor,
// Plugin-specific // Plugin-specific
DepsEditorBackground: tcell.NewHexColor(0x4e5768), // Muted slate DepsEditorBackground: p.DepsEditorBgColor,
// Fallback solid colors (no-gradient terminals) // Fallback solid colors
FallbackTaskIDColor: tcell.NewRGBColor(0, 191, 255), // Deep Sky Blue FallbackTaskIDColor: p.DeepSkyBlue,
FallbackBurndownColor: tcell.NewRGBColor(134, 90, 214), // Purple FallbackBurndownColor: p.DeepPurple,
// Statusline (Nord theme) // Statusline
StatuslineBg: "#434c5e", // Nord polar night 3 StatuslineBg: p.NordPolarNight3,
StatuslineFg: "#d8dee9", // Nord snow storm 1 StatuslineFg: p.NordSnowStorm1,
StatuslineAccentBg: "#5e81ac", // Nord frost blue StatuslineAccentBg: p.NordFrostBlue,
StatuslineAccentFg: "#2e3440", // Nord polar night 1 StatuslineAccentFg: p.NordPolarNight1,
StatuslineInfoFg: "#a3be8c", // Nord aurora green StatuslineInfoFg: p.NordAuroraGreen,
StatuslineInfoBg: "#3b4252", // Nord polar night 2 StatuslineInfoBg: p.NordPolarNight2,
StatuslineErrorFg: "#ffff00", // yellow, matches header global key color StatuslineErrorFg: p.HighlightColor,
StatuslineErrorBg: "#3b4252", // Nord polar night 2 StatuslineErrorBg: p.NordPolarNight2,
StatuslineFillBg: "#3b4252", // Nord polar night 2 StatuslineFillBg: p.NordPolarNight2,
} }
} }
@ -251,13 +363,14 @@ func GetColors() *ColorConfig {
globalColors = DefaultColors() globalColors = DefaultColors()
// Apply theme-aware overrides for critical text colors // Apply theme-aware overrides for critical text colors
if GetEffectiveTheme() == "light" { if GetEffectiveTheme() == "light" {
globalColors.ContentBackgroundColor = tcell.ColorDefault black := NewColor(tcell.ColorBlack)
globalColors.ContentTextColor = tcell.ColorBlack globalColors.ContentBackgroundColor = DefaultColor()
globalColors.SearchBoxLabelColor = tcell.ColorBlack globalColors.ContentTextColor = black
globalColors.SearchBoxTextColor = tcell.ColorBlack globalColors.SearchBoxLabelColor = black
globalColors.InputFieldTextColor = tcell.ColorBlack globalColors.SearchBoxTextColor = black
globalColors.TaskDetailEditFocusText = "[black]" globalColors.InputFieldTextColor = black
globalColors.HeaderKeyText = "[black]" globalColors.TaskDetailEditFocusText = black
globalColors.HeaderKeyText = black
} }
colorsInitialized = true colorsInitialized = true
} }

View file

@ -381,9 +381,10 @@ func (c *TaskEditCoordinator) prepareView(activeView View, focus model.EditField
_ = c.FocusNextField(activeView) _ = c.FocusNextField(activeView)
} }
// rejectionMessage extracts clean rejection reasons from an error. // rejectionMessage extracts a clean user-facing message from an error.
// If the error wraps a RejectionError, returns just the reasons without // For RejectionError: returns just the rejection reasons.
// the "failed to update task:" / "validation failed:" prefixes. // 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 { func rejectionMessage(err error) string {
var re *service.RejectionError var re *service.RejectionError
if errors.As(err, &re) { if errors.As(err, &re) {
@ -393,5 +394,13 @@ func rejectionMessage(err error) string {
} }
return strings.Join(reasons, "; ") 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() return err.Error()
} }

View file

@ -3,6 +3,7 @@ package plugin
import ( import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/boolean-maybe/tiki/config"
"github.com/boolean-maybe/tiki/ruki" "github.com/boolean-maybe/tiki/ruki"
) )
@ -24,8 +25,8 @@ type BasePlugin struct {
Key tcell.Key // tcell key constant (e.g. KeyCtrlH) Key tcell.Key // tcell key constant (e.g. KeyCtrlH)
Rune rune // printable character (e.g. 'L') Rune rune // printable character (e.g. 'L')
Modifier tcell.ModMask // modifier keys (Alt, Shift, Ctrl, etc.) Modifier tcell.ModMask // modifier keys (Alt, Shift, Ctrl, etc.)
Foreground tcell.Color // caption text color Foreground config.Color // caption text color
Background tcell.Color // caption background color Background config.Color // caption background color
FilePath string // source file path (for error messages) FilePath string // source file path (for error messages)
ConfigIndex int // index in workflow.yaml views array (-1 if not from a config file) ConfigIndex int // index in workflow.yaml views array (-1 if not from a config file)
Type string // plugin type: "tiki" or "doki" Type string // plugin type: "tiki" or "doki"

View file

@ -1,9 +1,5 @@
package plugin package plugin
import (
"github.com/gdamore/tcell/v2"
)
// pluginFileConfig represents the YAML structure of a plugin file // pluginFileConfig represents the YAML structure of a plugin file
type pluginFileConfig struct { type pluginFileConfig struct {
Name string `yaml:"name"` Name string `yaml:"name"`
@ -58,10 +54,10 @@ func mergePluginDefinitions(base Plugin, override Plugin) Plugin {
result.Rune = overrideTiki.Rune result.Rune = overrideTiki.Rune
result.Modifier = overrideTiki.Modifier result.Modifier = overrideTiki.Modifier
} }
if overrideTiki.Foreground != tcell.ColorDefault { if !overrideTiki.Foreground.IsDefault() {
result.Foreground = overrideTiki.Foreground result.Foreground = overrideTiki.Foreground
} }
if overrideTiki.Background != tcell.ColorDefault { if !overrideTiki.Background.IsDefault() {
result.Background = overrideTiki.Background result.Background = overrideTiki.Background
} }
if len(overrideTiki.Lanes) > 0 { if len(overrideTiki.Lanes) > 0 {

View file

@ -5,6 +5,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/boolean-maybe/tiki/config"
rukiRuntime "github.com/boolean-maybe/tiki/internal/ruki/runtime" rukiRuntime "github.com/boolean-maybe/tiki/internal/ruki/runtime"
"github.com/boolean-maybe/tiki/ruki" "github.com/boolean-maybe/tiki/ruki"
) )
@ -30,8 +31,8 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) {
Key: tcell.KeyRune, Key: tcell.KeyRune,
Rune: 'B', Rune: 'B',
Modifier: 0, Modifier: 0,
Foreground: tcell.ColorRed, Foreground: config.NewColor(tcell.ColorRed),
Background: tcell.ColorBlue, Background: config.NewColor(tcell.ColorBlue),
Type: "tiki", Type: "tiki",
}, },
Lanes: []TikiLane{ Lanes: []TikiLane{
@ -47,8 +48,8 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) {
Key: tcell.KeyRune, Key: tcell.KeyRune,
Rune: 'O', Rune: 'O',
Modifier: tcell.ModAlt, Modifier: tcell.ModAlt,
Foreground: tcell.ColorGreen, Foreground: config.NewColor(tcell.ColorGreen),
Background: tcell.ColorDefault, Background: config.DefaultColor(),
FilePath: "override.yaml", FilePath: "override.yaml",
ConfigIndex: 1, ConfigIndex: 1,
Type: "tiki", Type: "tiki",
@ -72,7 +73,7 @@ func TestMergePluginDefinitions_TikiToTiki(t *testing.T) {
if resultTiki.Modifier != tcell.ModAlt { if resultTiki.Modifier != tcell.ModAlt {
t.Errorf("expected ModAlt, got %v", resultTiki.Modifier) 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) t.Errorf("expected green foreground, got %v", resultTiki.Foreground)
} }
if resultTiki.ViewMode != "expanded" { if resultTiki.ViewMode != "expanded" {
@ -92,8 +93,8 @@ func TestMergePluginDefinitions_PreservesModifier(t *testing.T) {
Key: tcell.KeyRune, Key: tcell.KeyRune,
Rune: 'M', Rune: 'M',
Modifier: tcell.ModAlt, // this should be preserved Modifier: tcell.ModAlt, // this should be preserved
Foreground: tcell.ColorWhite, Foreground: config.NewColor(tcell.ColorWhite),
Background: tcell.ColorDefault, Background: config.DefaultColor(),
Type: "tiki", Type: "tiki",
}, },
Lanes: []TikiLane{ Lanes: []TikiLane{

View file

@ -9,6 +9,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/boolean-maybe/tiki/config"
"github.com/boolean-maybe/tiki/ruki" "github.com/boolean-maybe/tiki/ruki"
) )
@ -20,8 +21,8 @@ func parsePluginConfig(cfg pluginFileConfig, source string, schema ruki.Schema)
// Common fields // Common fields
// Use ColorDefault as sentinel so views can detect "not specified" and use theme-appropriate colors // Use ColorDefault as sentinel so views can detect "not specified" and use theme-appropriate colors
fg := parseColor(cfg.Foreground, tcell.ColorDefault) fg := config.NewColor(parseColor(cfg.Foreground, tcell.ColorDefault))
bg := parseColor(cfg.Background, tcell.ColorDefault) bg := config.NewColor(parseColor(cfg.Background, tcell.ColorDefault))
key, r, mod, err := parseKey(cfg.Key) key, r, mod, err := parseKey(cfg.Key)
if err != nil { 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. // 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. // 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 { if len(text) == 0 {
return "" return ""
} }
@ -136,7 +136,7 @@ func RenderAdaptiveGradientText(text string, gradient config.Gradient, fallbackC
} }
// GradientFromColor derives a gradient by lightening the base color. // 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() r, g, b := primary.RGB()
if r == 0 && g == 0 && b == 0 { if r == 0 && g == 0 && b == 0 {
return fallback 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. // 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() r, g, b := primary.RGB()
if r == 0 && g == 0 && b == 0 { if r == 0 && g == 0 && b == 0 {
return fallback return fallback

View file

@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/boolean-maybe/tiki/config" "github.com/boolean-maybe/tiki/config"
"github.com/gdamore/tcell/v2"
) )
func TestInterpolateRGB(t *testing.T) { func TestInterpolateRGB(t *testing.T) {
@ -227,7 +226,7 @@ func TestGradientFromColor(t *testing.T) {
} }
t.Run("black color uses fallback", func(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) got := GradientFromColor(black, 0.5, fallback)
if got != fallback { if got != fallback {
t.Errorf("GradientFromColor(black) should return fallback, got %v", got) 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) { 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) got := GradientFromColor(blue, 0.5, fallback)
// Should have base color and lighter version // 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) { 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) got := GradientFromColorVibrant(black, 1.5, fallback)
if got != fallback { if got != fallback {
t.Errorf("GradientFromColorVibrant(black) should return fallback, got %v", got) 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) { 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) got := GradientFromColorVibrant(blue, 1.5, fallback)
// Should have base color and boosted version // Should have base color and boosted version
@ -301,7 +300,7 @@ func TestRenderAdaptiveGradientText(t *testing.T) {
Start: [3]int{30, 144, 255}, // Dodger Blue Start: [3]int{30, 144, 255}, // Dodger Blue
End: [3]int{0, 191, 255}, // Deep Sky 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 { tests := []struct {
name string name string
@ -427,15 +426,15 @@ func TestAdaptiveGradientRespectConfig(t *testing.T) {
Start: [3]int{100, 100, 100}, Start: [3]int{100, 100, 100},
End: [3]int{200, 200, 200}, End: [3]int{200, 200, 200},
} }
fallback := tcell.NewRGBColor(200, 200, 200) fallbackColor := config.NewColorRGB(200, 200, 200)
text := "Test" text := "Test"
// Test toggle behavior // Test toggle behavior
config.UseGradients = true config.UseGradients = true
resultWithGradients := RenderAdaptiveGradientText(text, gradient, fallback) resultWithGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor)
config.UseGradients = false config.UseGradients = false
resultWithoutGradients := RenderAdaptiveGradientText(text, gradient, fallback) resultWithoutGradients := RenderAdaptiveGradientText(text, gradient, fallbackColor)
// Results should be different // Results should be different
if resultWithGradients == resultWithoutGradients { if resultWithGradients == resultWithoutGradients {

View file

@ -1,6 +1,10 @@
package util package util
import "strings" import (
"strings"
"github.com/boolean-maybe/tiki/config"
)
// GeneratePointsVisual formats points as a visual representation using a styled bar. // GeneratePointsVisual formats points as a visual representation using a styled bar.
// Points are scaled to a 0-10 display range based on maxPoints configuration. // Points are scaled to a 0-10 display range based on maxPoints configuration.
@ -8,8 +12,8 @@ import "strings"
// Parameters: // Parameters:
// - points: The task's point value // - points: The task's point value
// - maxPoints: The configured maximum points value (for scaling) // - maxPoints: The configured maximum points value (for scaling)
// - filledColor: tview color tag for filled segments (e.g. "[#508cff]") // - filledColor: Color for filled segments
// - unfilledColor: tview color tag for unfilled segments (e.g. "[#5f6982]") // - unfilledColor: Color for unfilled segments
// //
// Returns: A string with colored filled (❚) and unfilled (❘) segments representing the points value. // Returns: A string with colored filled (❚) and unfilled (❘) segments representing the points value.
// //
@ -17,8 +21,8 @@ import "strings"
// //
// Example: // Example:
// //
// GeneratePointsVisual(7, 10, "[#508cff]", "[#5f6982]") returns a bar with 7 blue segments and 3 gray segments // GeneratePointsVisual(7, 10, filledColor, unfilledColor) returns a bar with 7 blue segments and 3 gray segments
func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfilledColor string) string { func GeneratePointsVisual(points int, maxPoints int, filledColor config.Color, unfilledColor config.Color) string {
const displaySegments = 10 const displaySegments = 10
const filledChar = "❚" const filledChar = "❚"
const unfilledChar = "❘" const unfilledChar = "❘"
@ -36,7 +40,7 @@ func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfille
// Add filled segments // Add filled segments
if displayPoints > 0 { if displayPoints > 0 {
result.WriteString(filledColor) result.WriteString(filledColor.Tag().String())
for i := 0; i < displayPoints; i++ { for i := 0; i < displayPoints; i++ {
result.WriteString(filledChar) result.WriteString(filledChar)
} }
@ -44,7 +48,7 @@ func GeneratePointsVisual(points int, maxPoints int, filledColor string, unfille
// Add unfilled segments // Add unfilled segments
if displayPoints < displaySegments { if displayPoints < displaySegments {
result.WriteString(unfilledColor) result.WriteString(unfilledColor.Tag().String())
for i := displayPoints; i < displaySegments; i++ { for i := displayPoints; i < displaySegments; i++ {
result.WriteString(unfilledChar) result.WriteString(unfilledChar)
} }

View file

@ -1,8 +1,14 @@
package util package util
import "testing" import (
"testing"
"github.com/boolean-maybe/tiki/config"
)
func TestGeneratePointsVisual(t *testing.T) { func TestGeneratePointsVisual(t *testing.T) {
blue := config.NewColorHex("#508cff")
gray := config.NewColorHex("#5f6982")
const blueColor = "[#508cff]" const blueColor = "[#508cff]"
const grayColor = "[#5f6982]" const grayColor = "[#5f6982]"
const resetColor = "[-]" const resetColor = "[-]"
@ -65,7 +71,7 @@ func TestGeneratePointsVisual(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if got != tt.want {
t.Errorf("GeneratePointsVisual(%d, %d) = %q, want %q", tt.points, tt.maxPoints, 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() 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) DrawSingleLineBorderWithStyle(screen, x, y, width, height, style)
} }

View file

@ -57,8 +57,8 @@ func NewDokiView(
func (dv *DokiView) build() { func (dv *DokiView) build() {
// title bar with gradient background using plugin color // title bar with gradient background using plugin color
textColor := tcell.ColorDefault textColor := config.DefaultColor()
if dv.pluginDef.Foreground != tcell.ColorDefault { if !dv.pluginDef.Foreground.IsDefault() {
textColor = dv.pluginDef.Foreground textColor = dv.pluginDef.Foreground
} }
dv.titleBar = NewGradientCaptionRow([]string{dv.pluginDef.Name}, nil, dv.pluginDef.Background, textColor) dv.titleBar = NewGradientCaptionRow([]string{dv.pluginDef.Name}, nil, dv.pluginDef.Background, textColor)

View file

@ -15,12 +15,12 @@ type GradientCaptionRow struct {
laneNames []string laneNames []string
laneWidths []int // proportional widths (same values used in tview.Flex) laneWidths []int // proportional widths (same values used in tview.Flex)
gradient config.Gradient // computed gradient (for truecolor/256-color terminals) gradient config.Gradient // computed gradient (for truecolor/256-color terminals)
textColor tcell.Color textColor config.Color
} }
// NewGradientCaptionRow creates a new gradient caption row widget. // NewGradientCaptionRow creates a new gradient caption row widget.
// laneWidths should match the flex proportions used for lane layout (nil = equal). // 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{ return &GradientCaptionRow{
Box: tview.NewBox(), Box: tview.NewBox(),
laneNames: laneNames, laneNames: laneNames,
@ -106,7 +106,7 @@ func (gcr *GradientCaptionRow) Draw(screen tcell.Screen) {
} }
// Render the cell with gradient background // 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++ { for row := 0; row < height; row++ {
screen.SetContent(x+col, y+row, char, nil, style) 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. // 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 fallback := config.GetColors().CaptionFallbackGradient
if useVibrantPluginGradient { if useVibrantPluginGradient {
return gradient.GradientFromColorVibrant(primary, vibrantBoost, fallback) return gradient.GradientFromColorVibrant(primary, vibrantBoost, fallback)

View file

@ -17,7 +17,7 @@ type ChartWidget struct {
func NewChartWidgetSimple() *ChartWidget { func NewChartWidgetSimple() *ChartWidget {
colors := config.GetColors() colors := config.GetColors()
chartTheme := barchart.DefaultTheme() chartTheme := barchart.DefaultTheme()
chartTheme.AxisColor = colors.BurndownChartAxisColor chartTheme.AxisColor = colors.BurndownChartAxisColor.TCell()
chartTheme.BarGradientFrom = colors.BurndownHeaderGradientFrom.Start // Use header-specific gradient chartTheme.BarGradientFrom = colors.BurndownHeaderGradientFrom.Start // Use header-specific gradient
chartTheme.BarGradientTo = colors.BurndownHeaderGradientTo.Start // Use header-specific gradient chartTheme.BarGradientTo = colors.BurndownHeaderGradientTo.Start // Use header-specific gradient
chartTheme.DotChar = '⣿' // braille full cell for compact dots 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 // ColorScheme defines color pairs for different action categories
type ColorScheme struct { type ColorScheme struct {
KeyColor string KeyColor config.Color
LabelColor string LabelColor config.Color
} }
// getColorScheme returns the color scheme for the given action type. // 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 // Render cell with colors
scheme := getColorScheme(cell.colorType) 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 // Add key padding
if keyPadding := maxKeyLenPerCol[col] - cell.keyLen; keyPadding > 0 { if keyPadding := maxKeyLenPerCol[col] - cell.keyLen; keyPadding > 0 {

View file

@ -30,14 +30,15 @@ func NewInfoWidget() *InfoWidget {
func (iw *InfoWidget) SetViewInfo(name, description string) { func (iw *InfoWidget) SetViewInfo(name, description string) {
colors := config.GetColors() colors := config.GetColors()
// convert "[orange]" to "[orange::b]" for bold name boldColor := colors.HeaderInfoLabel.Tag().Bold().String()
boldColor := makeBold(colors.HeaderInfoLabel) separatorTag := colors.HeaderInfoSeparator.Tag().String()
descTag := colors.HeaderInfoDesc.Tag().String()
separator := strings.Repeat("─", InfoWidth) separator := strings.Repeat("─", InfoWidth)
var text string var text string
if description != "" { 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 { } else {
text = fmt.Sprintf("%s%s[-::-]", boldColor, name) text = fmt.Sprintf("%s%s[-::-]", boldColor, name)
} }
@ -45,11 +46,6 @@ func (iw *InfoWidget) SetViewInfo(name, description string) {
iw.SetText(text) 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 // Primitive returns the underlying tview primitive
func (iw *InfoWidget) Primitive() tview.Primitive { func (iw *InfoWidget) Primitive() tview.Primitive {
return iw.TextView return iw.TextView

View file

@ -47,7 +47,7 @@ func NewNavigableMarkdown(cfg NavigableMarkdownConfig) *NavigableMarkdown {
renderer = renderer.WithCodeBorder(b) renderer = renderer.WithCodeBorder(b)
} }
nm.viewer.SetRenderer(renderer) nm.viewer.SetRenderer(renderer)
nm.viewer.SetBackgroundColor(config.GetColors().ContentBackgroundColor) nm.viewer.SetBackgroundColor(config.GetColors().ContentBackgroundColor.TCell())
if cfg.ImageManager != nil && cfg.ImageManager.Supported() { if cfg.ImageManager != nil && cfg.ImageManager.Supported() {
nm.viewer.SetImageManager(cfg.ImageManager) nm.viewer.SetImageManager(cfg.ImageManager)
} }

View file

@ -21,9 +21,9 @@ func NewSearchBox() *SearchBox {
// Configure the input field (border drawn manually in Draw) // Configure the input field (border drawn manually in Draw)
inputField.SetLabel("> ") inputField.SetLabel("> ")
inputField.SetLabelColor(colors.SearchBoxLabelColor) inputField.SetLabelColor(colors.SearchBoxLabelColor.TCell())
inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor) inputField.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
inputField.SetFieldTextColor(colors.ContentTextColor) inputField.SetFieldTextColor(colors.ContentTextColor.TCell())
inputField.SetBorder(false) inputField.SetBorder(false)
sb := &SearchBox{ sb := &SearchBox{
@ -60,7 +60,7 @@ func (sb *SearchBox) Draw(screen tcell.Screen) {
} }
// Fill interior with theme-aware background color // Fill interior with theme-aware background color
bgColor := config.GetColors().ContentBackgroundColor bgColor := config.GetColors().ContentBackgroundColor.TCell()
bgStyle := tcell.StyleDefault.Background(bgColor) bgStyle := tcell.StyleDefault.Background(bgColor)
for row := y; row < y+height; row++ { for row := y; row < y+height; row++ {
for col := x; col < x+width; col++ { for col := x; col < x+width; col++ {

View file

@ -102,7 +102,7 @@ func (sw *StatuslineWidget) render(width int) {
if padLen < 1 { if padLen < 1 {
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) sw.SetText(left + msgRendered + padding + rightStats)
} }
@ -121,14 +121,14 @@ func (sw *StatuslineWidget) renderLeftSegments(segments []statSegment, colors *c
bg, fg := segmentColors(i, colors) bg, fg := segmentColors(i, colors)
// segment text: " value " // 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 // separator: fg = current bg (creates the arrow), bg = next segment's bg or fill
nextBg := colors.StatuslineFillBg nextBg := colors.StatuslineFillBg
if i < len(segments)-1 { if i < len(segments)-1 {
nextBg, _ = segmentColors(i+1, colors) 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 // reset colors
@ -153,10 +153,10 @@ func (sw *StatuslineWidget) renderRightSegments(segments []statSegment, colors *
if i > 0 { if i > 0 {
prevBg, _ = segmentColors(i-1, colors) 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 // 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 // reset colors
@ -166,7 +166,7 @@ func (sw *StatuslineWidget) renderRightSegments(segments []statSegment, colors *
// segmentColors returns (bg, fg) for a segment at the given index. // segmentColors returns (bg, fg) for a segment at the given index.
// Even indices use accent colors, odd indices use normal colors. // 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 { if index%2 == 0 {
return colors.StatuslineAccentBg, colors.StatuslineAccentFg return colors.StatuslineAccentBg, colors.StatuslineAccentFg
} }
@ -179,11 +179,11 @@ func (sw *StatuslineWidget) renderMessage(msg string, level model.MessageLevel,
return "" return ""
} }
fg, bg := messageColors(level, colors) 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 // 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 { switch level {
case model.MessageLevelError: case model.MessageLevelError:
return colors.StatuslineErrorFg, colors.StatuslineErrorBg return colors.StatuslineErrorFg, colors.StatuslineErrorBg

View file

@ -11,15 +11,15 @@ import (
func testColors() *config.ColorConfig { func testColors() *config.ColorConfig {
return &config.ColorConfig{ return &config.ColorConfig{
StatuslineBg: "#normal_bg", StatuslineBg: config.NewColorHex("#111111"),
StatuslineFg: "#normal_fg", StatuslineFg: config.NewColorHex("#222222"),
StatuslineAccentBg: "#accent_bg", StatuslineAccentBg: config.NewColorHex("#333333"),
StatuslineAccentFg: "#accent_fg", StatuslineAccentFg: config.NewColorHex("#444444"),
StatuslineInfoFg: "#info_fg", StatuslineInfoFg: config.NewColorHex("#555555"),
StatuslineInfoBg: "#info_bg", StatuslineInfoBg: config.NewColorHex("#666666"),
StatuslineErrorFg: "#error_fg", StatuslineErrorFg: config.NewColorHex("#777777"),
StatuslineErrorBg: "#error_bg", StatuslineErrorBg: config.NewColorHex("#888888"),
StatuslineFillBg: "#fill_bg", StatuslineFillBg: config.NewColorHex("#999999"),
} }
} }
@ -108,11 +108,11 @@ func TestRenderLeftSegments_singleSegment(t *testing.T) {
result := sw.renderLeftSegments(segments, colors) result := sw.renderLeftSegments(segments, colors)
// first segment (index 0) uses accent 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) t.Errorf("first segment should use accent colors, got %q", result)
} }
// separator: fg=accent_bg (current), bg=fill (last segment) // 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) t.Errorf("separator should transition to fill background, got %q", result)
} }
// ends with color reset // ends with color reset
@ -132,15 +132,15 @@ func TestRenderLeftSegments_twoSegments(t *testing.T) {
result := sw.renderLeftSegments(segments, colors) result := sw.renderLeftSegments(segments, colors)
// first segment (index 0): accent // 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) t.Errorf("first segment should use accent, got %q", result)
} }
// separator between 1st and 2nd: fg=accent_bg, bg=normal_bg // 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) t.Errorf("separator should transition accent→normal, got %q", result)
} }
// second segment (index 1): normal // 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) 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) result := sw.renderRightSegments(segments, colors)
// index 0 (even) → accent 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) t.Errorf("segment 0 should use accent colors, got %q", result)
} }
// separator: fg=accent_bg, bg=fill (first right segment) // 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) 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) result := sw.renderRightSegments(segments, colors)
// index 0 (even) → accent // 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) t.Errorf("segment 0 should use accent, got %q", result)
} }
// index 1 (odd) → normal // 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) t.Errorf("segment 1 should use normal, got %q", result)
} }
// separator between 0→1: fg=normal_bg, bg=accent_bg (prev segment) // 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) 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) result := sw.renderRightSegments(segments, colors)
// index 0 → accent, index 1 → normal, index 2 → accent // 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") 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") 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") t.Error("segment 2 should use accent")
} }
} }
@ -230,7 +230,7 @@ func TestRenderMessage_info(t *testing.T) {
colors := testColors() colors := testColors()
result := sw.renderMessage("task saved", model.MessageLevelInfo, colors) 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) t.Errorf("info message should use info colors, got %q", result)
} }
if !strings.HasSuffix(result, "[-:-]") { if !strings.HasSuffix(result, "[-:-]") {
@ -243,7 +243,7 @@ func TestRenderMessage_error(t *testing.T) {
colors := testColors() colors := testColors()
result := sw.renderMessage("validation failed", model.MessageLevelError, colors) 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) t.Errorf("error message should use error colors, got %q", result)
} }
if !strings.HasSuffix(result, "[-:-]") { if !strings.HasSuffix(result, "[-:-]") {
@ -255,18 +255,18 @@ func TestRightSegmentColors(t *testing.T) {
colors := testColors() colors := testColors()
bg0, fg0 := segmentColors(0, colors) bg0, fg0 := segmentColors(0, colors)
if bg0 != "#accent_bg" || fg0 != "#accent_fg" { if bg0.Hex() != colors.StatuslineAccentBg.Hex() || fg0.Hex() != colors.StatuslineAccentFg.Hex() {
t.Errorf("index 0: got (%s, %s), want accent", bg0, fg0) t.Errorf("index 0: got (%s, %s), want accent", bg0.Hex(), fg0.Hex())
} }
bg1, fg1 := segmentColors(1, colors) bg1, fg1 := segmentColors(1, colors)
if bg1 != "#normal_bg" || fg1 != "#normal_fg" { if bg1.Hex() != colors.StatuslineBg.Hex() || fg1.Hex() != colors.StatuslineFg.Hex() {
t.Errorf("index 1: got (%s, %s), want normal", bg1, fg1) t.Errorf("index 1: got (%s, %s), want normal", bg1.Hex(), fg1.Hex())
} }
bg2, fg2 := segmentColors(2, colors) bg2, fg2 := segmentColors(2, colors)
if bg2 != "#accent_bg" || fg2 != "#accent_fg" { if bg2.Hex() != colors.StatuslineAccentBg.Hex() || fg2.Hex() != colors.StatuslineAccentFg.Hex() {
t.Errorf("index 2: got (%s, %s), want accent", bg2, fg2) 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 // applyFrameStyle applies selected/unselected styling to a frame
func applyFrameStyle(frame *tview.Frame, selected bool, colors *config.ColorConfig) { func applyFrameStyle(frame *tview.Frame, selected bool, colors *config.ColorConfig) {
if selected { if selected {
frame.SetBorderColor(colors.TaskBoxSelectedBorder) frame.SetBorderColor(colors.TaskBoxSelectedBorder.TCell())
} else { } else {
frame.SetBorderColor(colors.TaskBoxUnselectedBorder) frame.SetBorderColor(colors.TaskBoxUnselectedBorder.TCell())
if colors.TaskBoxUnselectedBackground != tcell.ColorDefault { if !colors.TaskBoxUnselectedBackground.IsDefault() {
frame.SetBackgroundColor(colors.TaskBoxUnselectedBackground) frame.SetBackgroundColor(colors.TaskBoxUnselectedBackground.TCell())
} }
} }
} }
@ -35,11 +35,14 @@ func buildCompactTaskContent(task *taskpkg.Task, colors *config.ColorConfig, ava
priorityEmoji := taskpkg.PriorityLabel(task.Priority) priorityEmoji := taskpkg.PriorityLabel(task.Priority)
pointsVisual := util.GeneratePointsVisual(task.Points, config.GetMaxPoints(), colors.PointsFilledColor, colors.PointsUnfilledColor) 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[-]", return fmt.Sprintf("%s %s\n%s%s[-]\n%spriority[-] %s %spoints[-] %s%s[-]",
emoji, idGradient, emoji, idGradient,
colors.TaskBoxTitleColor, truncatedTitle, titleTag, truncatedTitle,
colors.TaskBoxLabelColor, priorityEmoji, labelTag, priorityEmoji,
colors.TaskBoxLabelColor, colors.TaskBoxLabelColor, pointsVisual) labelTag, labelTag, pointsVisual)
} }
// buildExpandedTaskContent builds the content string for expanded task display // 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)) 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 // Build tags string
tagsStr := "" tagsStr := ""
if len(task.Tags) > 0 { 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 // Build priority/points line
priorityEmoji := taskpkg.PriorityLabel(task.Priority) priorityEmoji := taskpkg.PriorityLabel(task.Priority)
pointsVisual := util.GeneratePointsVisual(task.Points, config.GetMaxPoints(), colors.PointsFilledColor, colors.PointsUnfilledColor) pointsVisual := util.GeneratePointsVisual(task.Points, config.GetMaxPoints(), colors.PointsFilledColor, colors.PointsUnfilledColor)
priorityPointsStr := fmt.Sprintf("%spriority[-] %s %spoints[-] %s%s[-]", priorityPointsStr := fmt.Sprintf("%spriority[-] %s %spoints[-] %s%s[-]",
colors.TaskBoxLabelColor, priorityEmoji, labelTag, priorityEmoji,
colors.TaskBoxLabelColor, colors.TaskBoxLabelColor, pointsVisual) labelTag, labelTag, pointsVisual)
return fmt.Sprintf("%s %s\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s\n%s", return fmt.Sprintf("%s %s\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s%s[-]\n%s\n%s",
emoji, idGradient, emoji, idGradient,
colors.TaskBoxTitleColor, truncatedTitle, titleTag, truncatedTitle,
colors.TaskBoxDescriptionColor, descLine1, descTag, descLine1,
colors.TaskBoxDescriptionColor, descLine2, descTag, descLine2,
colors.TaskBoxDescriptionColor, descLine3, descTag, descLine3,
tagsStr, priorityPointsStr) tagsStr, priorityPointsStr)
} }

View file

@ -115,7 +115,7 @@ func (b *Base) assembleMetadataBox(
metadataBox := tview.NewFrame(metadataContainer).SetBorders(0, 0, 0, 0, 0, 0) metadataBox := tview.NewFrame(metadataContainer).SetBorders(0, 0, 0, 0, 0, 0)
metadataBox.SetBorder(true).SetTitle( metadataBox.SetBorder(true).SetTitle(
fmt.Sprintf(" %s ", gradient.RenderAdaptiveGradientText(task.ID, colors.TaskDetailIDColor, colors.FallbackTaskIDColor)), fmt.Sprintf(" %s ", gradient.RenderAdaptiveGradientText(task.ID, colors.TaskDetailIDColor, colors.FallbackTaskIDColor)),
).SetBorderColor(colors.TaskBoxUnselectedBorder) ).SetBorderColor(colors.TaskBoxUnselectedBorder.TCell())
metadataBox.SetBorderPadding(1, 0, 2, 2) metadataBox.SetBorderPadding(1, 0, 2, 2)
return metadataBox 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 // 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 { if mode == RenderModeEdit && !focused {
return dimColor 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 // getFocusMarker returns the focus marker string (arrow + text color) from colors config
func getFocusMarker(colors *config.ColorConfig) string { 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 // 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 focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldStatus
statusDisplay := taskpkg.StatusDisplay(task.Status) statusDisplay := taskpkg.StatusDisplay(task.Status)
labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String()
valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String()
focusMarker := "" focusMarker := ""
if focused && ctx.Mode == RenderModeEdit { if focused && ctx.Mode == RenderModeEdit {
focusMarker = getFocusMarker(ctx.Colors) 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
textView.SetBorderPadding(0, 0, 0, 0) textView.SetBorderPadding(0, 0, 0, 0)
@ -69,15 +69,15 @@ func RenderTypeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive
typeDisplay = "[gray](none)[-]" typeDisplay = "[gray](none)[-]"
} }
labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String()
valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String()
focusMarker := "" focusMarker := ""
if focused && ctx.Mode == RenderModeEdit { if focused && ctx.Mode == RenderModeEdit {
focusMarker = getFocusMarker(ctx.Colors) 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
textView.SetBorderPadding(0, 0, 0, 0) 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 { func RenderPriorityText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive {
focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPriority focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPriority
labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String()
valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String()
focusMarker := "" focusMarker := ""
if focused && ctx.Mode == RenderModeEdit { if focused && ctx.Mode == RenderModeEdit {
focusMarker = getFocusMarker(ctx.Colors) 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
textView.SetBorderPadding(0, 0, 0, 0) 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 { func RenderAssigneeText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive {
focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldAssignee focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldAssignee
labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String()
valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String()
focusMarker := "" focusMarker := ""
if focused && ctx.Mode == RenderModeEdit { if focused && ctx.Mode == RenderModeEdit {
focusMarker = getFocusMarker(ctx.Colors) 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
textView.SetBorderPadding(0, 0, 0, 0) 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 { func RenderPointsText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive {
focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPoints focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldPoints
labelColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor) labelTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailLabelText, ctx.Colors.TaskDetailEditDimLabelColor).Tag().String()
valueColor := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor) valueTag := getDimOrFullColor(ctx.Mode, focused, ctx.Colors.TaskDetailValueText, ctx.Colors.TaskDetailEditDimValueColor).Tag().String()
focusMarker := "" focusMarker := ""
if focused && ctx.Mode == RenderModeEdit { if focused && ctx.Mode == RenderModeEdit {
focusMarker = getFocusMarker(ctx.Colors) 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
textView.SetBorderPadding(0, 0, 0, 0) 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 // RenderTitleText renders a title as read-only text
func RenderTitleText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive { func RenderTitleText(task *taskpkg.Task, ctx FieldRenderContext) tview.Primitive {
focused := ctx.Mode == RenderModeEdit && ctx.FocusedField == model.EditFieldTitle 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) var titleTag string
titleText := fmt.Sprintf("%s%s%s", titleColor, tview.Escape(task.Title), ctx.Colors.TaskDetailValueText) 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(). titleBox := tview.NewTextView().
SetDynamicColors(true). SetDynamicColors(true).
SetText(titleText) SetText(titleText)
@ -159,7 +165,7 @@ func RenderTagsColumn(task *taskpkg.Task) tview.Primitive {
return tview.NewBox() return tview.NewBox()
} }
colors := config.GetColors() 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) label.SetBorderPadding(0, 0, 0, 0)
col := tview.NewFlex().SetDirection(tview.FlexRow) col := tview.NewFlex().SetDirection(tview.FlexRow)
@ -186,7 +192,7 @@ func RenderDependsOnColumn(task *taskpkg.Task, taskStore store.Store) tview.Prim
} }
colors := config.GetColors() 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) label.SetBorderPadding(0, 0, 0, 0)
col := tview.NewFlex().SetDirection(tview.FlexRow) col := tview.NewFlex().SetDirection(tview.FlexRow)
@ -204,7 +210,7 @@ func RenderBlocksColumn(blocked []*taskpkg.Task) tview.Primitive {
} }
colors := config.GetColors() 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) label.SetBorderPadding(0, 0, 0, 0)
col := tview.NewFlex().SetDirection(tview.FlexRow) 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 // RenderAuthorText renders the author field as read-only text
func RenderAuthorText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primitive { func RenderAuthorText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primitive {
text := fmt.Sprintf("%s%-10s%s%s", 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
view.SetBorderPadding(0, 0, 0, 0) view.SetBorderPadding(0, 0, 0, 0)
return view 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") createdAtStr = task.CreatedAt.Format("2006-01-02 15:04")
} }
text := fmt.Sprintf("%s%-10s%s%s", 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
view.SetBorderPadding(0, 0, 0, 0) view.SetBorderPadding(0, 0, 0, 0)
return view 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") updatedAtStr = task.UpdatedAt.Format("2006-01-02 15:04")
} }
text := fmt.Sprintf("%s%-10s%s%s", 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
view.SetBorderPadding(0, 0, 0, 0) view.SetBorderPadding(0, 0, 0, 0)
return view return view
@ -331,7 +337,7 @@ func RenderDueText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primiti
dueDisplay = task.Due.Format("2006-01-02") dueDisplay = task.Due.Format("2006-01-02")
} }
text := fmt.Sprintf("%s%-12s%s%s", 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
view.SetBorderPadding(0, 0, 0, 0) view.SetBorderPadding(0, 0, 0, 0)
return view 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 { func RenderRecurrenceText(task *taskpkg.Task, colors *config.ColorConfig) tview.Primitive {
display := taskpkg.RecurrenceDisplay(task.Recurrence) display := taskpkg.RecurrenceDisplay(task.Recurrence)
text := fmt.Sprintf("%s%-12s%s%s", 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 := tview.NewTextView().SetDynamicColors(true).SetText(text)
view.SetBorderPadding(0, 0, 0, 0) view.SetBorderPadding(0, 0, 0, 0)
return view return view

View file

@ -381,7 +381,7 @@ func (ev *TaskEditView) ensureTagsTextArea(task *taskpkg.Task) *tview.TextArea {
ev.tagsTextArea.SetBorder(false) ev.tagsTextArea.SetBorder(false)
ev.tagsTextArea.SetBorderPadding(1, 1, 2, 2) ev.tagsTextArea.SetBorderPadding(1, 1, 2, 2)
ev.tagsTextArea.SetPlaceholder("Enter tags separated by spaces") 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 { ev.tagsTextArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyCtrlS { if event.Key() == tcell.KeyCtrlS {
@ -448,8 +448,8 @@ func (ev *TaskEditView) ensureTitleInput(task *taskpkg.Task) *tview.InputField {
if ev.titleInput == nil { if ev.titleInput == nil {
colors := config.GetColors() colors := config.GetColors()
ev.titleInput = tview.NewInputField() ev.titleInput = tview.NewInputField()
ev.titleInput.SetFieldBackgroundColor(colors.ContentBackgroundColor) ev.titleInput.SetFieldBackgroundColor(colors.ContentBackgroundColor.TCell())
ev.titleInput.SetFieldTextColor(colors.InputFieldTextColor) ev.titleInput.SetFieldTextColor(colors.InputFieldTextColor.TCell())
ev.titleInput.SetBorder(false) ev.titleInput.SetBorder(false)
ev.titleInput.SetChangedFunc(func(text string) { ev.titleInput.SetChangedFunc(func(text string) {
@ -501,9 +501,9 @@ func (ev *TaskEditView) updateValidationState() {
if ev.metadataBox != nil { if ev.metadataBox != nil {
colors := config.DefaultColors() colors := config.DefaultColors()
if len(ev.validationErrors) > 0 { if len(ev.validationErrors) > 0 {
ev.metadataBox.SetBorderColor(colors.TaskBoxSelectedBorder) ev.metadataBox.SetBorderColor(colors.TaskBoxSelectedBorder.TCell())
} else { } 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/store"
"github.com/boolean-maybe/tiki/task" "github.com/boolean-maybe/tiki/task"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "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 // PluginView renders a filtered/sorted list of tasks across lanes
type PluginView struct { type PluginView struct {
root *tview.Flex root *tview.Flex
@ -61,8 +58,8 @@ func NewPluginView(
func (pv *PluginView) build() { func (pv *PluginView) build() {
// title bar with gradient background using plugin color // title bar with gradient background using plugin color
textColor := tcell.ColorDefault textColor := config.DefaultColor()
if pv.pluginDef.Foreground != tcell.ColorDefault { if !pv.pluginDef.Foreground.IsDefault() {
textColor = pv.pluginDef.Foreground textColor = pv.pluginDef.Foreground
} }
laneNames := make([]string, len(pv.pluginDef.Lanes)) laneNames := make([]string, len(pv.pluginDef.Lanes))