package activity import ( "context" "encoding/json" "os/exec" "strings" "time" ) // Event is one calendar event from the cal-events helper. type Event struct { Start, End time.Time Title, Calendar string } // CalendarFilter selects calendars by name, case-insensitively. Non-empty // Show is an allowlist; Hide is a blocklist applied after Show. Zero value // passes everything. type CalendarFilter struct { Show []string Hide []string } // Allow reports whether events from the named calendar should appear. func (f CalendarFilter) Allow(name string) bool { n := strings.ToLower(name) if len(f.Show) > 0 { ok := false for _, s := range f.Show { if strings.ToLower(s) == n { ok = true break } } if !ok { return false } } for _, h := range f.Hide { if strings.ToLower(h) == n { return false } } return true } // rawEvent mirrors one cal-events JSON object. type rawEvent struct { Title string `json:"title"` Start string `json:"start"` End string `json:"end"` Calendar string `json:"calendar"` } // parseEvents decodes the cal-events JSON array, keeping events that pass // filter. Any decode error yields an empty slice (tolerant). func parseEvents(b []byte, filter CalendarFilter) []Event { var raws []rawEvent if err := json.Unmarshal(b, &raws); err != nil { return nil } var events []Event for _, r := range raws { start, err := time.Parse(time.RFC3339, r.Start) if err != nil { continue } end, err := time.Parse(time.RFC3339, r.End) if err != nil { continue } if !filter.Allow(r.Calendar) { continue } events = append(events, Event{Start: start, End: end, Title: r.Title, Calendar: r.Calendar}) } return events } // LoadCalendar runs the cal-events helper for date (YYYY-MM-DD) and filters // the result. Missing helper, denied access, or bad output ⇒ empty slice, no // error — the timeline still renders without calendar data. func LoadCalendar(bin, date string, filter CalendarFilter) ([]Event, error) { if bin == "" { return nil, nil } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() out, err := exec.CommandContext(ctx, bin, date).Output() if err != nil { return nil, nil } return parseEvents(out, filter), nil }