package common import ( "log/slog" "strconv" "strings" // "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "tasksquire/taskwarrior" ) type TableStyle struct { Header lipgloss.Style Cell lipgloss.Style Selected lipgloss.Style } type Styles struct { Base lipgloss.Style Form *huh.Theme TableStyle TableStyle ColumnFocused lipgloss.Style ColumnBlurred lipgloss.Style ColumnInsert lipgloss.Style // TODO: make color config completely dynamic to account for keyword., project., tag. and uda. colors 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.Base = lipgloss.NewStyle() styles.TableStyle = TableStyle{ // 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().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 styles.ColumnFocused = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.DoubleBorder(), true) styles.ColumnBlurred = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.HiddenBorder(), true) styles.ColumnInsert = lipgloss.NewStyle().Width(30).Height(50).Border(lipgloss.DoubleBorder(), true).BorderForeground(styles.Active.GetForeground()) return styles } func parseColors(config map[string]string) *Styles { styles := Styles{} for key, value := range config { if strings.HasPrefix(key, "color.") { _, colorValue, _ := strings.Cut(key, ".") switch colorValue { 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 color == "" { return style } 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") { return lipgloss.Color(convertRgbToAnsi(strings.TrimPrefix(color, "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") } func convertRgbToAnsi(rgbString string) string { var err error if len(rgbString) != 3 { slog.Error("Invalid RGB color format") return "" } r, err := strconv.Atoi(string(rgbString[0])) if err != nil { slog.Error("Invalid value for R") return "" } g, err := strconv.Atoi(string(rgbString[1])) if err != nil { slog.Error("Invalid value for G") return "" } b, err := strconv.Atoi(string(rgbString[2])) if err != nil { slog.Error("Invalid value for B") return "" } return strconv.Itoa(16 + (36 * r) + (6 * g) + b) } 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, }