59861bc3bf
- Fix template task filtering bug: templates now hidden from all reports except 'template' and 'all' reports, even when using custom filters - Add support for status:template filter to explicitly show templates - Implement comprehensive report system with 12 predefined reports: * active - Started tasks * all - All tasks including templates * completed - Completed tasks * list - Pending tasks (default) * minimal - Pending tasks in minimal format * newest - Most recent pending tasks * oldest - Oldest pending tasks * overdue - Overdue tasks * ready - Tasks ready to work on * recurring - Pending recurring instances * template - Recurring template tasks * waiting - Hidden/waiting tasks - Replace list command with report-based architecture - Add configurable default_report option (defaults to 'list') - Add minimal display format (ID + description only) - Support flexible syntax: 'opal <report> [filters]' or 'opal [filters] <report>' - Add 'opal reports' command to list all available reports
343 lines
7.8 KiB
Go
343 lines
7.8 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."
|
|
}
|
|
|
|
// Minimal format: just ID and description
|
|
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
|
|
}
|
|
}
|
|
}
|
|
result += fmt.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: 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 {
|
|
// Use working set display ID if available
|
|
for id, uuid := range ws.byID {
|
|
if uuid == task.UUID {
|
|
displayID = id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
t.AppendRow(table.Row{
|
|
displayID,
|
|
formatStatus(task.Status),
|
|
formatPriority(task.Priority),
|
|
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"))
|
|
|
|
// 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 {
|
|
t.AppendRow(table.Row{"Started", formatTime(*task.Start)})
|
|
}
|
|
|
|
if task.End != nil {
|
|
t.AppendRow(table.Row{"Ended", formatTime(*task.End)})
|
|
}
|
|
|
|
if task.Due != nil {
|
|
t.AppendRow(table.Row{"Due", formatTimeWithColor(*task.Due)})
|
|
}
|
|
|
|
if task.Scheduled != nil {
|
|
t.AppendRow(table.Row{"Scheduled", formatTime(*task.Scheduled)})
|
|
}
|
|
|
|
if task.Wait != nil {
|
|
t.AppendRow(table.Row{"Wait", formatTime(*task.Wait)})
|
|
}
|
|
|
|
if task.Until != nil {
|
|
t.AppendRow(table.Row{"Until", formatTime(*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 formatProject(project *string) string {
|
|
if project == nil {
|
|
return "-"
|
|
}
|
|
return *project
|
|
}
|
|
|
|
func formatDue(due *time.Time) string {
|
|
if due == nil {
|
|
return ""
|
|
}
|
|
|
|
now := time.Now()
|
|
if due.Before(now) {
|
|
return color.RedString(due.Format("2006-01-02"))
|
|
}
|
|
|
|
if due.Before(now.Add(24 * time.Hour)) {
|
|
return color.YellowString(due.Format("2006-01-02"))
|
|
}
|
|
|
|
return due.Format("2006-01-02")
|
|
}
|
|
|
|
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
|
|
}
|