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
35 changes: 35 additions & 0 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreLocation
import EventKit
import Foundation

Expand Down Expand Up @@ -103,6 +104,13 @@ public actor RemindersStore {
if let dueDate = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
}

// Handle location-based alarm
if let locationTrigger = draft.location {
let alarm = try await createLocationAlarm(from: locationTrigger)
reminder.addAlarm(alarm)
}

try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
Expand All @@ -117,6 +125,33 @@ public actor RemindersStore {
)
}

private func createLocationAlarm(from trigger: LocationTrigger) async throws -> EKAlarm {
let structuredLocation: EKStructuredLocation

if let lat = trigger.latitude, let lon = trigger.longitude {
// Use provided coordinates
structuredLocation = EKStructuredLocation(title: trigger.address)
structuredLocation.geoLocation = CLLocation(latitude: lat, longitude: lon)
} else {
// Geocode the address
let geocoder = CLGeocoder()
let placemarks = try await geocoder.geocodeAddressString(trigger.address)
guard let placemark = placemarks.first, let location = placemark.location else {
throw RemindCoreError.operationFailed("Could not geocode address: \(trigger.address)")
}
structuredLocation = EKStructuredLocation(title: trigger.address)
structuredLocation.geoLocation = location
}

structuredLocation.radius = trigger.radius

let alarm = EKAlarm()
alarm.structuredLocation = structuredLocation
alarm.proximity = trigger.proximity == .arriving ? .enter : .leave

return alarm
}

public func updateReminder(id: String, update: ReminderUpdate) async throws -> ReminderItem {
let reminder = try reminder(withID: id)

Expand Down
37 changes: 36 additions & 1 deletion Sources/RemindCore/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,52 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
}
}

public enum LocationProximity: String, Codable, CaseIterable, Sendable {
case arriving = "arriving"
case leaving = "leaving"
}

public struct LocationTrigger: Sendable {
public let address: String
public let latitude: Double?
public let longitude: Double?
public let radius: Double // in meters
public let proximity: LocationProximity

public init(
address: String,
latitude: Double? = nil,
longitude: Double? = nil,
radius: Double = 100,
proximity: LocationProximity = .arriving
) {
self.address = address
self.latitude = latitude
self.longitude = longitude
self.radius = radius
self.proximity = proximity
}
}

public struct ReminderDraft: Sendable {
public let title: String
public let notes: String?
public let dueDate: Date?
public let priority: ReminderPriority
public let location: LocationTrigger?

public init(title: String, notes: String?, dueDate: Date?, priority: ReminderPriority) {
public init(
title: String,
notes: String?,
dueDate: Date?,
priority: ReminderPriority,
location: LocationTrigger? = nil
) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.priority = priority
self.location = location
}
}

Expand Down
32 changes: 31 additions & 1 deletion Sources/remindctl/Commands/AddCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,30 @@ enum AddCommand {
help: "none|low|medium|high",
parsing: .singleValue
),
.make(
label: "location",
names: [.long("location")],
help: "Location address for geofence trigger",
parsing: .singleValue
),
.make(
label: "radius",
names: [.long("radius")],
help: "Geofence radius in meters (default: 100)",
parsing: .singleValue
),
],
flags: [
.make(label: "leaving", names: [.long("leaving")], help: "Trigger when leaving location (default: arriving)"),
]
)
),
usageExamples: [
"remindctl add \"Buy milk\"",
"remindctl add --title \"Call mom\" --list Personal --due tomorrow",
"remindctl add \"Review docs\" --priority high",
"remindctl add \"Check mailbox\" --location \"50 West St, New York, NY\"",
"remindctl add \"Lock up\" --location \"Home\" --leaving",
]
) { values, runtime in
let titleOption = values.option("title")
Expand All @@ -56,10 +73,23 @@ enum AddCommand {
let notes = values.option("notes")
let dueValue = values.option("due")
let priorityValue = values.option("priority")
let locationValue = values.option("location")
let isLeaving = values.flag("leaving")
let radiusValue = values.option("radius")

let dueDate = try dueValue.map(CommandHelpers.parseDueDate)
let priority = try priorityValue.map(CommandHelpers.parsePriority) ?? .none

// Build location trigger if specified
let locationTrigger: LocationTrigger?
if let address = locationValue {
let radius = radiusValue.flatMap { Double($0) } ?? 100.0
let proximity: LocationProximity = isLeaving ? .leaving : .arriving
locationTrigger = LocationTrigger(address: address, radius: radius, proximity: proximity)
} else {
locationTrigger = nil
}

let store = RemindersStore()
try await store.requestAccess()

Expand All @@ -73,7 +103,7 @@ enum AddCommand {
throw RemindCoreError.operationFailed("No default list found. Specify --list.")
}

let draft = ReminderDraft(title: title, notes: notes, dueDate: dueDate, priority: priority)
let draft = ReminderDraft(title: title, notes: notes, dueDate: dueDate, priority: priority, location: locationTrigger)
let reminder = try await store.createReminder(draft, listName: targetList)
OutputRenderer.printReminder(reminder, format: runtime.outputFormat)
}
Expand Down