Architecture
How rem achieves sub-200ms macOS Reminders operations with a single Go binary — using go-eventkit to bridge EventKit via cgo, eliminating AppleScript IPC overhead entirely.
Overview
rem uses go-eventkit (github.com/BRO3886/go-eventkit) for all reads and writes — including reminder CRUD and list CRUD — via EventKit’s cgo bridge. AppleScript is only used for flagged operations and default list name queries that EventKit doesn’t support.
Main path: go-eventkit
All reminder read and write operations go through go-eventkit (github.com/BRO3886/go-eventkit/reminders), which provides native EventKit bindings via cgo + Objective-C.
How it works
- rem creates a
reminders.Clientviareminders.New()— this requests TCC authorization - Read operations (e.g.,
client.Reminders(opts...)) call into cgo → Objective-C → EventKit - Write operations (e.g.,
client.CreateReminder(input)) go through the same path - Results are serialized as JSON strings across the cgo boundary and parsed into Go types
- The entire round-trip completes in under 200ms for both reads and writes
Key implementation details
Store initialization happens once via dispatch_once inside go-eventkit:
static EKEventStore *store = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
store = [[EKEventStore alloc] init];
// Request TCC authorization
});
ARC is mandatory. go-eventkit’s cgo CFLAGS include -fobjc-arc. Without ARC, objects created inside completion handlers are released prematurely, causing silent empty results or crashes.
Why not JXA or AppleScript for reads?
JXA (JavaScript for Automation) was rem’s original read layer. Each property access is an Apple Event — a cross-process IPC call to the Reminders app. For 224 reminders with 11 properties, that’s thousands of IPC calls serialized through a single pipe. Result: 42-60 seconds.
EventKit is an in-process framework — direct memory access to the reminder store with no IPC. Result: 0.13 seconds for the same dataset. That’s a 462x speedup.
AppleScript fallback
Two operations still use AppleScript via osascript:
- Flag/unflag reminders — EventKit doesn’t expose the
flaggedproperty - Default list name — not exposed by go-eventkit
The flagged exception
EventKit’s EKReminder does not expose a flagged property. When the --flagged filter is active, rem falls back to JXA to fetch flagged reminder IDs. This is the only remaining slow path (~3-4 seconds) but is rarely used. Flag/unflag write operations use AppleScript.
Single binary
go-eventkit’s Objective-C code compiles directly into the Go binary via cgo. go build detects the .m files, invokes Clang to compile the Objective-C, and links the EventKit and Foundation frameworks. The result is a single binary with no external dependencies.
This means go install github.com/BRO3886/rem/cmd/rem@latest works out of the box — no separate compilation step, no helper binaries to distribute.
Project structure
internal/
├── service/ # Service layer (go-eventkit + AppleScript for flagged only)
│ ├── executor.go # Runs osascript (flagged ops, default list name)
│ ├── reminders.go # ReminderService wrapping go-eventkit
│ ├── lists.go # ListService wrapping go-eventkit
│ └── parser.go # URL extraction from notes
│
├── reminder/ # Domain models
│ └── model.go # Reminder, List, Priority types
│
├── parser/ # Natural language date parser
│ └── date.go # 20+ patterns, no external deps
│
├── export/ # Import/export
│ ├── json.go # JSON format
│ └── csv.go # CSV format
│
└── ui/ # Terminal output
└── output.go # Table, JSON, plain formatters
Dependencies
rem uses four external Go dependencies:
| Package | Purpose |
|---|---|
BRO3886/go-eventkit | Native EventKit bindings (cgo + ObjC, reads AND writes) |
spf13/cobra | CLI framework (commands, flags, help) |
olekukonko/tablewriter | Terminal table formatting |
fatih/color | Terminal colors |
System frameworks linked via cgo (through go-eventkit):
| Framework | Purpose |
|---|---|
EventKit | macOS native reminder store access |
Foundation | Objective-C runtime and utilities |
Design decisions
go-eventkit as a standalone library
The EventKit bridge was extracted from rem into a standalone Go library (github.com/BRO3886/go-eventkit). This provides:
- Reusability — other Go projects can use EventKit without rem
- Separation of concerns — rem is a thin CLI wrapper, go-eventkit handles all cgo/EventKit complexity
- Calendar support — go-eventkit also supports Calendar/Events, which rem doesn’t use
Custom date parser
Instead of using an external NL date library, rem includes a custom parser in internal/parser/. It handles 20+ patterns in ~250 lines of Go with deterministic behavior and no locale surprises.
Prefix-matched IDs
Reminder IDs are UUIDs in the format x-apple-reminder://UUID. rem strips the prefix and displays only the first 8 characters. Users can pass any unique prefix to commands — matching is case-insensitive.