Style forms; [WIP] Draw table
This commit is contained in:
368
common/styles.go
368
common/styles.go
@ -1,6 +1,13 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"tasksquire/taskwarrior"
|
||||
@ -9,17 +16,360 @@ import (
|
||||
type Styles struct {
|
||||
Main lipgloss.Style
|
||||
|
||||
ActiveTask lipgloss.Style
|
||||
InactiveTask lipgloss.Style
|
||||
NormalTask 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 {
|
||||
return &Styles{
|
||||
Main: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
||||
styles := parseColors(config.GetConfig())
|
||||
styles.Main = lipgloss.NewStyle()
|
||||
|
||||
ActiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Background(lipgloss.Color("236")),
|
||||
InactiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Background(lipgloss.Color("236")),
|
||||
NormalTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
||||
}
|
||||
formTheme := huh.ThemeBase()
|
||||
formTheme.Focused.Card = formTheme.Focused.Card.BorderStyle(lipgloss.RoundedBorder()).BorderBottom(true).BorderTop(true)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user