From a30c703efa9480885bdaf6c74aaf8b1950fc18cf Mon Sep 17 00:00:00 2001 From: Elias Kohout Date: Thu, 2 Apr 2026 04:54:15 +0200 Subject: [PATCH] feat: add colored due date indicators to list and show views - List view: show relative due date (+12d, -3d, today) after namespace - Show view: colorize full due date based on urgency - Color scheme: red (overdue), yellow (due within 3 days), green (4+ days) Co-Authored-By: Claude Haiku 4.5 --- src/cmd/output.go | 58 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/cmd/output.go b/src/cmd/output.go index 32ddcdc..c5b49d5 100644 --- a/src/cmd/output.go +++ b/src/cmd/output.go @@ -7,8 +7,10 @@ import ( "encoding/json" "fmt" "io" + "math" "sort" "strings" + "time" "github.com/fatih/color" ) @@ -89,13 +91,14 @@ func PrintNodes(w io.Writer, svc service.NodeService, nodes []*models.Node, json } ns_rel_node_titles = append(ns_rel_node_titles, ns_rel_node.Title) } - fmt.Fprintf(w, " %s %s %s %s %s %s", + fmt.Fprintf(w, " %s %s %s %s %s %s %s", cDim.Sprint(n.ID), render(prioRM, n.GetProperty("prio"), true), render(statusRM, n.GetProperty("status"), true), render(typeRM, n.GetProperty("type"), true), cTitle.Sprint(truncate(n.Title, 80)), cDim.Sprint("["+strings.Join(ns_rel_node_titles, ",")+"]"), + dueDateShort(n.DueDate), ) tags := n.GetDisplayTags() if len(tags) > 0 { @@ -117,7 +120,7 @@ func PrintNode(w io.Writer, svc service.NodeService, n *models.Node, jsonOut boo fmt.Fprintf(w, " Status: %s\n", render(statusRM, n.GetProperty("status"), false)) fmt.Fprintf(w, " Priority: %s\n", render(prioRM, n.GetProperty("prio"), false)) if n.DueDate != nil { - fmt.Fprintf(w, " Due: %s %s\n", iconCalendar, n.DueDate.Format("2006-01-02")) + fmt.Fprintf(w, " Due: %s\n", dueDateLong(n.DueDate)) } fmt.Fprintf(w, " Created: %s\n", cDim.Sprint(n.CreatedAt)) fmt.Fprintf(w, " Updated: %s\n", cDim.Sprint(n.UpdatedAt)) @@ -202,6 +205,57 @@ func render(rm RenderMap, key string, short bool) string { return v.c.Sprint(v.l) } +// dueDateShort returns a "+12d" / "-3d" style string colored by urgency. +func dueDateShort(d *models.Date) string { + if d == nil { + return "" + } + now := time.Now().UTC().Truncate(24 * time.Hour) + due := d.UTC().Truncate(24 * time.Hour) + days := int(math.Round(due.Sub(now).Hours() / 24)) + + var label string + if days == 0 { + label = "today" + } else if days > 0 { + label = fmt.Sprintf("+%dd", days) + } else { + label = fmt.Sprintf("%dd", days) + } + + var c *color.Color + switch { + case days < 0: + c = cBad + case days <= 3: + c = cWarn + default: + c = cGood + } + return c.Sprint(label) +} + +// dueDateLong returns the full date string colored by urgency. +func dueDateLong(d *models.Date) string { + if d == nil { + return "" + } + now := time.Now().UTC().Truncate(24 * time.Hour) + due := d.UTC().Truncate(24 * time.Hour) + days := int(math.Round(due.Sub(now).Hours() / 24)) + + var c *color.Color + switch { + case days < 0: + c = cBad + case days <= 3: + c = cWarn + default: + c = cGood + } + return c.Sprintf("%s %s", iconCalendar, d.Format("2006-01-02")) +} + func truncate(s string, max int) string { if len(s) <= max { return s