Skip to content

Commit e2e47f9

Browse files
committed
Add age/activity column to PR listing
Shows time since creation (priority sort) or last activity (date sort). Compact format: now, 3h, 5d, 2w, 3m, 1y — always max 3 chars. Header label switches between "age" and "act" with sort mode.
1 parent 62a6c0c commit e2e47f9

4 files changed

Lines changed: 94 additions & 16 deletions

File tree

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func main() {
3131
}
3232

3333
if *demo {
34-
renderPlain(os.Stdout, demoData())
34+
renderPlain(os.Stdout, demoData(), SortPriority)
3535
return
3636
}
3737

@@ -94,7 +94,7 @@ func main() {
9494
fmt.Fprintln(os.Stderr, "No open PRs authored by you.")
9595
return
9696
}
97-
renderPlain(os.Stdout, classified)
97+
renderPlain(os.Stdout, classified, SortPriority)
9898
return
9999
}
100100

@@ -115,7 +115,7 @@ func main() {
115115
fmt.Fprintln(os.Stderr, "No PRs pending your review.")
116116
return
117117
}
118-
renderPlain(os.Stdout, classified)
118+
renderPlain(os.Stdout, classified, SortPriority)
119119
return
120120
}
121121

plain.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"io"
6+
"time"
67
)
78

