Fix table formatting

This commit is contained in:
Martin
2024-05-25 00:28:34 +02:00
parent f7b54b607b
commit 5d930685a3
8 changed files with 235 additions and 249 deletions

View File

@ -1,25 +1,29 @@
package common package common
import ( import (
"errors"
"log/slog" "log/slog"
"math"
"strconv" "strconv"
"strings" "strings"
// "github.com/charmbracelet/bubbles/table" // "github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"tasksquire/components/table"
"tasksquire/taskwarrior" "tasksquire/taskwarrior"
) )
type TableStyle struct {
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
}
type Styles struct { type Styles struct {
Main lipgloss.Style Base lipgloss.Style
Form *huh.Theme Form *huh.Theme
TableStyle table.Styles TableStyle TableStyle
Active lipgloss.Style Active lipgloss.Style
Alternate lipgloss.Style Alternate lipgloss.Style
@ -72,13 +76,13 @@ type Styles struct {
func NewStyles(config *taskwarrior.TWConfig) *Styles { func NewStyles(config *taskwarrior.TWConfig) *Styles {
styles := parseColors(config.GetConfig()) styles := parseColors(config.GetConfig())
styles.Main = lipgloss.NewStyle() styles.Base = lipgloss.NewStyle()
styles.TableStyle = table.Styles{ styles.TableStyle = TableStyle{
// Header: lipgloss.NewStyle().Bold(true).Padding(0, 1).BorderBottom(true), // Header: lipgloss.NewStyle().Bold(true).Padding(0, 1).BorderBottom(true),
Cell: lipgloss.NewStyle().Padding(0, 1, 0, 0), Cell: lipgloss.NewStyle().Padding(0, 1, 0, 0),
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1, 0, 0).Underline(true), 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), Selected: lipgloss.NewStyle().Bold(true).Reverse(true),
} }
formTheme := huh.ThemeBase() formTheme := huh.ThemeBase()
@ -104,9 +108,8 @@ func parseColors(config map[string]string) *Styles {
for key, value := range config { for key, value := range config {
if strings.HasPrefix(key, "color.") { if strings.HasPrefix(key, "color.") {
if value != "" { _, colorValue, _ := strings.Cut(key, ".")
color := strings.Split(key, ".")[1] switch colorValue {
switch color {
case "active": case "active":
styles.Active = parseColorString(value) styles.Active = parseColorString(value)
case "alternate": case "alternate":
@ -204,13 +207,16 @@ func parseColors(config map[string]string) *Styles {
} }
} }
} }
}
return &styles return &styles
} }
func parseColorString(color string) lipgloss.Style { func parseColorString(color string) lipgloss.Style {
style := lipgloss.NewStyle() style := lipgloss.NewStyle()
if color == "" {
return style
}
if strings.Contains(color, "on") { if strings.Contains(color, "on") {
fgbg := strings.Split(color, "on") fgbg := strings.Split(color, "on")
fg := strings.TrimSpace(fgbg[0]) fg := strings.TrimSpace(fgbg[0])
@ -230,12 +236,7 @@ func parseColorString(color string) lipgloss.Style {
func parseColor(color string) lipgloss.Color { func parseColor(color string) lipgloss.Color {
if strings.HasPrefix(color, "rgb") { if strings.HasPrefix(color, "rgb") {
rgb, err := parseRGBString(strings.TrimPrefix(color, "rgb")) return lipgloss.Color(convertRgbToAnsi(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") { if strings.HasPrefix(color, "color") {
return lipgloss.Color(strings.TrimPrefix(color, "color")) return lipgloss.Color(strings.TrimPrefix(color, "color"))
@ -256,36 +257,33 @@ func parseColor(color string) lipgloss.Color {
return lipgloss.Color("0") return lipgloss.Color("0")
} }
type RGB struct { func convertRgbToAnsi(rgbString string) string {
r int
g int
b int
}
func parseRGBString(rgbString string) (RGB, error) {
var err error var err error
rgb := RGB{}
if len(rgbString) != 3 { if len(rgbString) != 3 {
return rgb, errors.New("invalid RGB format") slog.Error("Invalid RGB color format")
return ""
} }
rgb.r, err = strconv.Atoi(string(rgbString[0])) r, err := strconv.Atoi(string(rgbString[0]))
if err != nil { if err != nil {
return rgb, errors.New("invalid value for R") slog.Error("Invalid value for R")
return ""
} }
rgb.g, err = strconv.Atoi(string(rgbString[1])) g, err := strconv.Atoi(string(rgbString[1]))
if err != nil { if err != nil {
return rgb, errors.New("invalid value for G") slog.Error("Invalid value for G")
return ""
} }
rgb.b, err = strconv.Atoi(string(rgbString[2])) b, err := strconv.Atoi(string(rgbString[2]))
if err != nil { if err != nil {
return rgb, errors.New("invalid value for B") slog.Error("Invalid value for B")
return ""
} }
return rgb, nil return strconv.Itoa(16 + (36 * r) + (6 * g) + b)
} }
var colorStrings = map[string]int{ var colorStrings = map[string]int{
@ -306,79 +304,3 @@ var colorStrings = map[string]int{
"bright cyan": 14, "bright cyan": 14,
"bright white": 15, "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)
}

View File

@ -2,6 +2,7 @@ package table
import ( import (
"strings" "strings"
"time"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
@ -23,7 +24,7 @@ type Model struct {
rowStyles []lipgloss.Style rowStyles []lipgloss.Style
cursor int cursor int
focus bool focus bool
styles Styles styles common.TableStyle
styleFunc StyleFunc styleFunc StyleFunc
viewport viewport.Model viewport viewport.Model
@ -108,25 +109,8 @@ func DefaultKeyMap() KeyMap {
} }
} }
// Styles contains style definitions for this list component. By default, these
// values are generated by DefaultStyles.
type Styles struct {
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
}
// DefaultStyles returns a set of default style definitions for this table.
func DefaultStyles() Styles {
return Styles{
Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
Cell: lipgloss.NewStyle().Padding(0, 1),
}
}
// SetStyles sets the table styles. // SetStyles sets the table styles.
func (m *Model) SetStyles(s Styles) { func (m *Model) SetStyles(s common.TableStyle) {
m.styles = s m.styles = s
m.UpdateViewport() m.UpdateViewport()
} }
@ -139,11 +123,11 @@ type Option func(*Model)
// New creates a new model for the table widget. // New creates a new model for the table widget.
func New(com *common.Common, opts ...Option) Model { func New(com *common.Common, opts ...Option) Model {
m := Model{ m := Model{
common: com,
cursor: 0, cursor: 0,
viewport: viewport.New(0, 20), viewport: viewport.New(0, 20),
KeyMap: DefaultKeyMap(), KeyMap: DefaultKeyMap(),
styles: DefaultStyles(),
} }
for _, opt := range opts { for _, opt := range opts {
@ -158,9 +142,75 @@ func New(com *common.Common, opts ...Option) Model {
return m return m
} }
func (m *Model) parseRowStyles() // TODO: dynamically read rule.precedence.color
func (m *Model) parseRowStyles(rows taskwarrior.Tasks) []lipgloss.Style {
styles := make([]lipgloss.Style, len(rows))
if len(rows) == 0 {
return styles
}
for i, task := range rows {
if task.Status == "deleted" {
styles[i] = m.common.Styles.Deleted.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if task.Status == "completed" {
styles[i] = m.common.Styles.Completed.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if task.Status == "pending" && task.Start != "" {
styles[i] = m.common.Styles.Active.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
// TODO: implement keyword
// TODO: implement tag
// TODO: implement project
if !task.GetDate("due").IsZero() && task.GetDate("due").Before(time.Now()) {
styles[i] = m.common.Styles.Overdue.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if task.Scheduled != "" {
styles[i] = m.common.Styles.Scheduled.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if !task.GetDate("due").IsZero() && task.GetDate("due").Truncate(24*time.Hour).Equal(time.Now().Truncate(24*time.Hour)) {
styles[i] = m.common.Styles.DueToday.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if task.Due != "" {
styles[i] = m.common.Styles.Due.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if len(task.Depends) > 0 {
styles[i] = m.common.Styles.Blocked.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
// TODO implement blocking
if task.Recur != "" {
styles[i] = m.common.Styles.Recurring.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue
}
if len(task.Tags) > 0 {
styles[i] = m.common.Styles.Tagged.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
taskIteration:
for _, tag := range task.Tags {
if tag == "next" {
styles[i] = m.common.Styles.TagNext.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
break taskIteration
}
}
continue
}
// TODO implement uda
styles[i] = m.common.Styles.Base.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
}
return styles
}
func (m *Model) parseColumns(cols []Column) []Column { func (m *Model) parseColumns(cols []Column) []Column {
if len(cols) == 0 {
return cols
}
for i, col := range cols { for i, col := range cols {
for _, task := range m.rows { for _, task := range m.rows {
col.ContentWidth = max(col.ContentWidth, lipgloss.Width(task.GetString(col.Name))) col.ContentWidth = max(col.ContentWidth, lipgloss.Width(task.GetString(col.Name)))
@ -172,9 +222,8 @@ func (m *Model) parseColumns(cols []Column) []Column {
nonZeroWidths := 0 nonZeroWidths := 0
descIndex := -1 descIndex := -1
for i, col := range cols { for i, col := range cols {
if col.ContentWidth > 0 {
col.Width = max(col.ContentWidth, lipgloss.Width(col.Title)) col.Width = max(col.ContentWidth, lipgloss.Width(col.Title))
if col.Width > 0 {
nonZeroWidths++ nonZeroWidths++
} }
@ -251,7 +300,7 @@ func WithFocused(f bool) Option {
} }
// WithStyles sets the table styles. // WithStyles sets the table styles.
func WithStyles(s Styles) Option { func WithStyles(s common.TableStyle) Option {
return func(m *Model) { return func(m *Model) {
m.styles = s m.styles = s
} }
@ -497,13 +546,16 @@ func (m *Model) renderRow(r int) string {
continue continue
} }
var cellStyle lipgloss.Style var cellStyle lipgloss.Style
if m.styleFunc != nil { // if m.styleFunc != nil {
cellStyle = m.styleFunc(r, i, m.rows[r].GetString(col.Name)) // cellStyle = m.styleFunc(r, i, m.rows[r].GetString(col.Name))
// if r == m.cursor {
// cellStyle.Inherit(m.styles.Selected)
// }
// } else {
cellStyle = m.rowStyles[r]
// }
if r == m.cursor { if r == m.cursor {
cellStyle.Inherit(m.styles.Selected) cellStyle = cellStyle.Inherit(m.styles.Selected)
}
} else {
cellStyle = m.rowStyle[r]
} }
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)

View File

@ -96,7 +96,7 @@ func (p *ContextPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (p *ContextPickerPage) View() string { func (p *ContextPickerPage) View() string {
return p.common.Styles.Main.Render(p.form.View()) return p.common.Styles.Base.Render(p.form.View())
} }
func (p *ContextPickerPage) updateContextCmd() tea.Msg { func (p *ContextPickerPage) updateContextCmd() tea.Msg {

View File

@ -92,7 +92,7 @@ func (p *ProjectPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (p *ProjectPickerPage) View() string { func (p *ProjectPickerPage) View() string {
return p.common.Styles.Main.Render(p.form.View()) return p.common.Styles.Base.Render(p.form.View())
} }
func (p *ProjectPickerPage) updateProjectCmd() tea.Msg { func (p *ProjectPickerPage) updateProjectCmd() tea.Msg {

View File

@ -2,7 +2,6 @@
package pages package pages
import ( import (
"log/slog"
"tasksquire/common" "tasksquire/common"
"tasksquire/components/table" "tasksquire/components/table"
"tasksquire/taskwarrior" "tasksquire/taskwarrior"
@ -34,15 +33,15 @@ func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage {
activeReport: report, activeReport: report,
activeContext: com.TW.GetActiveContext(), activeContext: com.TW.GetActiveContext(),
activeProject: "", activeProject: "",
taskTable: table.New(com),
} }
} }
func (p *ReportPage) SetSize(width int, height int) { func (p *ReportPage) SetSize(width int, height int) {
p.common.SetSize(width, height) p.common.SetSize(width, height)
slog.Info("FramSize", "vert", p.common.Styles.Main.GetVerticalFrameSize(), "horz", p.common.Styles.Main.GetHorizontalFrameSize())
p.taskTable.SetWidth(width - p.common.Styles.Main.GetVerticalFrameSize()) p.taskTable.SetWidth(width - p.common.Styles.Base.GetVerticalFrameSize())
p.taskTable.SetHeight(height - p.common.Styles.Main.GetHorizontalFrameSize()) p.taskTable.SetHeight(height - p.common.Styles.Base.GetHorizontalFrameSize())
} }
func (p *ReportPage) Init() tea.Cmd { func (p *ReportPage) Init() tea.Cmd {
@ -130,9 +129,9 @@ func (p *ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p *ReportPage) View() string { func (p *ReportPage) View() string {
// return p.common.Styles.Main.Render(p.taskTable.View()) + "\n" // return p.common.Styles.Main.Render(p.taskTable.View()) + "\n"
if p.tasks == nil || len(p.tasks) == 0 { if p.tasks == nil || len(p.tasks) == 0 {
return p.common.Styles.Main.Render("No tasks found") return p.common.Styles.Base.Render("No tasks found")
} }
return p.common.Styles.Main.Render(p.taskTable.View()) return p.taskTable.View()
} }
func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
@ -151,11 +150,12 @@ func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) {
} }
p.taskTable = table.New( p.taskTable = table.New(
p.common,
table.WithReport(p.activeReport), table.WithReport(p.activeReport),
table.WithTasks(tasks), table.WithTasks(tasks),
table.WithFocused(true), table.WithFocused(true),
table.WithWidth(p.common.Width()-p.common.Styles.Main.GetVerticalFrameSize()), table.WithWidth(p.common.Width()-p.common.Styles.Base.GetVerticalFrameSize()),
table.WithHeight(p.common.Height()-p.common.Styles.Main.GetHorizontalFrameSize()-10), table.WithHeight(p.common.Height()-p.common.Styles.Base.GetHorizontalFrameSize()-1),
table.WithStyles(p.common.Styles.TableStyle), table.WithStyles(p.common.Styles.TableStyle),
) )

View File

@ -93,7 +93,7 @@ func (p *ReportPickerPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (p *ReportPickerPage) View() string { func (p *ReportPickerPage) View() string {
return p.common.Styles.Main.Render(p.form.View()) return p.common.Styles.Base.Render(p.form.View())
} }
func (p *ReportPickerPage) updateReportCmd() tea.Msg { func (p *ReportPickerPage) updateReportCmd() tea.Msg {

View File

@ -314,7 +314,7 @@ func (s *StatusLine) View() string {
var mode string var mode string
switch s.mode { switch s.mode {
case ModeNormal: case ModeNormal:
mode = s.common.Styles.Main.Render("NORMAL") mode = s.common.Styles.Base.Render("NORMAL")
case ModeInsert: case ModeInsert:
mode = s.common.Styles.Active.Inline(true).Render("INSERT") mode = s.common.Styles.Active.Inline(true).Render("INSERT")
} }

View File

@ -9,6 +9,10 @@ import (
"time" "time"
) )
const (
dtformat = "20060102T150405Z"
)
type Annotation struct { type Annotation struct {
Entry string `json:"entry,omitempty"` Entry string `json:"entry,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
@ -184,6 +188,15 @@ func (t *Task) GetString(fieldWFormat string) string {
} }
} }
func (t *Task) GetDate(dateString string) time.Time {
dt, err := time.Parse(dtformat, dateString)
if err != nil {
slog.Error("Failed to parse time:", err)
return time.Time{}
}
return dt
}
type Tasks []*Task type Tasks []*Task
type Context struct { type Context struct {
@ -212,7 +225,6 @@ func formatDate(date string, format string) string {
return "" return ""
} }
dtformat := "20060102T150405Z"
dt, err := time.Parse(dtformat, date) dt, err := time.Parse(dtformat, date)
if err != nil { if err != nil {
slog.Error("Failed to parse time:", err) slog.Error("Failed to parse time:", err)