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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
29 changes: 29 additions & 0 deletions Sources/reminder-cli/OutputFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ struct AlarmOutput: Codable {
let proximity: String? // "arriving", "leaving" for location-based alarms
}

struct ListOutput: Codable {
let name: String
let reminderCount: Int
let isDefault: Bool
}

struct RecurrenceRuleOutput: Codable {
let frequency: String
let interval: Int
Expand Down Expand Up @@ -321,6 +327,29 @@ struct OutputFormatter {
try outputYAML(data: reminders)
}

// MARK: - Lists Output

func outputLists(_ lists: [[String: Any]]) throws {
let codableLists = lists.map { dict -> ListOutput in
ListOutput(
name: dict["name"] as? String ?? "",
reminderCount: dict["reminderCount"] as? Int ?? 0,
isDefault: dict["isDefault"] as? Bool ?? false
)
}

switch format {
case .text:
break // handled by caller
case .json:
try outputJSON(data: codableLists, pretty: false)
case .prettyJson:
try outputJSON(data: codableLists, pretty: true)
case .yaml:
try outputYAML(data: codableLists)
}
}

// MARK: - Helper Methods

private func priorityIndicator(for priority: Int) -> String {
Expand Down
84 changes: 84 additions & 0 deletions Sources/reminder-cli/ReminderStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ enum ReminderStoreError: LocalizedError {
case noAvailableLists
case invalidDateFormat(String)
case invalidPriority(Int)
case listAlreadyExists(String)
case cannotDeleteList(String)

var errorDescription: String? {
switch self {
Expand All @@ -34,6 +36,10 @@ enum ReminderStoreError: LocalizedError {
return "Invalid date format: \(format). Use YYYY-MM-DD or YYYY-MM-DD HH:MM"
case .invalidPriority(let priority):
return "Invalid priority: \(priority). Use 0-9 (0=none, 1-4=high, 5=medium, 6-9=low)"
case .listAlreadyExists(let name):
return "List already exists: \(name)"
case .cannotDeleteList(let name):
return "Failed to delete list: \(name)"
}
}
}
Expand Down Expand Up @@ -190,6 +196,84 @@ class ReminderStore {
}
}

// MARK: - Lists Operations

func showAllLists(format: OutputFormat = .text) async throws {
let calendars = eventStore.calendars(for: .reminder)

if format == .text {
print("\n📋 Reminder Lists")
print("─────────────────────────────────────")

for calendar in calendars {
let predicate = eventStore.predicateForReminders(in: [calendar])
let reminders = try await fetchReminders(matching: predicate)
let incompleteCount = reminders.filter { !$0.isCompleted }.count
let isDefault = calendar == eventStore.defaultCalendarForNewReminders()
let defaultMark = isDefault ? " (default)" : ""
print(" \(calendar.title)\(defaultMark) — \(incompleteCount) reminder(s)")
}
print()
} else {
var listOutputs: [[String: Any]] = []
for calendar in calendars {
let predicate = eventStore.predicateForReminders(in: [calendar])
let reminders = try await fetchReminders(matching: predicate)
let incompleteCount = reminders.filter { !$0.isCompleted }.count
let isDefault = calendar == eventStore.defaultCalendarForNewReminders()
listOutputs.append([
"name": calendar.title,
"reminderCount": incompleteCount,
"isDefault": isDefault,
])
}

let formatter = OutputFormatter(format: format)
try formatter.outputLists(listOutputs)
}
}

func createList(name: String) throws {
if findCalendar(named: name) != nil {
throw ReminderStoreError.listAlreadyExists(name)
}

let newCalendar = EKCalendar(for: .reminder, eventStore: eventStore)
newCalendar.title = name

if let defaultCalendar = eventStore.defaultCalendarForNewReminders() {
newCalendar.source = defaultCalendar.source
} else if let source = eventStore.sources.first(where: { $0.sourceType == .calDAV }) {
newCalendar.source = source
} else if let source = eventStore.sources.first(where: { $0.sourceType == .local }) {
newCalendar.source = source
}

try eventStore.saveCalendar(newCalendar, commit: true)
print("✅ Created list: \(name)")
}

func deleteList(name: String, force: Bool) throws {
guard let calendar = findCalendar(named: name) else {
throw ReminderStoreError.listNotFound(name)
}

if !force {
print("Are you sure you want to delete list '\(calendar.title)' and all its reminders? [y/N]: ", terminator: "")
guard let response = readLine()?.lowercased(), response == "y" || response == "yes" else {
print("Cancelled.")
return
}
}

do {
try eventStore.removeCalendar(calendar, commit: true)
print("🗑️ Deleted list: \(name)")
} catch {
throw ReminderStoreError.cannotDeleteList(name)
}
}

// MARK: - Show Operation

func showReminder(identifier: String, format: OutputFormat = .text) async throws {
Expand Down
61 changes: 61 additions & 0 deletions Sources/reminder-cli/reminder_cli.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct ReminderCLI: AsyncParsableCommand {
version: "1.0.1",
subcommands: [
List.self,
Lists.self,
Show.self,
Create.self,
Update.self,
Expand Down Expand Up @@ -93,6 +94,66 @@ extension ReminderCLI {
}
}

// MARK: - Lists Command Group
extension ReminderCLI {
struct Lists: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Manage reminder lists",
subcommands: [ListAll.self, Create.self, Delete.self],
defaultSubcommand: ListAll.self
)

struct ListAll: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "Show all reminder lists"
)

@Option(name: .shortAndLong, help: "Output format (text, json, pretty-json, yaml)")
var format: OutputFormat = .text

mutating func run() async throws {
let store = ReminderStore()
try await store.requestAccess()
try await store.showAllLists(format: format)
}
}

struct Create: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Create a new reminder list"
)

@Argument(help: "The name of the list to create")
var name: String

mutating func run() async throws {
let store = ReminderStore()
try await store.requestAccess()
try store.createList(name: name)
}
}

struct Delete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Delete a reminder list"
)

@Argument(help: "The name of the list to delete")
var name: String

@Flag(name: .shortAndLong, help: "Skip confirmation")
var force: Bool = false

mutating func run() async throws {
let store = ReminderStore()
try await store.requestAccess()
try store.deleteList(name: name, force: force)
}
}
}
}

// MARK: - Show Command
extension ReminderCLI {
struct Show: AsyncParsableCommand {
Expand Down