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