Migrate table formatting to go-pretty for proper UTF-8 and ANSI handling
Replace manual string-based table formatting with jedib0t/go-pretty/v6/table library to fix alignment issues with Norwegian characters (æøå) and ANSI color codes. Changes: - Migrate FormatTaskList() to use go-pretty table - Migrate FormatTaskDetail() to use clean table format - Migrate FormatProjects() and FormatTagCounts() for consistency - Remove truncate() function (no longer needed) - Configure StyleLight with Unicode box-drawing characters - Set proper column widths and alignment Fixes: - Priority column now center-aligned under 'Pri' - Norwegian characters (æøå) display and align correctly - Tags column properly aligned - Description field truncates at 40 chars with proper UTF-8 handling - All ANSI color codes handled automatically - Consistent formatting across all table views All existing color functions work unchanged. UTF-8 and ANSI codes are handled automatically by go-pretty's width calculation.
This commit is contained in:
@@ -2,10 +2,11 @@ package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// FormatTaskList formats a list of tasks for display
|
||||
@@ -14,14 +15,29 @@ func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
||||
return "No tasks found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
t := table.NewWriter()
|
||||
|
||||
// Header
|
||||
sb.WriteString(fmt.Sprintf("%-3s %-8s %-3s %-12s %-40s %-12s %s\n",
|
||||
"ID", "Status", "Pri", "Project", "Description", "Due", "Tags"))
|
||||
sb.WriteString(strings.Repeat("-", 100) + "\n")
|
||||
// Configure style
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = false
|
||||
t.Style().Options.DrawBorder = false
|
||||
|
||||
// Tasks
|
||||
// Configure columns with proper widths
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, WidthMin: 3, WidthMax: 3, Align: text.AlignRight}, // ID
|
||||
{Number: 2, WidthMin: 8, WidthMax: 8, Align: text.AlignLeft}, // Status
|
||||
{Number: 3, WidthMin: 3, WidthMax: 3, Align: text.AlignCenter}, // Pri
|
||||
{Number: 4, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft}, // Project
|
||||
{Number: 5, WidthMin: 40, WidthMax: 40, Align: text.AlignLeft, // Description
|
||||
WidthMaxEnforcer: text.Trim},
|
||||
{Number: 6, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft}, // Due
|
||||
{Number: 7, Align: text.AlignLeft}, // Tags (variable)
|
||||
})
|
||||
|
||||
// Add header
|
||||
t.AppendHeader(table.Row{"ID", "Status", "Pri", "Project", "Description", "Due", "Tags"})
|
||||
|
||||
// Add rows
|
||||
for i, task := range tasks {
|
||||
displayID := i + 1
|
||||
if ws != nil {
|
||||
@@ -34,73 +50,90 @@ func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
||||
}
|
||||
}
|
||||
|
||||
status := formatStatus(task.Status)
|
||||
priority := formatPriority(task.Priority)
|
||||
project := formatProject(task.Project)
|
||||
description := truncate(task.Description, 40)
|
||||
due := formatDue(task.Due)
|
||||
tags := formatTags(task.Tags)
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%-3d %-8s %-3s %-12s %-40s %-12s %s\n",
|
||||
displayID, status, priority, project, description, due, tags))
|
||||
t.AppendRow(table.Row{
|
||||
displayID,
|
||||
formatStatus(task.Status),
|
||||
formatPriority(task.Priority),
|
||||
formatProject(task.Project),
|
||||
task.Description,
|
||||
formatDue(task.Due),
|
||||
formatTags(task.Tags),
|
||||
})
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
// FormatTaskDetail formats detailed task information
|
||||
func FormatTaskDetail(task *Task) string {
|
||||
var sb strings.Builder
|
||||
t := table.NewWriter()
|
||||
|
||||
sb.WriteString(color.New(color.Bold).Sprint("Task Details") + "\n")
|
||||
sb.WriteString(strings.Repeat("=", 50) + "\n")
|
||||
// Configure style
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = false
|
||||
t.Style().Options.DrawBorder = false
|
||||
t.Style().Options.SeparateHeader = true
|
||||
|
||||
sb.WriteString(fmt.Sprintf("UUID: %s\n", task.UUID))
|
||||
sb.WriteString(fmt.Sprintf("Status: %s\n", formatStatus(task.Status)))
|
||||
sb.WriteString(fmt.Sprintf("Description: %s\n", task.Description))
|
||||
sb.WriteString(fmt.Sprintf("Priority: %s\n", formatPriority(task.Priority)))
|
||||
sb.WriteString(fmt.Sprintf("Project: %s\n", formatProject(task.Project)))
|
||||
// Configure columns - field name and value
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft},
|
||||
{Number: 2, Align: text.AlignLeft},
|
||||
})
|
||||
|
||||
sb.WriteString(fmt.Sprintf("\nCreated: %s\n", formatTime(task.Created)))
|
||||
sb.WriteString(fmt.Sprintf("Modified: %s\n", formatTime(task.Modified)))
|
||||
// Add title as header
|
||||
t.SetTitle(color.New(color.Bold).Sprint("Task Details"))
|
||||
|
||||
// Core fields
|
||||
t.AppendRow(table.Row{"UUID", task.UUID})
|
||||
t.AppendRow(table.Row{"Status", formatStatus(task.Status)})
|
||||
t.AppendRow(table.Row{"Description", task.Description})
|
||||
t.AppendRow(table.Row{"Priority", formatPriority(task.Priority)})
|
||||
t.AppendRow(table.Row{"Project", formatProject(task.Project)})
|
||||
|
||||
t.AppendSeparator()
|
||||
|
||||
// Timestamps
|
||||
t.AppendRow(table.Row{"Created", formatTime(task.Created)})
|
||||
t.AppendRow(table.Row{"Modified", formatTime(task.Modified)})
|
||||
|
||||
if task.Start != nil {
|
||||
sb.WriteString(fmt.Sprintf("Started: %s\n", formatTime(*task.Start)))
|
||||
t.AppendRow(table.Row{"Started", formatTime(*task.Start)})
|
||||
}
|
||||
|
||||
if task.End != nil {
|
||||
sb.WriteString(fmt.Sprintf("Ended: %s\n", formatTime(*task.End)))
|
||||
t.AppendRow(table.Row{"Ended", formatTime(*task.End)})
|
||||
}
|
||||
|
||||
if task.Due != nil {
|
||||
sb.WriteString(fmt.Sprintf("Due: %s\n", formatTimeWithColor(*task.Due)))
|
||||
t.AppendRow(table.Row{"Due", formatTimeWithColor(*task.Due)})
|
||||
}
|
||||
|
||||
if task.Scheduled != nil {
|
||||
sb.WriteString(fmt.Sprintf("Scheduled: %s\n", formatTime(*task.Scheduled)))
|
||||
t.AppendRow(table.Row{"Scheduled", formatTime(*task.Scheduled)})
|
||||
}
|
||||
|
||||
if task.Wait != nil {
|
||||
sb.WriteString(fmt.Sprintf("Wait: %s\n", formatTime(*task.Wait)))
|
||||
t.AppendRow(table.Row{"Wait", formatTime(*task.Wait)})
|
||||
}
|
||||
|
||||
if task.Until != nil {
|
||||
sb.WriteString(fmt.Sprintf("Until: %s\n", formatTime(*task.Until)))
|
||||
t.AppendRow(table.Row{"Until", formatTime(*task.Until)})
|
||||
}
|
||||
|
||||
if task.RecurrenceDuration != nil {
|
||||
sb.WriteString(fmt.Sprintf("Recurrence: %s\n", FormatRecurrenceDuration(*task.RecurrenceDuration)))
|
||||
t.AppendRow(table.Row{"Recurrence", FormatRecurrenceDuration(*task.RecurrenceDuration)})
|
||||
}
|
||||
|
||||
if task.ParentUUID != nil {
|
||||
sb.WriteString(fmt.Sprintf("Parent: %s (recurring instance)\n", *task.ParentUUID))
|
||||
t.AppendRow(table.Row{"Parent", fmt.Sprintf("%s (recurring instance)", *task.ParentUUID)})
|
||||
}
|
||||
|
||||
if len(task.Tags) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("\nTags: %s\n", formatTags(task.Tags)))
|
||||
t.AppendSeparator()
|
||||
t.AppendRow(table.Row{"Tags", formatTags(task.Tags)})
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
// FormatProjects formats project list with counts
|
||||
@@ -109,32 +142,58 @@ func FormatProjects(projectCounts map[string]int) string {
|
||||
return "No projects found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%-20s %s\n", "Project", "Count"))
|
||||
sb.WriteString(strings.Repeat("-", 30) + "\n")
|
||||
t := table.NewWriter()
|
||||
|
||||
// Configure style
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = false
|
||||
t.Style().Options.DrawBorder = false
|
||||
|
||||
// Configure columns
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, WidthMin: 20, WidthMax: 20, Align: text.AlignLeft},
|
||||
{Number: 2, Align: text.AlignRight},
|
||||
})
|
||||
|
||||
// Add header
|
||||
t.AppendHeader(table.Row{"Project", "Count"})
|
||||
|
||||
// Add rows
|
||||
for project, count := range projectCounts {
|
||||
sb.WriteString(fmt.Sprintf("%-20s %d\n", project, count))
|
||||
t.AppendRow(table.Row{project, count})
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
// FormatTags formats tag list with counts
|
||||
// FormatTagCounts formats tag list with counts
|
||||
func FormatTagCounts(tagCounts map[string]int) string {
|
||||
if len(tagCounts) == 0 {
|
||||
return "No tags found."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%-20s %s\n", "Tag", "Count"))
|
||||
sb.WriteString(strings.Repeat("-", 30) + "\n")
|
||||
t := table.NewWriter()
|
||||
|
||||
// Configure style
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = false
|
||||
t.Style().Options.DrawBorder = false
|
||||
|
||||
// Configure columns
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, WidthMin: 20, WidthMax: 20, Align: text.AlignLeft},
|
||||
{Number: 2, Align: text.AlignRight},
|
||||
})
|
||||
|
||||
// Add header
|
||||
t.AppendHeader(table.Row{"Tag", "Count"})
|
||||
|
||||
// Add rows
|
||||
for tag, count := range tagCounts {
|
||||
sb.WriteString(fmt.Sprintf("%-20s %d\n", tag, count))
|
||||
t.AppendRow(table.Row{tag, count})
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
@@ -214,14 +273,13 @@ func formatTags(tags []string) string {
|
||||
for i, tag := range tags {
|
||||
formatted[i] = color.CyanString("+" + tag)
|
||||
}
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
// Join tags with spaces
|
||||
result := formatted[0]
|
||||
for i := 1; i < len(formatted); i++ {
|
||||
result += " " + formatted[i]
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
return result
|
||||
}
|
||||
|
||||
// GetProjectCounts returns a map of project names to task counts
|
||||
|
||||
Reference in New Issue
Block a user