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}