▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
2.2 KB raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
}