b02c40f716
Add relative date formatting (today, tomorrow, in 3d, etc.) for list and detail views. Add structured feedback helpers for add/complete/delete operations showing display IDs and parsed modifiers. Change Complete() to return spawned recurring instance so callers can display recurrence info. Add AppendTask to working set for immediate display ID assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
396 lines
9.2 KiB
Go
396 lines
9.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"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
|
|
func FormatTaskList(tasks []*Task, ws *WorkingSet) string {
|
|
return FormatTaskListWithFormat(tasks, ws, "table")
|
|
}
|
|
|
|
// FormatTaskListWithFormat formats a list of tasks with specified format
|
|
func FormatTaskListWithFormat(tasks []*Task, ws *WorkingSet, format string) string {
|
|
if len(tasks) == 0 {
|
|
return "No tasks found."
|
|
}
|
|
|
|
// Get urgency coefficients
|
|
cfg, _ := GetConfig()
|
|
coeffs := BuildUrgencyCoefficients(cfg)
|
|
|
|
// Minimal format: just ID and description, color-coded by urgency
|
|
if format == "minimal" {
|
|
result := ""
|
|
for i, task := range tasks {
|
|
displayID := i + 1
|
|
if ws != nil {
|
|
// Use working set display ID if available
|
|
for id, uuid := range ws.byID {
|
|
if uuid == task.UUID {
|
|
displayID = id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
urgency := task.CalculateUrgency(coeffs)
|
|
urgencyColor := getUrgencyColor(urgency)
|
|
result += urgencyColor.Sprintf("%3d %s\n", displayID, task.Description)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Table format (default)
|
|
t := table.NewWriter()
|
|
|
|
// Configure style
|
|
t.SetStyle(table.StyleLight)
|
|
t.Style().Options.SeparateRows = false
|
|
t.Style().Options.DrawBorder = false
|
|
|
|
// 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: 4, WidthMax: 4, Align: text.AlignRight}, // Urg
|
|
{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", "Urg", "Project", "Description", "Due", "Tags"})
|
|
|
|
// Add rows
|
|
for i, task := range tasks {
|
|
displayID := i + 1
|
|
if ws != nil {
|
|
// Use working set display ID if available
|
|
for id, uuid := range ws.byID {
|
|
if uuid == task.UUID {
|
|
displayID = id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
urgency := task.CalculateUrgency(coeffs)
|
|
t.AppendRow(table.Row{
|
|
displayID,
|
|
formatStatus(task.Status),
|
|
formatUrgency(urgency),
|
|
formatProject(task.Project),
|
|
task.Description,
|
|
formatDue(task.Due),
|
|
formatTags(task.Tags),
|
|
})
|
|
}
|
|
|
|
return t.Render()
|
|
}
|
|
|
|
// FormatTaskDetail formats detailed task information
|
|
func FormatTaskDetail(task *Task) string {
|
|
t := table.NewWriter()
|
|
|
|
// Configure style
|
|
t.SetStyle(table.StyleLight)
|
|
t.Style().Options.SeparateRows = false
|
|
t.Style().Options.DrawBorder = false
|
|
t.Style().Options.SeparateHeader = true
|
|
|
|
// Configure columns - field name and value
|
|
t.SetColumnConfigs([]table.ColumnConfig{
|
|
{Number: 1, WidthMin: 12, WidthMax: 12, Align: text.AlignLeft},
|
|
{Number: 2, Align: text.AlignLeft},
|
|
})
|
|
|
|
// Add title as header
|
|
t.SetTitle(color.New(color.Bold).Sprint("Task Details"))
|
|
|
|
// Calculate urgency
|
|
cfg, _ := GetConfig()
|
|
coeffs := BuildUrgencyCoefficients(cfg)
|
|
urgency := task.CalculateUrgency(coeffs)
|
|
|
|
// 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{"Urgency", formatUrgency(urgency)})
|
|
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 {
|
|
t.AppendRow(table.Row{"Started", formatTime(*task.Start)})
|
|
}
|
|
|
|
if task.End != nil {
|
|
t.AppendRow(table.Row{"Ended", formatTime(*task.End)})
|
|
}
|
|
|
|
if task.Due != nil {
|
|
dueStr := FormatDateWithRelative(*task.Due)
|
|
if task.Due.Before(timeNow()) {
|
|
dueStr = color.RedString(dueStr)
|
|
}
|
|
t.AppendRow(table.Row{"Due", dueStr})
|
|
}
|
|
|
|
if task.Scheduled != nil {
|
|
t.AppendRow(table.Row{"Scheduled", FormatDateWithRelative(*task.Scheduled)})
|
|
}
|
|
|
|
if task.Wait != nil {
|
|
t.AppendRow(table.Row{"Wait", FormatDateWithRelative(*task.Wait)})
|
|
}
|
|
|
|
if task.Until != nil {
|
|
t.AppendRow(table.Row{"Until", FormatDateWithRelative(*task.Until)})
|
|
}
|
|
|
|
if task.RecurrenceDuration != nil {
|
|
t.AppendRow(table.Row{"Recurrence", FormatRecurrenceDuration(*task.RecurrenceDuration)})
|
|
}
|
|
|
|
if task.ParentUUID != nil {
|
|
t.AppendRow(table.Row{"Parent", fmt.Sprintf("%s (recurring instance)", *task.ParentUUID)})
|
|
}
|
|
|
|
if len(task.Tags) > 0 {
|
|
t.AppendSeparator()
|
|
t.AppendRow(table.Row{"Tags", formatTags(task.Tags)})
|
|
}
|
|
|
|
return t.Render()
|
|
}
|
|
|
|
// FormatProjects formats project list with counts
|
|
func FormatProjects(projectCounts map[string]int) string {
|
|
if len(projectCounts) == 0 {
|
|
return "No projects found."
|
|
}
|
|
|
|
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 {
|
|
t.AppendRow(table.Row{project, count})
|
|
}
|
|
|
|
return t.Render()
|
|
}
|
|
|
|
// FormatTagCounts formats tag list with counts
|
|
func FormatTagCounts(tagCounts map[string]int) string {
|
|
if len(tagCounts) == 0 {
|
|
return "No tags found."
|
|
}
|
|
|
|
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 {
|
|
t.AppendRow(table.Row{tag, count})
|
|
}
|
|
|
|
return t.Render()
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func formatStatus(status Status) string {
|
|
switch status {
|
|
case StatusPending:
|
|
return color.YellowString("pending")
|
|
case StatusCompleted:
|
|
return color.GreenString("done")
|
|
case StatusDeleted:
|
|
return color.RedString("deleted")
|
|
case StatusRecurring:
|
|
return color.BlueString("recurring")
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func formatPriority(priority Priority) string {
|
|
switch priority {
|
|
case PriorityHigh:
|
|
return color.RedString("H")
|
|
case PriorityMedium:
|
|
return color.YellowString("M")
|
|
case PriorityLow:
|
|
return color.CyanString("L")
|
|
case PriorityDefault:
|
|
return "D"
|
|
default:
|
|
return "D"
|
|
}
|
|
}
|
|
|
|
func formatUrgency(urgency float64) string {
|
|
formatted := fmt.Sprintf("%.1f", urgency)
|
|
|
|
// 4-tier color coding
|
|
if urgency >= 10.0 {
|
|
// Critical urgency - bright red
|
|
return color.New(color.FgHiRed, color.Bold).Sprint(formatted)
|
|
} else if urgency >= 5.0 {
|
|
// High urgency - red
|
|
return color.RedString(formatted)
|
|
} else if urgency >= 2.0 {
|
|
// Medium urgency - yellow
|
|
return color.YellowString(formatted)
|
|
} else {
|
|
// Low urgency - cyan
|
|
return color.CyanString(formatted)
|
|
}
|
|
}
|
|
|
|
func getUrgencyColor(urgency float64) *color.Color {
|
|
// Returns color for minimal format
|
|
if urgency >= 10.0 {
|
|
return color.New(color.FgHiRed, color.Bold)
|
|
} else if urgency >= 5.0 {
|
|
return color.New(color.FgRed)
|
|
} else if urgency >= 2.0 {
|
|
return color.New(color.FgYellow)
|
|
} else {
|
|
return color.New(color.FgCyan)
|
|
}
|
|
}
|
|
|
|
func formatProject(project *string) string {
|
|
if project == nil {
|
|
return "-"
|
|
}
|
|
return *project
|
|
}
|
|
|
|
func formatDue(due *time.Time) string {
|
|
if due == nil {
|
|
return ""
|
|
}
|
|
|
|
rel := FormatRelativeDate(*due)
|
|
|
|
now := timeNow()
|
|
if due.Before(now) {
|
|
return color.RedString(rel)
|
|
}
|
|
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
tomorrow := today.Add(24 * time.Hour)
|
|
if due.Before(tomorrow) {
|
|
return color.YellowString(rel)
|
|
}
|
|
|
|
return rel
|
|
}
|
|
|
|
func formatTimeWithColor(t time.Time) string {
|
|
now := time.Now()
|
|
if t.Before(now) {
|
|
return color.RedString(t.Format("2006-01-02 15:04"))
|
|
}
|
|
return t.Format("2006-01-02 15:04")
|
|
}
|
|
|
|
func formatTime(t time.Time) string {
|
|
return t.Format("2006-01-02 15:04")
|
|
}
|
|
|
|
func formatTags(tags []string) string {
|
|
if len(tags) == 0 {
|
|
return ""
|
|
}
|
|
|
|
formatted := make([]string, len(tags))
|
|
for i, tag := range tags {
|
|
formatted[i] = color.CyanString("+" + tag)
|
|
}
|
|
|
|
// Join tags with spaces
|
|
result := formatted[0]
|
|
for i := 1; i < len(formatted); i++ {
|
|
result += " " + formatted[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetProjectCounts returns a map of project names to task counts
|
|
func GetProjectCounts() (map[string]int, error) {
|
|
tasks, err := GetTasks(DefaultFilter())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
counts := make(map[string]int)
|
|
for _, task := range tasks {
|
|
if task.Project != nil {
|
|
counts[*task.Project]++
|
|
}
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
// GetTagCounts returns a map of tags to task counts
|
|
func GetTagCounts() (map[string]int, error) {
|
|
tasks, err := GetTasks(DefaultFilter())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
counts := make(map[string]int)
|
|
for _, task := range tasks {
|
|
for _, tag := range task.Tags {
|
|
counts[tag]++
|
|
}
|
|
}
|
|
|
|
return counts, nil
|
|
}
|