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 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 := resolveDisplayID(task, ws) if displayID == 0 { displayID = i + 1 } 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 := resolveDisplayID(task, ws) if displayID == 0 { displayID = i + 1 } 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)}) } if len(task.Annotations) > 0 { t.AppendSeparator() for i, ann := range task.Annotations { label := "" if i == 0 { label = "Annotations" } ts := time.Unix(ann.Timestamp, 0).Format("2006-01-02 15:04") t.AppendRow(table.Row{label, fmt.Sprintf("%s %s", ts, ann.Text)}) } } // Recent changes from change_log (last 5) if entries, err := GetTaskHistory(task.UUID); err == nil && len(entries) > 0 { t.AppendSeparator() // Show last 5 entries start := 0 if len(entries) > 5 { start = len(entries) - 5 } historyStr := FormatTaskHistory(entries[start:]) lines := strings.Split(strings.TrimSpace(historyStr), "\n") for i, line := range lines { label := "" if i == 0 { label = "History" } t.AppendRow(table.Row{label, line}) } } 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() } 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 { 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 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 }