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