summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-23 06:47:31 +0300
committerPaul Buetow <paul@buetow.org>2026-06-23 06:47:31 +0300
commit811a4f70fcaa6d806feb03b7b69fb6cb18d19142 (patch)
tree443ff2e5c08c11667f592d8c902d988178dce5db
parentf75924987a1685055d4eb4517a0f2443c6f7d6be (diff)
Refactor due and age helpers for wq0
-rw-r--r--internal/ui/helpers.go24
-rw-r--r--internal/ui/helpers_test.go90
-rw-r--r--internal/ui/table.go26
-rw-r--r--internal/ui/ultra.go10
4 files changed, 122 insertions, 28 deletions
diff --git a/internal/ui/helpers.go b/internal/ui/helpers.go
index 0358e86..bebc937 100644
--- a/internal/ui/helpers.go
+++ b/internal/ui/helpers.go
@@ -39,17 +39,22 @@ func daysUntil(t time.Time) int {
// formatDueText returns a human-readable due date string
func formatDueText(dueStr string) string {
+ text, _, _ := dueTextAndDays(dueStr)
+ return text
+}
+
+func dueTextAndDays(dueStr string) (string, int, bool) {
if dueStr == "" {
- return ""
+ return "", 0, false
}
ts, err := parseTaskDate(dueStr)
if err != nil {
- return dueStr
+ return dueStr, 0, false
}
days := daysUntil(ts)
- return formatDueTextFromDays(days)
+ return formatDueTextFromDays(days), days, true
}
func formatDueTextFromDays(days int) string {
@@ -65,6 +70,19 @@ func formatDueTextFromDays(days int) string {
}
}
+func taskAgeText(entry string) (string, bool) {
+ if entry == "" {
+ return "", false
+ }
+
+ ts, err := parseTaskDate(entry)
+ if err != nil {
+ return "", false
+ }
+
+ return fmt.Sprintf("%dd", int(time.Since(ts).Hours()/24)), true
+}
+
// compileAndCacheRegex compiles a regex and adds it to the cache
func compileAndCacheRegex(pattern string) (*regexp.Regexp, error) {
re, err := regexp.Compile(pattern)
diff --git a/internal/ui/helpers_test.go b/internal/ui/helpers_test.go
index f4afc11..7245145 100644
--- a/internal/ui/helpers_test.go
+++ b/internal/ui/helpers_test.go
@@ -135,6 +135,96 @@ func TestFormatDueText(t *testing.T) {
}
}
+func TestDueTextAndDays(t *testing.T) {
+ loc, now := useSofiaLocalTime(t)
+ tests := []struct {
+ name string
+ input string
+ wantText string
+ wantDays int
+ wantOK bool
+ }{
+ {
+ name: "empty",
+ input: "",
+ wantText: "",
+ },
+ {
+ name: "invalid",
+ input: "not-a-task-date",
+ wantText: "not-a-task-date",
+ },
+ {
+ name: "today",
+ input: taskwarriorDateOnlyDue(now, loc, 0),
+ wantText: "today",
+ wantOK: true,
+ },
+ {
+ name: "future day count",
+ input: taskwarriorDateOnlyDue(now, loc, 4),
+ wantText: "4d",
+ wantDays: 4,
+ wantOK: true,
+ },
+ {
+ name: "past day count",
+ input: taskwarriorDateOnlyDue(now, loc, -2),
+ wantText: "-2d",
+ wantDays: -2,
+ wantOK: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotText, gotDays, gotOK := dueTextAndDays(tt.input)
+ skipIfLocalDateChanged(t, loc, now)
+ if gotText != tt.wantText || gotDays != tt.wantDays || gotOK != tt.wantOK {
+ t.Fatalf("dueTextAndDays() = %q, %d, %t; want %q, %d, %t", gotText, gotDays, gotOK, tt.wantText, tt.wantDays, tt.wantOK)
+ }
+ })
+ }
+}
+
+func TestTaskAgeText(t *testing.T) {
+ tests := []struct {
+ name string
+ entry string
+ wantText string
+ wantOK bool
+ }{
+ {
+ name: "empty",
+ },
+ {
+ name: "invalid",
+ entry: "not-a-task-date",
+ },
+ {
+ name: "less than one day",
+ entry: time.Now().Add(-23 * time.Hour).UTC().Format(task.DateFormat),
+ wantText: "0d",
+ wantOK: true,
+ },
+ {
+ name: "more than one day",
+ entry: time.Now().Add(-25 * time.Hour).UTC().Format(task.DateFormat),
+ wantText: "1d",
+ wantOK: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotText, gotOK := taskAgeText(tt.entry)
+ if gotText != tt.wantText || gotOK != tt.wantOK {
+ t.Fatalf("taskAgeText() = %q, %t; want %q, %t", gotText, gotOK, tt.wantText, tt.wantOK)
+ }
+ })
+ }
+}
+
func TestSearchRegexCacheConcurrentAccess(t *testing.T) {
searchRegexMu.Lock()
searchRegexCache = make(map[string]*regexp.Regexp)
diff --git a/internal/ui/table.go b/internal/ui/table.go
index d4c9d46..be775fb 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -1056,16 +1056,14 @@ func (m *Model) topStatusLine() string {
// are returned as "today" or "tomorrow" respectively. Past due dates are
// highlighted in red.
func (m *Model) formatDue(s string, width int) string {
- if s == "" {
+ val, days, ok := dueTextAndDays(s)
+ if val == "" {
return ""
}
- ts, err := parseTaskDate(s)
- if err != nil {
- return s
+ if !ok {
+ return val
}
- days := daysUntil(ts)
- val := formatDueTextFromDays(days)
style := lipgloss.NewStyle().Width(width)
if days < 0 {
style = style.Background(lipgloss.Color(m.theme.OverdueBG))
@@ -1159,11 +1157,7 @@ func (m *Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.St
rowStyle = rowStyle.Reverse(true)
}
- age := ""
- if ts, err := time.Parse(task.DateFormat, t.Entry); err == nil {
- days := int(time.Since(ts).Hours() / 24)
- age = fmt.Sprintf("%dd", days)
- }
+ age, _ := taskAgeText(t.Entry)
tags := strings.Join(t.Tags, " ")
urg := fmt.Sprintf("%.1f", t.Urgency)
@@ -1228,10 +1222,7 @@ func (m *Model) expandedCellView() string {
case 1:
val = strconv.Itoa(t.ID)
case 2:
- if ts, err := time.Parse(task.DateFormat, t.Entry); err == nil {
- days := int(time.Since(ts).Hours() / 24)
- val = fmt.Sprintf("%dd", days)
- }
+ val, _ = taskAgeText(t.Entry)
case 3:
val = ansi.Strip(m.formatDue(t.Due, m.dueWidth))
case 4:
@@ -1318,10 +1309,7 @@ func (m *Model) computeColumnWidths() {
if l := len(strconv.Itoa(t.ID)); l > maxID {
maxID = l
}
- age := ""
- if ts, err := time.Parse(task.DateFormat, t.Entry); err == nil {
- age = fmt.Sprintf("%dd", int(time.Since(ts).Hours()/24))
- }
+ age, _ := taskAgeText(t.Entry)
if l := len(age); l > maxAge {
maxAge = l
}
diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go
index 999ad73..2c06afb 100644
--- a/internal/ui/ultra.go
+++ b/internal/ui/ultra.go
@@ -4,7 +4,6 @@ import (
"fmt"
"regexp"
"strings"
- "time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
@@ -930,14 +929,13 @@ func ultraBodyWidth(width int) int {
}
func ultraTaskAge(entry string) string {
+ if age, ok := taskAgeText(entry); ok {
+ return age
+ }
if entry == "" {
return "-"
}
- ts, err := time.Parse(task.DateFormat, entry)
- if err != nil {
- return entry
- }
- return fmt.Sprintf("%dd", int(time.Since(ts).Hours()/24))
+ return entry
}
func ultraOrDash(text string) string {