(output: string)
| 58 | } |
| 59 | |
| 60 | function parseKhalOutput(output: string): CalendarEvent[] { |
| 61 | const lines = output.trim().split("\n").filter(Boolean); |
| 62 | const events: CalendarEvent[] = []; |
| 63 | const seen = new Set<string>(); |
| 64 | let currentDate = ""; |
| 65 | |
| 66 | for (const line of lines) { |
| 67 | // Date header: "Today, 03/22/26" or "Monday, 03/24/26" or "Tomorrow, 03/23/26" |
| 68 | const dateMatch = line.match(/^[A-Za-z]+,?\s+(\d{2})\/(\d{2})\/(\d{2})$/); |
| 69 | if (dateMatch) { |
| 70 | currentDate = `20${dateMatch[3]}-${dateMatch[1]}-${dateMatch[2]}`; |
| 71 | continue; |
| 72 | } |
| 73 | |
| 74 | // Skip raw datetime strings (e.g. " Mar 23 13:30:00 2026 ...") |
| 75 | if (/^\s*[A-Z][a-z]{2}\s+\d/.test(line) && /\d{2}:\d{2}:\d{2}/.test(line)) continue; |
| 76 | |
| 77 | if (!currentDate) continue; |
| 78 | |
| 79 | // Event with time: "13:30-14:00 Terri Meeting ⏰" |
| 80 | const eventMatch = line.match(/^(\d{1,2}:\d{2}(?:-\d{1,2}:\d{2})?)\s+(.+)$/); |
| 81 | if (eventMatch) { |
| 82 | const title = decodeHtmlEntities(eventMatch[2].replace(/[🔔⏰🔕⟳]/g, "").trim()); |
| 83 | const key = `${currentDate}|${eventMatch[1]}|${title}`; |
| 84 | if (!seen.has(key)) { |
| 85 | seen.add(key); |
| 86 | events.push({ |
| 87 | key: buildEventKey({ date: currentDate, time: eventMatch[1], title, allDay: false }), |
| 88 | date: currentDate, |
| 89 | time: eventMatch[1], |
| 90 | title, |
| 91 | allDay: false, |
| 92 | }); |
| 93 | } |
| 94 | continue; |
| 95 | } |
| 96 | |
| 97 | // All-day event (no time prefix, no raw datetime) |
| 98 | const trimmed = line.trim(); |
| 99 | if (trimmed && !/\d{2}:\d{2}:\d{2}/.test(trimmed)) { |
| 100 | const title = decodeHtmlEntities(trimmed.replace(/[🔔⏰🔕⟳]/g, "").trim()); |
| 101 | if (title) { |
| 102 | const key = `${currentDate}|allday|${title}`; |
| 103 | if (!seen.has(key)) { |
| 104 | seen.add(key); |
| 105 | events.push({ |
| 106 | key: buildEventKey({ date: currentDate, time: "All Day", title, allDay: true }), |
| 107 | date: currentDate, |
| 108 | time: "All Day", |
| 109 | title, |
| 110 | allDay: true, |
| 111 | }); |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | return events; |
no test coverage detected