Files
tasksquire/common/styles.go
Martin Pander 14dbfc406d [WIP] Layout
2024-05-22 16:20:57 +02:00

375 lines
10 KiB
Go

package common
import (
"errors"
"log/slog"
"math"
"strconv"
"strings"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"tasksquire/taskwarrior"
)
type Styles struct {
Main lipgloss.Style
Form *huh.Theme
Active lipgloss.Style
Alternate lipgloss.Style
Blocked lipgloss.Style
Blocking lipgloss.Style
BurndownDone lipgloss.Style
BurndownPending lipgloss.Style
BurndownStarted lipgloss.Style
CalendarDue lipgloss.Style
CalendarDueToday lipgloss.Style
CalendarHoliday lipgloss.Style
CalendarOverdue lipgloss.Style
CalendarScheduled lipgloss.Style
CalendarToday lipgloss.Style
CalendarWeekend lipgloss.Style
CalendarWeeknumber lipgloss.Style
Completed lipgloss.Style
Debug lipgloss.Style
Deleted lipgloss.Style
Due lipgloss.Style
DueToday lipgloss.Style
Error lipgloss.Style
Footnote lipgloss.Style
Header lipgloss.Style
HistoryAdd lipgloss.Style
HistoryDelete lipgloss.Style
HistoryDone lipgloss.Style
Label lipgloss.Style
LabelSort lipgloss.Style
Overdue lipgloss.Style
ProjectNone lipgloss.Style
Recurring lipgloss.Style
Scheduled lipgloss.Style
SummaryBackground lipgloss.Style
SummaryBar lipgloss.Style
SyncAdded lipgloss.Style
SyncChanged lipgloss.Style
SyncRejected lipgloss.Style
TagNext lipgloss.Style
TagNone lipgloss.Style
Tagged lipgloss.Style
UdaPriorityH lipgloss.Style
UdaPriorityL lipgloss.Style
UdaPriorityM lipgloss.Style
UndoAfter lipgloss.Style
UndoBefore lipgloss.Style
Until lipgloss.Style
Warning lipgloss.Style
}
func NewStyles(config *taskwarrior.TWConfig) *Styles {
styles := parseColors(config.GetConfig())
styles.Main = lipgloss.NewStyle()
formTheme := huh.ThemeBase()
formTheme.Focused.Title = formTheme.Focused.Title.Bold(true)
formTheme.Focused.SelectSelector = formTheme.Focused.SelectSelector.SetString("> ")
formTheme.Focused.SelectedOption = formTheme.Focused.SelectedOption.Bold(true)
formTheme.Focused.MultiSelectSelector = formTheme.Focused.MultiSelectSelector.SetString("> ")
formTheme.Focused.SelectedPrefix = formTheme.Focused.SelectedPrefix.SetString("✓ ")
formTheme.Focused.UnselectedPrefix = formTheme.Focused.SelectedPrefix.SetString("• ")
formTheme.Blurred.SelectSelector = formTheme.Blurred.SelectSelector.SetString(" ")
formTheme.Blurred.SelectedOption = formTheme.Blurred.SelectedOption.Bold(true)
formTheme.Blurred.MultiSelectSelector = formTheme.Blurred.MultiSelectSelector.SetString(" ")
formTheme.Blurred.SelectedPrefix = formTheme.Blurred.SelectedPrefix.SetString("✓ ")
formTheme.Blurred.UnselectedPrefix = formTheme.Blurred.SelectedPrefix.SetString("• ")
styles.Form = formTheme
return styles
}
func parseColors(config map[string]string) *Styles {
styles := Styles{}
for key, value := range config {
if strings.HasPrefix(key, "color.") {
if value != "" {
color := strings.Split(key, ".")[1]
switch color {
case "active":
styles.Active = parseColorString(value)
case "alternate":
styles.Alternate = parseColorString(value)
case "blocked":
styles.Blocked = parseColorString(value)
case "blocking":
styles.Blocking = parseColorString(value)
case "burndown.done":
styles.BurndownDone = parseColorString(value)
case "burndown.pending":
styles.BurndownPending = parseColorString(value)
case "burndown.started":
styles.BurndownStarted = parseColorString(value)
case "calendar.due":
styles.CalendarDue = parseColorString(value)
case "calendar.due.today":
styles.CalendarDueToday = parseColorString(value)
case "calendar.holiday":
styles.CalendarHoliday = parseColorString(value)
case "calendar.overdue":
styles.CalendarOverdue = parseColorString(value)
case "calendar.scheduled":
styles.CalendarScheduled = parseColorString(value)
case "calendar.today":
styles.CalendarToday = parseColorString(value)
case "calendar.weekend":
styles.CalendarWeekend = parseColorString(value)
case "calendar.weeknumber":
styles.CalendarWeeknumber = parseColorString(value)
case "completed":
styles.Completed = parseColorString(value)
case "debug":
styles.Debug = parseColorString(value)
case "deleted":
styles.Deleted = parseColorString(value)
case "due":
styles.Due = parseColorString(value)
case "due.today":
styles.DueToday = parseColorString(value)
case "error":
styles.Error = parseColorString(value)
case "footnote":
styles.Footnote = parseColorString(value)
case "header":
styles.Header = parseColorString(value)
case "history.add":
styles.HistoryAdd = parseColorString(value)
case "history.delete":
styles.HistoryDelete = parseColorString(value)
case "history.done":
styles.HistoryDone = parseColorString(value)
case "label":
styles.Label = parseColorString(value)
case "label.sort":
styles.LabelSort = parseColorString(value)
case "overdue":
styles.Overdue = parseColorString(value)
case "project.none":
styles.ProjectNone = parseColorString(value)
case "recurring":
styles.Recurring = parseColorString(value)
case "scheduled":
styles.Scheduled = parseColorString(value)
case "summary.background":
styles.SummaryBackground = parseColorString(value)
case "summary.bar":
styles.SummaryBar = parseColorString(value)
case "sync.added":
styles.SyncAdded = parseColorString(value)
case "sync.changed":
styles.SyncChanged = parseColorString(value)
case "sync.rejected":
styles.SyncRejected = parseColorString(value)
case "tag.next":
styles.TagNext = parseColorString(value)
case "tag.none":
styles.TagNone = parseColorString(value)
case "tagged":
styles.Tagged = parseColorString(value)
case "uda.priority.H":
styles.UdaPriorityH = parseColorString(value)
case "uda.priority.L":
styles.UdaPriorityL = parseColorString(value)
case "uda.priority.M":
styles.UdaPriorityM = parseColorString(value)
case "undo.after":
styles.UndoAfter = parseColorString(value)
case "undo.before":
styles.UndoBefore = parseColorString(value)
case "until":
styles.Until = parseColorString(value)
case "warning":
styles.Warning = parseColorString(value)
}
}
}
}
return &styles
}
func parseColorString(color string) lipgloss.Style {
style := lipgloss.NewStyle()
if strings.Contains(color, "on") {
fgbg := strings.Split(color, "on")
fg := strings.TrimSpace(fgbg[0])
bg := strings.TrimSpace(fgbg[1])
if fg != "" {
style = style.Foreground(parseColor(fg))
}
if bg != "" {
style = style.Background(parseColor(bg))
}
} else {
style = style.Foreground(parseColor(strings.TrimSpace(color)))
}
return style
}
func parseColor(color string) lipgloss.Color {
if strings.HasPrefix(color, "rgb") {
rgb, err := parseRGBString(strings.TrimPrefix(color, "rgb"))
if err != nil {
slog.Error("Invalid RGB color format")
return lipgloss.Color("0")
}
return lipgloss.Color(rgbToAnsi(rgb))
}
if strings.HasPrefix(color, "color") {
return lipgloss.Color(strings.TrimPrefix(color, "color"))
}
if strings.HasPrefix(color, "gray") {
gray, err := strconv.Atoi(strings.TrimPrefix(color, "gray"))
if err != nil {
slog.Error("Invalid gray color format")
return lipgloss.Color("0")
}
return lipgloss.Color(strconv.Itoa(gray + 232))
}
if ansi, okcolor := colorStrings[color]; okcolor {
return lipgloss.Color(strconv.Itoa(ansi))
}
slog.Error("Invalid color format")
return lipgloss.Color("0")
}
type RGB struct {
r int
g int
b int
}
func parseRGBString(rgbString string) (RGB, error) {
var err error
rgb := RGB{}
if len(rgbString) != 3 {
return rgb, errors.New("invalid RGB format")
}
rgb.r, err = strconv.Atoi(string(rgbString[0]))
if err != nil {
return rgb, errors.New("invalid value for R")
}
rgb.g, err = strconv.Atoi(string(rgbString[1]))
if err != nil {
return rgb, errors.New("invalid value for G")
}
rgb.b, err = strconv.Atoi(string(rgbString[2]))
if err != nil {
return rgb, errors.New("invalid value for B")
}
return rgb, nil
}
var colorStrings = map[string]int{
"black": 0,
"red": 1,
"green": 2,
"yellow": 3,
"blue": 4,
"magenta": 5,
"cyan": 6,
"white": 7,
"bright black": 8,
"bright red": 9,
"bright green": 10,
"bright yellow": 11,
"bright blue": 12,
"bright magenta": 13,
"bright cyan": 14,
"bright white": 15,
}
var baseColors = []RGB{
{0, 0, 0}, // Black
{128, 0, 0}, // Red
{0, 128, 0}, // Green
{128, 128, 0}, // Yellow
{0, 0, 128}, // Blue
{128, 0, 128}, // Magenta
{0, 128, 128}, // Cyan
{192, 192, 192}, // White
}
var highIntensityColors = []RGB{
{128, 128, 128}, // Bright Black (Gray)
{255, 0, 0}, // Bright Red
{0, 255, 0}, // Bright Green
{255, 255, 0}, // Bright Yellow
{0, 0, 255}, // Bright Blue
{255, 0, 255}, // Bright Magenta
{0, 255, 255}, // Bright Cyan
{255, 255, 255}, // Bright White
}
// Calculate the Euclidean distance between two colors
func colorDistance(c1, c2 RGB) float64 {
return math.Sqrt(float64((c1.r-c2.r)*(c1.r-c2.r) + (c1.g-c2.g)*(c1.g-c2.g) + (c1.b-c2.b)*(c1.b-c2.b)))
}
// Convert RGB to the nearest ANSI color code
func rgbToAnsi(rgb RGB) string {
// Check standard and high-intensity colors
allColors := append(baseColors, highIntensityColors...)
bestIndex := 0
minDist := colorDistance(rgb, allColors[0])
for i := 1; i < len(allColors); i++ {
dist := colorDistance(rgb, allColors[i])
if dist < minDist {
bestIndex = i
minDist = dist
}
}
if bestIndex < 8 {
return strconv.Itoa(bestIndex)
} else if bestIndex < 16 {
return strconv.Itoa(bestIndex + 8)
}
// Check 6x6x6 color cube
for i := 0; i < 216; i++ {
cubeColor := RGB{
(rgb.r / 51) * 51,
(rgb.g / 51) * 51,
(rgb.b / 51) * 51,
}
dist := colorDistance(rgb, cubeColor)
if dist < minDist {
bestIndex = i + 16
minDist = dist
}
}
// Check grayscale colors
for i := 0; i < 24; i++ {
gray := i*10 + 8
grayColor := RGB{gray, gray, gray}
dist := colorDistance(rgb, grayColor)
if dist < minDist {
bestIndex = i + 232
minDist = dist
}
}
return strconv.Itoa(bestIndex)
}