From 811a4f70fcaa6d806feb03b7b69fb6cb18d19142 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 23 Jun 2026 06:47:31 +0300 Subject: Refactor due and age helpers for wq0 --- internal/ui/helpers.go | 24 ++++++++++-- internal/ui/helpers_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++ internal/ui/table.go | 26 ++++--------- internal/ui/ultra.go | 10 ++--- 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 { -- cgit v1.2.3