flake-update-20260505
1// Package display provides terminal output formatting.
2package display
3
4import (
5 "fmt"
6 "strings"
7
8 "github.com/vdemeester/home/tools/daily-plan/internal/ai"
9 "github.com/vdemeester/home/tools/daily-plan/internal/github"
10 "github.com/vdemeester/home/tools/daily-plan/internal/jira"
11 "github.com/vdemeester/home/tools/daily-plan/internal/org"
12)
13
14const (
15 bold = "\033[1m"
16 dim = "\033[2m"
17 red = "\033[0;31m"
18 green = "\033[0;32m"
19 yellow = "\033[0;33m"
20 blue = "\033[0;34m"
21 magenta = "\033[0;35m"
22 cyan = "\033[0;36m"
23 reset = "\033[0m"
24)
25
26// Header prints a section header.
27func Header(title string) {
28 fmt.Printf("\n%s%s── %s ──%s\n", bold, cyan, title, reset)
29}
30
31// SubHeader prints a subsection header.
32func SubHeader(title string) {
33 fmt.Printf(" %s%s%s%s\n", bold, magenta, title, reset)
34}
35
36// OrgItems prints scheduled org items.
37func OrgItems(items []org.ScheduledItem) {
38 if len(items) == 0 {
39 fmt.Printf(" %s(nothing scheduled)%s\n", dim, reset)
40 return
41 }
42 for _, item := range items {
43 state := item.State
44 if state != "" {
45 state += " "
46 }
47 fmt.Printf(" %s%s%s\n", state, item.Heading, reset)
48 }
49}
50
51// JiraIssues prints a list of Jira issues.
52func JiraIssues(issues []jira.Issue, style string) {
53 if len(issues) == 0 {
54 fmt.Printf(" %s(none)%s\n", dim, reset)
55 return
56 }
57 for _, issue := range issues {
58 color := yellow
59 switch style {
60 case "dim":
61 color = dim
62 case "alert":
63 color = red
64 case "done":
65 color = green
66 }
67
68 summary := issue.Summary
69 if len(summary) > 70 {
70 summary = summary[:67] + "..."
71 }
72
73 if style == "alert" {
74 fmt.Printf(" %s⚠ %-14s%s %s %s[%s]%s\n", color, issue.Key, reset, summary, dim, issue.Status, reset)
75 } else {
76 fmt.Printf(" %s%-14s%s %-70s %s[%s]%s\n", color, issue.Key, reset, summary, dim, issue.Status, reset)
77 }
78 }
79}
80
81// JiraIssuesLimited prints issues with a limit and "N more" indicator.
82func JiraIssuesLimited(issues []jira.Issue, limit int, style string) {
83 if len(issues) <= limit {
84 JiraIssues(issues, style)
85 return
86 }
87 JiraIssues(issues[:limit], style)
88 fmt.Printf(" %s... and %d more%s\n", dim, len(issues)-limit, reset)
89}
90
91// CVEIssues prints CVE issues grouped by CVE ID with total count.
92func CVEIssues(grouped []jira.Issue, totalCount int) {
93 if len(grouped) == 0 {
94 fmt.Printf(" %s(none)%s\n", dim, reset)
95 return
96 }
97 JiraIssues(grouped, "alert")
98 if totalCount > len(grouped) {
99 fmt.Printf(" %s(%d total issues across images — showing first per CVE)%s\n", dim, totalCount, reset)
100 }
101}
102
103// GitHubItems prints GitHub issues or PRs.
104func GitHubItems(items []github.Item, style string) {
105 if len(items) == 0 {
106 fmt.Printf(" %s(none)%s\n", dim, reset)
107 return
108 }
109 for _, item := range items {
110 color := blue
111 switch style {
112 case "review":
113 color = red
114 case "pr":
115 color = green
116 case "done":
117 color = green
118 }
119
120 ref := item.Ref()
121 title := item.Title
122 if len(title) > 60 {
123 title = title[:57] + "..."
124 }
125
126 extra := ""
127 if item.Author != "" {
128 extra = fmt.Sprintf("@%s", item.Author)
129 }
130 dateStr := item.CreatedAt.Format("2006-01-02")
131 if item.ClosedAt != nil {
132 dateStr = item.ClosedAt.Format("2006-01-02")
133 }
134
135 parts := []string{}
136 if extra != "" {
137 parts = append(parts, extra)
138 }
139 parts = append(parts, dateStr)
140
141 fmt.Printf(" %s%-40s%s %s %s(%s)%s\n", color, ref, reset, title, dim, strings.Join(parts, ", "), reset)
142 }
143}
144
145// SecurityAdvisories prints GitHub security advisories.
146func SecurityAdvisories(advisories []github.SecurityAdvisory) {
147 if len(advisories) == 0 {
148 fmt.Printf(" %s(none)%s\n", dim, reset)
149 return
150 }
151 for _, a := range advisories {
152 sevColor := yellow
153 switch a.Severity {
154 case "critical":
155 sevColor = red
156 case "high":
157 sevColor = red
158 case "low":
159 sevColor = dim
160 }
161 ghsa := a.GHSAID
162 if a.CVEID != "" {
163 ghsa = a.CVEID
164 }
165 summary := a.Summary
166 if len(summary) > 60 {
167 summary = summary[:57] + "..."
168 }
169 fmt.Printf(" %s⚠ %-20s%s %-60s %s[%s, %s]%s\n",
170 sevColor, ghsa, reset, summary, dim, a.Severity, a.Repo, reset)
171 }
172}
173
174// DependabotAlerts prints dependabot alerts.
175func DependabotAlerts(alerts []github.DependabotAlert) {
176 if len(alerts) == 0 {
177 fmt.Printf(" %s(none)%s\n", dim, reset)
178 return
179 }
180 for _, a := range alerts {
181 sevColor := yellow
182 switch a.Severity {
183 case "critical", "high":
184 sevColor = red
185 case "low":
186 sevColor = dim
187 }
188 id := a.CVE
189 if id == "" {
190 id = fmt.Sprintf("#%d", a.Number)
191 }
192 summary := a.Summary
193 if len(summary) > 55 {
194 summary = summary[:52] + "..."
195 }
196 fmt.Printf(" %s⚠ %-20s%s %-55s %s[%s, %s, %s]%s\n",
197 sevColor, id, reset, summary, dim, a.Severity, a.Package, a.Repo, reset)
198 }
199}
200
201// OrgDoneItems prints completed org tasks, grouped by section.
202func OrgDoneItems(items []org.DoneItem) {
203 if len(items) == 0 {
204 fmt.Printf(" %s(none)%s\n", dim, reset)
205 return
206 }
207 // Group by section
208 bySection := make(map[string][]org.DoneItem)
209 var order []string
210 for _, item := range items {
211 section := item.Section
212 if section == "" {
213 section = "(uncategorized)"
214 }
215 if _, exists := bySection[section]; !exists {
216 order = append(order, section)
217 }
218 bySection[section] = append(bySection[section], item)
219 }
220 for _, section := range order {
221 sectionItems := bySection[section]
222 fmt.Printf(" %s%s%s%s\n", dim, bold, section, reset)
223 for _, item := range sectionItems {
224 title := item.Title
225 if len(title) > 65 {
226 title = title[:62] + "..."
227 }
228 fmt.Printf(" %s✓%s %s %s(%s)%s\n",
229 green, reset, title, dim, item.CompletedAt.Format("2006-01-02 15:04"), reset)
230 }
231 }
232}
233
234// AIItems prints AI session/learning/research items grouped by type.
235// Sessions are shown as a compact summary (count per day + notable titles).
236// Learnings and research are shown individually.
237func AIItems(items []ai.Item) {
238 if len(items) == 0 {
239 fmt.Printf(" %s(none)%s\n", dim, reset)
240 return
241 }
242 // Separate by type
243 var sessions, learnings, research []ai.Item
244 for _, item := range items {
245 // Skip auto-recovered noise
246 if strings.Contains(strings.ToLower(item.Title), "auto-recovered") {
247 continue
248 }
249 switch item.Type {
250 case "session":
251 sessions = append(sessions, item)
252 case "learning":
253 learnings = append(learnings, item)
254 case "research":
255 research = append(research, item)
256 }
257 }
258
259 if len(sessions) > 0 {
260 // Group sessions by date and show counts + notable titles
261 byDate := make(map[string][]ai.Item)
262 var dates []string
263 for _, s := range sessions {
264 d := s.Date.Format("2006-01-02")
265 if _, exists := byDate[d]; !exists {
266 dates = append(dates, d)
267 }
268 byDate[d] = append(byDate[d], s)
269 }
270 fmt.Printf(" %s%sSessions (%d total)%s\n", dim, bold, len(sessions), reset)
271 for _, d := range dates {
272 daySessions := byDate[d]
273 // Show up to 3 notable titles per day
274 fmt.Printf(" %s%s%s — %d sessions\n", cyan, d, reset, len(daySessions))
275 shown := 0
276 for _, s := range daySessions {
277 if shown >= 3 {
278 remaining := len(daySessions) - shown
279 if remaining > 0 {
280 fmt.Printf(" %s... and %d more%s\n", dim, remaining, reset)
281 }
282 break
283 }
284 title := s.Title
285 // Strip "Session: " prefix if present
286 title = strings.TrimPrefix(title, "Session: ")
287 if len(title) > 55 {
288 title = title[:52] + "..."
289 }
290 project := ""
291 if s.Project != "" {
292 project = fmt.Sprintf(" %s[%s]%s", dim, s.Project, reset)
293 }
294 fmt.Printf(" • %s%s\n", title, project)
295 shown++
296 }
297 }
298 }
299
300 printAIList := func(label string, items []ai.Item) {
301 if len(items) == 0 {
302 return
303 }
304 fmt.Printf(" %s%s%s (%d)%s\n", dim, bold, label, len(items), reset)
305 for _, item := range items {
306 title := item.Title
307 if len(title) > 60 {
308 title = title[:57] + "..."
309 }
310 fmt.Printf(" %s•%s %s %s%s%s\n",
311 cyan, reset, title, dim, item.Date.Format("2006-01-02"), reset)
312 }
313 }
314
315 printAIList("Learnings", learnings)
316 printAIList("Research", research)
317}
318
319// DiscussionItems prints GitHub discussions.
320func DiscussionItems(items []github.DiscussionItem) {
321 if len(items) == 0 {
322 fmt.Printf(" %s(none)%s\n", dim, reset)
323 return
324 }
325 for _, item := range items {
326 color := cyan
327 if item.Type == "discussion_comment" {
328 color = blue
329 }
330 title := item.Title
331 if len(title) > 60 {
332 title = title[:57] + "..."
333 }
334 cat := ""
335 if item.Category != "" {
336 cat = fmt.Sprintf("[%s] ", item.Category)
337 }
338 fmt.Printf(" %s%-30s%s %s%s %s(%s)%s\n",
339 color, item.Repo, reset, cat, title, dim, item.CreatedAt.Format("2006-01-02"), reset)
340 }
341}
342
343// CommentItems prints GitHub issue/PR comments.
344func CommentItems(items []github.CommentItem) {
345 if len(items) == 0 {
346 fmt.Printf(" %s(none)%s\n", dim, reset)
347 return
348 }
349 for _, item := range items {
350 title := item.IssueTitle
351 if len(title) > 55 {
352 title = title[:52] + "..."
353 }
354 ref := fmt.Sprintf("%s#%d", item.Repo, item.IssueNumber)
355 fmt.Printf(" %s%-40s%s %s %s(%s)%s\n",
356 blue, ref, reset, title, dim, item.CreatedAt.Format("2006-01-02"), reset)
357 }
358}
359
360// Hint prints a dim hint line.
361func Hint(msg string) {
362 fmt.Printf("\n%s%s%s\n", dim, msg, reset)
363}