{"id":47876520,"url":"https://github.com/bro3886/go-eventkit","last_synced_at":"2026-04-11T23:04:17.939Z","repository":{"id":337776569,"uuid":"1154844023","full_name":"BRO3886/go-eventkit","owner":"BRO3886","description":"Native macOS Calendar \u0026 Reminders bindings for Go via EventKit — 3000x faster than AppleScript","archived":false,"fork":false,"pushed_at":"2026-03-20T18:59:04.000Z","size":500,"stargazers_count":11,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T01:24:25.515Z","etag":null,"topics":["apple","calendar","cgo","cli","eventkit","go","golang","icloud","macos","native","objective-c","reminders"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/BRO3886/go-eventkit","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BRO3886.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-10T20:52:34.000Z","updated_at":"2026-03-27T17:27:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/BRO3886/go-eventkit","commit_stats":null,"previous_names":["bro3886/go-eventkit"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/BRO3886/go-eventkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BRO3886%2Fgo-eventkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BRO3886%2Fgo-eventkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BRO3886%2Fgo-eventkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BRO3886%2Fgo-eventkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BRO3886","download_url":"https://codeload.github.com/BRO3886/go-eventkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BRO3886%2Fgo-eventkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31698152,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-11T21:17:31.016Z","status":"ssl_error","status_checked_at":"2026-04-11T21:17:24.556Z","response_time":54,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apple","calendar","cgo","cli","eventkit","go","golang","icloud","macos","native","objective-c","reminders"],"created_at":"2026-04-04T01:17:32.305Z","updated_at":"2026-04-11T23:04:17.922Z","avatar_url":"https://github.com/BRO3886.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-eventkit\n\nNative macOS Calendar and Reminders access for Go. **3000x faster than AppleScript.**\n\n```go\nclient, _ := calendar.New()\nevents, _ := client.Events(time.Now(), time.Now().Add(7*24*time.Hour)) // ~9ms\n```\n\nNo AppleScript. No subprocesses. Direct EventKit access via cgo, with an idiomatic Go API.\n\n|                       | go-eventkit | AppleScript | Speedup   |\n| --------------------- | ----------- | ----------- | --------- |\n| Fetch calendars       | 0.2ms       | 620ms       | **3101x** |\n| Fetch events (7 days) | 18ms        | 432ms       | **24x**   |\n| Fetch reminders       | 47ms        | 9.2s        | **197x**  |\n\n## Features\n\n- **Calendar events** — Full CRUD: list/create/rename/delete calendars, query events by date range, create, update, delete\n- **Reminders** — Full CRUD: list/create/rename/delete reminder lists, query/filter reminders, create, update, delete, complete/uncomplete\n- **Recurrence rules** — Daily, weekly, monthly, yearly with full constraint support (days of week, days of month, set positions, end date/count)\n- **Structured locations** — Geographic coordinates and geofence radius on events\n- **Change notifications** — `WatchChanges(ctx)` delivers a signal on any EventKit database change (iCloud sync, other apps, own writes) via a Go channel\n- **Date parsing** — Shared natural language date parser (`dateparser/`) with support for \"tomorrow 2pm\", \"next friday\", \"in 3 hours\", \"eow\", ISO 8601, and more\n- **All accounts** — Sees iCloud, Google, Exchange, subscribed, and local calendars/reminders\n- **Concurrency safe** — Write operations serialized via dispatch queue, inline error returns (no thread-local storage), safe for use from multiple goroutines\n- **Pure Go API** — Idiomatic types, no cgo leaking to consumers\n- **Cross-platform safe** — Types importable everywhere, bridge returns `ErrUnsupported` on non-darwin\n\n## Requirements\n\n- macOS (darwin), Go 1.24+, Xcode Command Line Tools (`xcode-select --install`)\n\n## Installation\n\n```bash\ngo get github.com/BRO3886/go-eventkit\n```\n\n## Quick Start\n\n### Calendar\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/BRO3886/go-eventkit\"\n    \"github.com/BRO3886/go-eventkit/calendar\"\n)\n\nfunc main() {\n    client, err := calendar.New()\n    if err != nil {\n        log.Fatal(err) // TCC access denied\n    }\n\n    // List all calendars\n    calendars, _ := client.Calendars()\n    for _, c := range calendars {\n        fmt.Printf(\"%s (%s, %s)\\n\", c.Title, c.Type, c.Source)\n    }\n\n    // Fetch events for the next 7 days\n    now := time.Now()\n    events, _ := client.Events(now, now.Add(7*24*time.Hour))\n    for _, e := range events {\n        fmt.Printf(\"%s: %s - %s\\n\", e.Title, e.StartDate.Format(time.Kitchen), e.EndDate.Format(time.Kitchen))\n    }\n\n    // Create an event\n    event, _ := client.CreateEvent(calendar.CreateEventInput{\n        Title:     \"Team standup\",\n        StartDate: time.Date(2026, 2, 12, 10, 0, 0, 0, time.Local),\n        EndDate:   time.Date(2026, 2, 12, 10, 30, 0, 0, time.Local),\n        Calendar:  \"Work\",\n        Alerts:    []calendar.Alert{{RelativeOffset: -15 * time.Minute}},\n    })\n    fmt.Printf(\"Created: %s (ID: %s)\\n\", event.Title, event.ID)\n\n    // Create a recurring event with a structured location\n    event, _ = client.CreateEvent(calendar.CreateEventInput{\n        Title:     \"Weekly sync\",\n        StartDate: time.Date(2026, 2, 12, 14, 0, 0, 0, time.Local),\n        EndDate:   time.Date(2026, 2, 12, 15, 0, 0, 0, time.Local),\n        Calendar:  \"Work\",\n        RecurrenceRules: []eventkit.RecurrenceRule{\n            eventkit.Weekly(1, eventkit.Monday, eventkit.Wednesday, eventkit.Friday).\n                Until(time.Date(2026, 12, 31, 0, 0, 0, 0, time.Local)),\n        },\n        StructuredLocation: \u0026eventkit.StructuredLocation{\n            Title:     \"Apple Park\",\n            Latitude:  37.3349,\n            Longitude: -122.0090,\n            Radius:    150,\n        },\n    })\n    fmt.Printf(\"Created recurring: %s (rules: %d)\\n\", event.Title, len(event.RecurrenceRules))\n}\n```\n\n### Reminders\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/BRO3886/go-eventkit/reminders\"\n)\n\nfunc main() {\n    client, err := reminders.New()\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    // List all reminder lists\n    lists, _ := client.Lists()\n    for _, l := range lists {\n        fmt.Printf(\"%s (%d items)\\n\", l.Title, l.Count)\n    }\n\n    // Get incomplete reminders from a specific list\n    items, _ := client.Reminders(\n        reminders.WithList(\"Shopping\"),\n        reminders.WithCompleted(false),\n    )\n    for _, r := range items {\n        fmt.Printf(\"[ ] %s (due: %v)\\n\", r.Title, r.DueDate)\n    }\n\n    // Create a reminder\n    due := time.Now().Add(24 * time.Hour)\n    reminder, _ := client.CreateReminder(reminders.CreateReminderInput{\n        Title:    \"Buy milk\",\n        ListName: \"Shopping\",\n        DueDate:  \u0026due,\n        Priority: reminders.PriorityHigh,\n    })\n    fmt.Printf(\"Created: %s (ID: %s)\\n\", reminder.Title, reminder.ID)\n\n    // Complete it\n    client.CompleteReminder(reminder.ID)\n}\n```\n\n### Change Notifications\n\n```go\nctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\ndefer cancel()\n\n// Calendar changes\nchanges, err := client.WatchChanges(ctx)\nif err != nil {\n    log.Fatal(err)\n}\nfor range changes {\n    events, _ := client.Events(start, end)\n    render(events) // re-fetch on every signal\n}\n```\n\n```go\n// Reminders changes\nchanges, err := remClient.WatchChanges(ctx)\nfor range changes {\n    items, _ := remClient.Reminders(reminders.WithCompleted(false))\n    render(items)\n}\n```\n\nThe channel is buffered (cap 16) and excess signals are coalesced — consumers always re-fetch. The channel closes when `ctx` is cancelled or the pipe fails. Only one watcher may be active per package at a time.\n\n\u003e **Cross-process note**: If your consumer and producer are separate binaries, the consumer must pump the main CFRunLoop to receive cross-process `EKEventStoreChangedNotification`. See [`scripts/watch-demo/consumer`](scripts/watch-demo/consumer/main.go) for the `runtime.LockOSThread()` + `CFRunLoopRunInMode` pattern. Single-binary use (same process writes and watches) works without any run loop setup.\n\n### Date Parsing\n\n```go\nimport \"github.com/BRO3886/go-eventkit/dateparser\"\n\n// Simple usage (defaults: midnight, no rollover)\nt, err := dateparser.ParseDate(\"tomorrow 2pm\")\nt, err = dateparser.ParseDate(\"next friday\")\nt, err = dateparser.ParseDate(\"in 3 hours\")\nt, err = dateparser.ParseDate(\"mar 15\")\nt, err = dateparser.ParseDate(\"eow\") // Friday 5pm\n\n// Reminder-style: bare dates at 9am, past times roll to tomorrow\nt, err = dateparser.ParseDate(\"tomorrow\",\n    dateparser.WithDefaultHour(9),        // \"today\" → 9am not midnight\n    dateparser.WithSmartTimeRollover(),    // \"9am\" when past → tomorrow\n    dateparser.WithEOWSkipToday(),         // \"eow\" on Friday → next Friday\n)\n\n// Deterministic (for testing)\nt, err = dateparser.ParseDateRelativeTo(\"in 3 hours\", refTime)\n\n// Formatting\ndateparser.FormatDuration(start, end, false)    // \"1h 30m\"\ndateparser.FormatTimeRange(start, end, true)    // \"All Day\"\nd, err := dateparser.ParseAlertDuration(\"15m\")  // 15 * time.Minute\n```\n\nSupports: keywords (`today`, `tomorrow`, `now`, `eod`, `eow`, `this week`, `next week`, `next month`), relative (`in 3 hours`, `5 days ago`), weekdays (`next monday`, `friday 2pm`), month-day (`mar 15`, `21 march 2026`), time-only (`5pm`, `17:00`), and standard formats (ISO 8601, RFC 3339, US dates).\n\n## API Reference\n\n### Calendar Package\n\n```go\nimport \"github.com/BRO3886/go-eventkit/calendar\"\n```\n\n| Method                                               | Description                       |\n| ---------------------------------------------------- | --------------------------------- |\n| `New() (*Client, error)`                             | Create client, request TCC access |\n| `Calendars() ([]Calendar, error)`                    | List all calendars                |\n| `Events(start, end, ...ListOption) ([]Event, error)` | Query events in date range        |\n| `Event(id) (*Event, error)`                          | Get single event by ID            |\n| `CreateEvent(input) (*Event, error)`                 | Create a new event                |\n| `UpdateEvent(id, input, span) (*Event, error)`       | Update an existing event          |\n| `DeleteEvent(id, span) error`                        | Delete an event                   |\n| `DeleteEvents(ids, span) map[string]error`           | Batch delete events               |\n| `CreateCalendar(input) (*Calendar, error)`           | Create a new calendar             |\n| `UpdateCalendar(id, input) (*Calendar, error)`       | Rename or recolor a calendar      |\n| `DeleteCalendar(id) error`                           | Delete a calendar and its events  |\n| `WatchChanges(ctx) (\u003c-chan struct{}, error)`          | Subscribe to database changes     |\n\n**Filter options:** `WithCalendar(name)`, `WithCalendarID(id)`, `WithSearch(query)`\n\n**Recurrence constructors** (from root `eventkit` package): `eventkit.Daily(interval)`, `eventkit.Weekly(interval, ...days)`, `eventkit.Monthly(interval, ...daysOfMonth)`, `eventkit.Yearly(interval)` — chain with `.Until(time)` or `.Count(n)`. Call `rule.Validate()` to check constraints before creating.\n\n### Dateparser Package\n\n```go\nimport \"github.com/BRO3886/go-eventkit/dateparser\"\n```\n\n| Function | Description |\n| --- | --- |\n| `ParseDate(input, ...Option) (time.Time, error)` | Parse natural language date using wall clock |\n| `ParseDateRelativeTo(input, now, ...Option) (time.Time, error)` | Parse relative to a given time (testable) |\n| `FormatDuration(start, end, allDay) string` | Human-readable duration (\"1h 30m\", \"All Day\") |\n| `FormatTimeRange(start, end, allDay) string` | Human-readable time range for display |\n| `ParseAlertDuration(s) (time.Duration, error)` | Parse \"15m\", \"1h\", \"1d\" into duration |\n\n**Options:** `WithDefaultHour(h)` (bare date hour, default 0), `WithSmartTimeRollover()` (past times → tomorrow), `WithEOWSkipToday()` (eow on Friday → next Friday)\n\n### Reminders Package\n\n```go\nimport \"github.com/BRO3886/go-eventkit/reminders\"\n```\n\n| Method                                         | Description                           |\n| ---------------------------------------------- | ------------------------------------- |\n| `New() (*Client, error)`                       | Create client, request TCC access     |\n| `Lists() ([]List, error)`                      | List all reminder lists               |\n| `Reminders(...ListOption) ([]Reminder, error)` | Query reminders with filters          |\n| `Reminder(id) (*Reminder, error)`              | Get single reminder by ID or prefix   |\n| `CreateReminder(input) (*Reminder, error)`     | Create a new reminder                 |\n| `UpdateReminder(id, input) (*Reminder, error)` | Update an existing reminder           |\n| `DeleteReminder(id) error`                     | Delete a reminder                     |\n| `DeleteReminders(ids) map[string]error`        | Batch delete reminders                |\n| `CompleteReminder(id) (*Reminder, error)`      | Mark as completed                     |\n| `UncompleteReminder(id) (*Reminder, error)`    | Mark as incomplete                    |\n| `CreateList(input) (*List, error)`             | Create a new reminder list            |\n| `UpdateList(id, input) (*List, error)`         | Rename or recolor a list              |\n| `DeleteList(id) error`                         | Delete a list and its reminders       |\n| `WatchChanges(ctx) (\u003c-chan struct{}, error)`    | Subscribe to database changes         |\n\n**Filter options:** `WithList(name)`, `WithListID(id)`, `WithCompleted(bool)`, `WithSearch(query)`, `WithDueBefore(time)`, `WithDueAfter(time)`\n\n### Priority Values\n\n| Constant         | Value | Apple Mapping  |\n| ---------------- | ----- | -------------- |\n| `PriorityNone`   | 0     | No priority    |\n| `PriorityHigh`   | 1     | Priorities 1-4 |\n| `PriorityMedium` | 5     | Priority 5     |\n| `PriorityLow`    | 9     | Priorities 6-9 |\n\n## Benchmarks\n\nMeasured on Apple M1 Pro, macOS 15.5. Every operation completes in under 50ms median (calendar CRUD under 10ms).\n\n### Integration Benchmarks (median, 50-100 iterations)\n\n| Operation                     | Median | P95   |\n| ----------------------------- | ------ | ----- |\n| `calendar.Calendars()`        | 0.2ms  | 0.3ms |\n| `calendar.Events(7 days)`     | 8.8ms  | 9.3ms |\n| `calendar.Events(30 days)`    | 20.6ms | 22ms  |\n| `calendar.Events(365 days)`   | 103ms  | 106ms |\n| `calendar.CreateEvent()`      | 4.9ms  | 11ms  |\n| `calendar.Event(id)`          | 0.3ms  | 0.5ms |\n| `calendar.UpdateEvent()`      | 3.5ms  | 4.5ms |\n| `calendar.DeleteEvent()`      | 3.0ms  | 9.5ms |\n| `reminders.Lists()`           | 33ms   | 35ms  |\n| `reminders.Reminders() [all]` | 41ms   | 42ms  |\n| `reminders.CreateReminder()`  | 16ms   | 37ms  |\n| `reminders.Reminder(id)`      | 37ms   | 42ms  |\n| `reminders.DeleteReminder()`  | 54ms   | 66ms  |\n\n### JSON Parsing Layer\n\n| Operation                   | Time  | Allocs |\n| --------------------------- | ----- | ------ |\n| Parse 50 events             | 378µs | 1499   |\n| Parse 500 events            | 3.8ms | 14700  |\n| Parse 50 reminders          | 213µs | 878    |\n| Parse 500 reminders         | 2.1ms | 8513   |\n| Marshal CreateEventInput    | 1.9µs | 12     |\n| Marshal CreateReminderInput | 5.1µs | 83     |\n\n\u003cdetails\u003e\n\u003csummary\u003eRun benchmarks yourself\u003c/summary\u003e\n\n```bash\n# Microbenchmarks (no TCC required)\ngo test -bench=. -benchmem ./calendar/\ngo test -bench=. -benchmem ./reminders/\n\n# Integration benchmarks (requires TCC calendar/reminders access)\ngo run -tags integration ./scripts/benchmark.go\n```\n\n\u003c/details\u003e\n\n## Permissions (TCC)\n\nOn first use, macOS will prompt for Calendar/Reminders access. The prompt shows the terminal app name (Terminal.app, iTerm2, etc.), not the Go binary.\n\nManage permissions in **System Settings \u003e Privacy \u0026 Security \u003e Calendars / Reminders**.\n\n## Architecture\n\n```mermaid\ngraph LR\n    App[\"Your Go App\"] --\u003e Cal[\"calendar/\"]\n    App --\u003e Rem[\"reminders/\"]\n    App --\u003e DP[\"dateparser/\u003cbr/\u003e\u003ci\u003edate parsing\u003c/i\u003e\"]\n    Cal --\u003e EK[\"eventkit.go\u003cbr/\u003e\u003ci\u003eshared types\u003c/i\u003e\"]\n    Rem --\u003e EK\n\n    subgraph \"calendar/\"\n        direction TB\n        CalGo[\"calendar.go\u003cbr/\u003e\u003ci\u003eGo types\u003c/i\u003e\"]\n        CalParse[\"parse.go\u003cbr/\u003e\u003ci\u003eJSON ↔ Go\u003c/i\u003e\"]\n        CalBridge[\"bridge_darwin.go\u003cbr/\u003e\u003ci\u003ecgo wrappers\u003c/i\u003e\"]\n        CalObjC[\"bridge_darwin.m\u003cbr/\u003e\u003ci\u003eObjC bridge\u003c/i\u003e\"]\n        CalGo --\u003e CalParse --\u003e CalBridge --\u003e CalObjC\n    end\n\n    subgraph \"reminders/\"\n        direction TB\n        RemGo[\"reminders.go\u003cbr/\u003e\u003ci\u003eGo types\u003c/i\u003e\"]\n        RemParse[\"parse.go\u003cbr/\u003e\u003ci\u003eJSON ↔ Go\u003c/i\u003e\"]\n        RemBridge[\"bridge_darwin.go\u003cbr/\u003e\u003ci\u003ecgo wrappers\u003c/i\u003e\"]\n        RemObjC[\"bridge_darwin.m\u003cbr/\u003e\u003ci\u003eObjC bridge\u003c/i\u003e\"]\n        RemGo --\u003e RemParse --\u003e RemBridge --\u003e RemObjC\n    end\n\n    CalObjC --\u003e EKStore1[\"EKEventStore\u003cbr/\u003e\u003ci\u003esingleton\u003c/i\u003e\"]\n    RemObjC --\u003e EKStore2[\"EKEventStore\u003cbr/\u003e\u003ci\u003esingleton\u003c/i\u003e\"]\n    EKStore1 --\u003e EventKit[\"Apple EventKit\u003cbr/\u003e\u003ci\u003eframework\u003c/i\u003e\"]\n    EKStore2 --\u003e EventKit\n\n    style App fill:#4a9eff,color:#fff\n    style EventKit fill:#333,color:#fff\n    style EK fill:#f0ad4e,color:#fff\n    style EKStore1 fill:#5cb85c,color:#fff\n    style EKStore2 fill:#5cb85c,color:#fff\n    style DP fill:#d9534f,color:#fff\n```\n\nThe data flow is: **Go types → JSON string → cgo → ObjC → EventKit** (and back). Each package has its own `EKEventStore` singleton — C objects can't cross cgo package boundaries. The public API is pure Go; cgo never leaks to consumers.\n\n## Known Limitations\n\nThese are Apple EventKit limitations, not bugs:\n\n- **Attendees/organizer are read-only** — Apple limitation since 2013\n- **Flagged property unavailable** — Not exposed by EventKit despite being visible in Reminders.app\n- **Events require date ranges** — Cannot fetch all events unbounded\n- **Birthday/subscription calendars are read-only**\n- **Recurrence is a subset of RFC 5545** — Daily/weekly/monthly/yearly only, no hourly/minutely\n\n## Building \u0026 Testing\n\n```bash\ngo build ./...                                        # Build\ngo test ./...                                         # Unit tests (includes dateparser)\ngo test ./dateparser/...                              # Dateparser tests only (35 tests)\ngo run -tags integration ./scripts/integration.go     # Calendar integration tests\ngo run -tags integration ./scripts/integration_reminders.go  # Reminder integration tests\nGOOS=linux CGO_ENABLED=0 go build ./...               # Cross-platform stubs\n```\n\n## Prior Art\n\nExtracts the proven cgo + ObjC bridge pattern from [rem](https://github.com/BRO3886/rem) (macOS Reminders CLI, 100+ stars). No competing Go EventKit package exists.\n\nKey improvements: all writes via EventKit (rem uses AppleScript), calendar support, library-first design.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbro3886%2Fgo-eventkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbro3886%2Fgo-eventkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbro3886%2Fgo-eventkit/lists"}