Architecture
How ical works under the hood — native EventKit bindings via cgo, Go project structure, and key design decisions.
Overview
ical is a native macOS CLI that manages Calendar events through go-eventkit, a Go library providing direct EventKit bindings via cgo. This gives ical near-native performance — roughly 3000x faster than AppleScript-based alternatives.
The entire application compiles to a single binary with no runtime dependencies beyond macOS itself.
How It Works
User → ical CLI (Cobra) → go-eventkit → EventKit (Objective-C via cgo) → Calendar.app store
- The user invokes a command via the Cobra CLI framework
- Commands call functions in the
go-eventkit/calendarpackage - go-eventkit uses cgo to call EventKit’s Objective-C APIs directly
- EventKit reads from and writes to the same store that Calendar.app uses
There is no IPC, no subprocess spawning, and no Apple Events bridge. The Go binary links against EventKit at compile time.
Project Structure
ical/
├── cmd/ical/
│ ├── main.go # Entry point (macOS check, version injection)
│ └── commands/ # One file per Cobra command
│ ├── root.go # Root command + global flags (--output, --no-color)
│ ├── calendars.go # List calendars
│ ├── list.go # List events (date range, filters)
│ ├── show.go # Show single event detail
│ ├── add.go # Create event (flags + interactive -i)
│ ├── update.go # Update event (flags + interactive -i)
│ ├── delete.go # Delete event + pickEvent() helper
│ ├── helpers.go # Shared helpers
│ ├── today.go # Shortcut: today's events
│ ├── upcoming.go # Next N days
│ ├── search.go # Search events
│ ├── export.go # Export events (JSON/CSV/ICS)
│ ├── import.go # Import events (JSON/CSV)
│ └── skills.go # AI agent skill management
├── internal/
│ ├── ui/ # Output formatting (table/json/plain)
│ │ └── output.go
│ ├── export/ # Import/export logic
│ │ ├── json.go
│ │ ├── csv.go
│ │ └── ics.go
│ ├── skills/ # Agent skill install/uninstall logic
│ │ └── skills.go
│ └── update/ # Background update check
│ └── check.go
├── skills/ical-cli/ # Embedded agent skill (baked into binary)
│ ├── SKILL.md
│ └── references/
├── skills.go # go:embed for skills directory
├── Makefile
└── go.mod
Key Dependencies
| Package | Purpose |
|---|---|
github.com/BRO3886/go-eventkit | Native EventKit bindings (cgo) + shared dateparser |
github.com/spf13/cobra | CLI framework |
github.com/olekukonko/tablewriter | Table output formatting |
github.com/fatih/color | Terminal colors |
github.com/charmbracelet/huh | Interactive forms and select menus |
Design Decisions
Row Numbers Instead of Short IDs
Calendar event identifiers in EventKit share a common prefix per calendar — the UUID before the : separator is the calendar ID, not the event ID. This makes short ID prefixes useless for disambiguation when events belong to the same calendar.
Instead, ical uses sequential row numbers (#1, #2, …) displayed in table output. These numbers are cached to ~/.ical-last-list so subsequent commands like ical show 2 or ical delete 1 can reference events from the last listing.
Three Event Selection Methods
- Interactive picker — No arguments triggers a searchable list powered by
charmbracelet/huh - Row number — Numeric argument maps to cached row from last listing
- Event ID — Full or partial
eventIdentifierfor scripting and automation
End-of-Day Bumping
When --to resolves to midnight (00:00:00), ical bumps it to 23:59:59 so that --to "feb 12" includes all events on February 12. Without this, midnight would exclude the entire day.
Embedded Agent Skills
ical embeds its own agent skill files into the binary via go:embed. When a user runs ical skills install, the embedded files are written to the appropriate agent’s skill directory (~/.claude/skills/, ~/.codex/skills/, ~/.openclaw/skills/, or ~/.agents/skills/). This ensures the skill documentation always matches the binary version — no separate download or version mismatch possible.
A .ical-version file is written alongside the skill files to track which binary version installed them. When the binary is updated, ical skills status detects the mismatch and the background update check prints a staleness notice.
Background Update Check
ical checks for new releases in a background goroutine on each command invocation. The check is non-blocking with a 2-second timeout, caches results to ~/.cache/ical/update-check with a 24-hour TTL, and prints to stderr so it doesn’t interfere with piped output. The check is skipped for JSON output, piped commands, dev builds, and when ICAL_NO_UPDATE_CHECK=1 is set.
UTC to Local Conversion
EventKit returns all times in UTC. ical converts them to local time using the event’s timezone (or the system timezone) for display. JSON output preserves ISO 8601 timestamps.
Limitations
These are Apple-imposed constraints, not bugs:
- Attendees and organizer are read-only — EventKit does not allow modifying attendee lists
- Subscribed calendars are read-only — Cannot create or modify events in subscribed calendars
- Birthday calendars are read-only — The Birthdays calendar is auto-generated
- macOS only — EventKit is an Apple framework; ical exits gracefully on other platforms
- Date ranges required — EventKit requires bounded queries; ical does not support unbounded event fetches