package common import ( "errors" "log/slog" "math" "strconv" "strings" // "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "tasksquire/components/table" "tasksquire/taskwarrior" ) type Styles struct { Main lipgloss.Style Form *huh.Theme TableStyle table.Styles 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() styles.TableStyle = table.Styles{ // Header: lipgloss.NewStyle().Bold(true).Padding(0, 1).BorderBottom(true), Cell: lipgloss.NewStyle().Padding(0, 1, 0, 0), Header: lipgloss.NewStyle().Bold(true).Padding(0, 1, 0, 0).Underline(true), Selected: lipgloss.NewStyle().Foreground(styles.Active.GetForeground()).Background(styles.Active.GetBackground()).Bold(true).Reverse(true), } 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) }