Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A native macOS command-line tool for managing Calendar events and Reminders usin
## Features

- List, create, and delete calendar events
- **Recurring events** - Create events with daily, weekly, monthly, or yearly recurrence rules
- List, create, complete, and delete reminders
- **Calendar aliases** - Use friendly names instead of long IDs
- JSON output for easy parsing and scripting
Expand Down Expand Up @@ -209,6 +210,82 @@ ekctl add event \
--all-day
```

#### Recurring Events

Create events that repeat on a schedule using `--frequency` and related options:

```bash
# Daily standup
ekctl add event \
--calendar work \
--title "Daily Standup" \
--start "2026-02-01T09:00:00Z" \
--end "2026-02-01T09:15:00Z" \
--frequency daily

# Weekly team meeting every Monday and Wednesday
ekctl add event \
--calendar work \
--title "Team Sync" \
--start "2026-02-03T14:00:00Z" \
--end "2026-02-03T15:00:00Z" \
--frequency weekly \
--days-of-the-week mon,wed

# Biweekly 1:1 on Fridays
ekctl add event \
--calendar work \
--title "1:1 with Manager" \
--start "2026-02-07T11:00:00Z" \
--end "2026-02-07T11:30:00Z" \
--frequency weekly \
--interval 2 \
--days-of-the-week fri

# Monthly on the 15th, ending after 6 occurrences
ekctl add event \
--calendar work \
--title "Monthly Report Due" \
--start "2026-02-15T09:00:00Z" \
--end "2026-02-15T10:00:00Z" \
--frequency monthly \
--days-of-the-month 15 \
--recurrence-count 6

# Last Friday of every month
ekctl add event \
--calendar work \
--title "Month-End Review" \
--start "2026-01-30T16:00:00Z" \
--end "2026-01-30T17:00:00Z" \
--frequency monthly \
--days-of-the-week -1fri

# Yearly event ending on a specific date
ekctl add event \
--calendar personal \
--title "Annual Checkup" \
--start "2026-03-10T10:00:00Z" \
--end "2026-03-10T11:00:00Z" \
--frequency yearly \
--recurrence-end-date "2030-12-31T23:59:59Z"
```

**Recurrence options:**

| Option | Description |
|--------|-------------|
| `--frequency` | **Required for recurrence.** `daily`, `weekly`, `monthly`, or `yearly` |
| `--interval` | Repeat every N periods (default: 1). E.g., `2` with `weekly` = every 2 weeks |
| `--days-of-the-week` | Comma-separated days: `mon,tue,wed,thu,fri,sat,sun`. Prefix with week number for monthly rules: `1mon` (first Monday), `-1fri` (last Friday) |
| `--days-of-the-month` | Comma-separated days (1–31). Negative values count from end: `-1` = last day |
| `--months-of-the-year` | Comma-separated months (1–12) |
| `--weeks-of-the-year` | Comma-separated weeks (1–53 or negative from end) |
| `--days-of-the-year` | Comma-separated days (1–366 or negative from end) |
| `--set-positions` | Filter expanded recurrence rules (e.g., `1,-1` for first and last) |
| `--recurrence-end-date` | Stop recurring after this date (ISO 8601) |
| `--recurrence-count` | Stop recurring after this many occurrences |

Output:
```json
{
Expand Down
53 changes: 51 additions & 2 deletions Sources/Ekctl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct Ekctl: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "ekctl",
abstract: "A command-line tool for managing macOS Calendar events and Reminders using EventKit.",
version: "1.2.0",
version: "1.3.0",
subcommands: [List.self, Show.self, Add.self, Delete.self, Complete.self, Alias.self],
defaultSubcommand: List.self
)
Expand Down Expand Up @@ -172,6 +172,36 @@ struct AddEvent: ParsableCommand {
@Flag(name: .long, help: "Mark as all-day event.")
var allDay: Bool = false

@Option(name: .long, help: "Recurrence frequency: daily, weekly, monthly, or yearly.")
var frequency: String?

@Option(name: .long, help: "Recurrence interval (default 1). E.g., 2 with weekly = every 2 weeks.")
var interval: Int?

@Option(name: .long, parsing: .unconditional, help: "Days of the week (comma-separated). Plain: mon,tue,fri. With week number: 1mon (first Monday), -1fri (last Friday).")
var daysOfTheWeek: String?

@Option(name: .long, parsing: .unconditional, help: "Days of the month (comma-separated, 1-31 or negative from end: -1 = last day).")
var daysOfTheMonth: String?

@Option(name: .long, help: "Months of the year (comma-separated, 1-12).")
var monthsOfTheYear: String?

@Option(name: .long, parsing: .unconditional, help: "Weeks of the year (comma-separated, 1-53 or negative from end).")
var weeksOfTheYear: String?

@Option(name: .long, parsing: .unconditional, help: "Days of the year (comma-separated, 1-366 or negative from end).")
var daysOfTheYear: String?

@Option(name: .long, parsing: .unconditional, help: "Set positions to filter expanded rules (comma-separated, e.g., 1,-1).")
var setPositions: String?

@Option(name: .long, help: "End recurrence after this date (ISO8601 format).")
var recurrenceEndDate: String?

@Option(name: .long, help: "End recurrence after this many occurrences.")
var recurrenceCount: Int?

func run() throws {
let manager = EventKitManager()
try manager.requestAccess()
Expand All @@ -185,6 +215,15 @@ struct AddEvent: ParsableCommand {
throw ExitCode.failure
}

var parsedRecurrenceEndDate: Date?
if let recurrenceEndDate = recurrenceEndDate {
guard let parsed = ISO8601DateFormatter().date(from: recurrenceEndDate) else {
print(JSONOutput.error("Invalid --recurrence-end-date format. Use ISO8601.").toJSON())
throw ExitCode.failure
}
parsedRecurrenceEndDate = parsed
}

let calendarID = ConfigManager.resolveAlias(calendar)
let result = manager.addEvent(
calendarID: calendarID,
Expand All @@ -193,7 +232,17 @@ struct AddEvent: ParsableCommand {
endDate: endDate,
location: location,
notes: notes,
allDay: allDay
allDay: allDay,
frequency: frequency,
interval: interval ?? 1,
daysOfTheWeek: daysOfTheWeek,
daysOfTheMonth: daysOfTheMonth,
monthsOfTheYear: monthsOfTheYear,
weeksOfTheYear: weeksOfTheYear,
daysOfTheYear: daysOfTheYear,
setPositions: setPositions,
recurrenceEndDate: parsedRecurrenceEndDate,
recurrenceCount: recurrenceCount
)
print(result.toJSON())
}
Expand Down
Loading