89
type colWidths struct {
@@ -67,15 +68,44 @@ func plainIndicators(pr ClassifiedPR) string {
6768
return col1 + " " + col2 + " " + col3
6869
}
6970

70-
func renderPlain(w io.Writer, items []ClassifiedPR) {
71+
func formatAge(t time.Time) string {
72+
if t.IsZero() {
73+
return " -"
74+
}
75+
d := time.Since(t)
76+
hours := int(d.Hours())
77+
days := hours / 24
78+
switch {
79+
case hours < 1:
80+
return "now"
81+
case hours < 10:
82+
return fmt.Sprintf("%dh", hours)
83+
case days < 10:
84+
return fmt.Sprintf("%dd", max(days, 1))
85+
case days < 30:
86+
return fmt.Sprintf("%dw", days/7)
87+
case days/30 < 10:
88+
return fmt.Sprintf("%dm", days/30)
89+
default:
90+
return fmt.Sprintf("%dy", max(days/365, 1))
91+
}
92+
}
93+
94+
func renderPlain(w io.Writer, items []ClassifiedPR, sortMode SortMode) {
7195
cols := computeColumns(items)
7296
for _, pr := range items {
7397
repoCol := fmt.Sprintf("%s#%d", pr.RepoName, pr.Number)
7498
indicators := plainIndicators(pr)
75-
fmt.Fprintf(w, "%s %-*s %-*s %s\n",
99+
ageTime := pr.CreatedAt
100+
if sortMode == SortDate {
101+
ageTime = pr.LastActivity
102+
}
103+
age := formatAge(ageTime)
104+
fmt.Fprintf(w, "%s %-*s %-*s %4s %s\n",
76105
indicators,
77106
cols.repo, repoCol,
78107
cols.author, pr.Author,
108+
age,
79109
pr.Title)
80110
}
81111
}

plain_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"strings"
66
"testing"
7+
"time"
78
)
89

910
func TestRenderPlain(t *testing.T) {
@@ -25,28 +26,28 @@ func TestRenderPlain(t *testing.T) {
2526
}
2627

2728
var buf bytes.Buffer
28-
renderPlain(&buf, items)
29+
renderPlain(&buf, items, SortPriority)
2930

3031
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
3132
if len(lines) != 2 {
3233
t.Fatalf("expected 2 lines, got %d: %q", len(lines), buf.String())
3334
}
3435

35-
// Columns are padded: repo to 6 (api#42), author to 5 (alice)
36-
expected0 := "· ✓ ● api#42 alice Add endpoint"
36+
// Columns are padded: repo to 6 (api#42), author to 5 (alice), age 4 chars right-aligned
37+
expected0 := "· ✓ ● api#42 alice - Add endpoint"
3738
if lines[0] != expected0 {
3839
t.Fatalf("line 0:\ngot: %q\nwant: %q", lines[0], expected0)
3940
}
4041

41-
expected1 := "~ · · web#7 bob Fix layout"
42+
expected1 := "~ · · web#7 bob - Fix layout"
4243
if lines[1] != expected1 {
4344
t.Fatalf("line 1:\ngot: %q\nwant: %q", lines[1], expected1)
4445
}
4546
}
4647

4748
func TestRenderPlain_Empty(t *testing.T) {
4849
var buf bytes.Buffer
49-
renderPlain(&buf, nil)
50+
renderPlain(&buf, nil, SortPriority)
5051
if buf.Len() != 0 {
5152
t.Fatalf("expected empty output, got %q", buf.String())
5253
}
@@ -61,7 +62,7 @@ func TestRenderPlain_AllIndicators(t *testing.T) {
6162
}
6263

6364
var buf bytes.Buffer
64-
renderPlain(&buf, items)
65+
renderPlain(&buf, items, SortPriority)
6566

6667
output := buf.String()
6768
// Should NOT contain old tags
@@ -76,6 +77,35 @@ func TestRenderPlain_AllIndicators(t *testing.T) {
7677
}
7778
}
7879

80+
func TestFormatAge(t *testing.T) {
81+
cases := []struct {
82+
dur time.Duration
83+
want string
84+
}{
85+
{30 * time.Minute, "now"},
86+
{2 * time.Hour, "2h"},
87+
{9 * time.Hour, "9h"},
88+
{10 * time.Hour, "1d"},
89+
{3 * 24 * time.Hour, "3d"},
90+
{10 * 24 * time.Hour, "1w"},
91+
{22 * 24 * time.Hour, "3w"},
92+
{45 * 24 * time.Hour, "1m"},
93+
{180 * 24 * time.Hour, "6m"},
94+
{400 * 24 * time.Hour, "1y"},
95+
}
96+
for _, tc := range cases {
97+
got := formatAge(time.Now().Add(-tc.dur))
98+
if got != tc.want {
99+
t.Errorf("formatAge(-%v) = %q, want %q", tc.dur, got, tc.want)
100+
}
101+
}
102+
103+
// Zero time
104+
if got := formatAge(time.Time{}); got != " -" {
105+
t.Errorf("formatAge(zero) = %q, want %q", got, " -")
106+
}
107+
}
108+
79109
func TestRenderPlain_NewFormat(t *testing.T) {
80110
items := []ClassifiedPR{
81111
{
@@ -88,7 +118,7 @@ func TestRenderPlain_NewFormat(t *testing.T) {
88118
},
89119
}
90120
var buf bytes.Buffer
91-
renderPlain(&buf, items)
121+
renderPlain(&buf, items, SortPriority)
92122
output := buf.String()
93123

94124
if strings.Contains(output, "[NEW]") || strings.Contains(output, "[STL]") {

tui.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,18 @@ func (m model) View() string {
336336
if m.showAuthor {
337337
header = "📝 👥 💬"
338338
}
339-
b.WriteString(helpStyle.Render(header))
339+
// Pad to align age label over the age column
340+
ageLabel := "age"
341+
if m.sortMode == SortDate {
342+
ageLabel = "act"
343+
}
344+
headerPad := 6 + m.cols.repo + 2 + m.cols.author + 2 // indicators + space + repo + sep + author + sep
345+
padNeeded := headerPad - lipgloss.Width(header)
346+
if padNeeded < 1 {
347+
padNeeded = 1
348+
}
349+
headerLine := header + strings.Repeat(" ", padNeeded) + fmt.Sprintf("%4s", ageLabel)
350+
b.WriteString(helpStyle.Render(headerLine))
340351
b.WriteString("\n")
341352

342353
// Scrolling: determine visible window
@@ -360,9 +371,14 @@ func (m model) View() string {
360371
indicators := formatIndicators(pr, m.showAuthor, bg)
361372
repoCol := fmt.Sprintf("%-*s", m.cols.repo, fmt.Sprintf("%s#%d", pr.RepoName, pr.Number))
362373
authorCol := fmt.Sprintf("%-*s", m.cols.author, pr.Author)
374+
ageTime := pr.CreatedAt
375+
if m.sortMode == SortDate {
376+
ageTime = pr.LastActivity
377+
}
378+
ageCol := fmt.Sprintf("%4s", formatAge(ageTime))
363379

364380
// Build plain line for truncation check, then colorized version for display
365-
plainLine := fmt.Sprintf("%s %s %s %s", " ", repoCol, authorCol, pr.Title)
381+
plainLine := fmt.Sprintf("%s %s %s %s %s", " ", repoCol, authorCol, ageCol, pr.Title)
366382
titleText := pr.Title
367383
if m.width > 0 && len(plainLine) > m.width {
368384
// Truncate title to fit
@@ -380,8 +396,9 @@ func (m model) View() string {
380396
coloredRepo = nameColor(pr.RepoName).Inherit(selBg).Render(repoCol)
381397
coloredAuthor = nameColor(pr.Author).Inherit(selBg).Render(authorCol)
382398
sep := selBg.Render(" ")
399+
ageRendered := styleDim.Inherit(selBg).Render(ageCol)
383400
titleRendered := selBg.Render(titleText)
384-
line = indicators + selBg.Render(" ") + coloredRepo + sep + coloredAuthor + sep + titleRendered
401+
line = indicators + selBg.Render(" ") + coloredRepo + sep + coloredAuthor + sep + ageRendered + sep + titleRendered
385402
// Pad to full width
386403
if m.width > 0 {
387404
lineLen := lipgloss.Width(line)
@@ -392,7 +409,8 @@ func (m model) View() string {
392409
} else {
393410
coloredRepo = nameColor(pr.RepoName).Render(repoCol)
394411
coloredAuthor = nameColor(pr.Author).Render(authorCol)
395-
line = fmt.Sprintf("%s %s %s %s", indicators, coloredRepo, coloredAuthor, titleText)
412+
ageRendered := styleDim.Render(ageCol)
413+
line = fmt.Sprintf("%s %s %s %s %s", indicators, coloredRepo, coloredAuthor, ageRendered, titleText)
396414
}
397415
b.WriteString(line)
398416
b.WriteString("\n")

0 commit comments

Comments
 (0)