"""Apple Calendar tools.""" from datetime import datetime, timedelta from helpers import run_applescript, safe_applescript_string, parse_applescript_date, today_str def list_calendars() -> list[dict]: """List all calendars.""" script = ''' tell application "Calendar" set output to "" repeat with c in calendars set cName to name of c set cDesc to "" try set cDesc to description of c end try set output to output & cName & "|||" & cDesc & linefeed end repeat return output end tell''' raw = run_applescript(script) results = [] for line in raw.strip().split("\n"): if not line.strip(): continue parts = line.split("|||") results.append({ "name": parts[0].strip(), "description": parts[1].strip() if len(parts) > 1 else "", }) return results def get_events(start_date: str | None = None, end_date: str | None = None, calendar_name: str | None = None, days: int = 7) -> list[dict]: """Get calendar events in a date range. Args: start_date: ISO date (YYYY-MM-DD). Defaults to today. end_date: ISO date (YYYY-MM-DD). Defaults to start_date + days. calendar_name: Filter to a specific calendar. days: Number of days to look ahead (used if end_date not provided). """ if not start_date: start_date = today_str() if not end_date: start_dt = datetime.fromisoformat(start_date) end_dt = start_dt + timedelta(days=days) end_date = end_dt.strftime("%Y-%m-%d") cal_filter = "" if calendar_name: safe_cal = safe_applescript_string(calendar_name) cal_filter = f'set cals to {{calendar "{safe_cal}"}}' else: cal_filter = "set cals to calendars" script = f''' tell application "Calendar" {cal_filter} set startD to current date set year of startD to {start_date[:4]} set month of startD to {int(start_date[5:7])} set day of startD to {int(start_date[8:10])} set hours of startD to 0 set minutes of startD to 0 set seconds of startD to 0 set endD to current date set year of endD to {end_date[:4]} set month of endD to {int(end_date[5:7])} set day of endD to {int(end_date[8:10])} set hours of endD to 23 set minutes of endD to 59 set seconds of endD to 59 set output to "" repeat with c in cals set calName to name of c set evts to (every event of c whose start date >= startD and start date <= endD) repeat with e in evts set eSummary to summary of e set eStart to (start date of e) as string set eEnd to (end date of e) as string set eLoc to "" try set eLoc to location of e end try set eAllDay to allday event of e as string set eNotes to "" try set eNotes to description of e on error set eNotes to "" end try set eId to uid of e set output to output & calName & ">>>" & eSummary & "|||" & eStart & "|||" & eEnd & "|||" & eLoc & "|||" & eAllDay & "|||" & eNotes & "|||" & eId & linefeed end repeat end repeat return output end tell''' raw = run_applescript(script, timeout=60) results = [] for line in raw.strip().split("\n"): if not line.strip(): continue parts = line.split("|||") if len(parts) < 7: continue name_part = parts[0] cal = None if ">>>" in name_part: cal, name_part = name_part.split(">>>", 1) results.append({ "summary": name_part.strip(), "calendar": cal.strip() if cal else None, "start": parse_applescript_date(parts[1].strip()), "end": parse_applescript_date(parts[2].strip()), "location": parts[3].strip() or None, "all_day": parts[4].strip() == "true", "notes": parts[5].strip() or None, "id": parts[6].strip(), }) return results def search_events(query: str, days_back: int = 30, days_forward: int = 30) -> list[dict]: """Search for events by summary text.""" start_dt = datetime.now() - timedelta(days=days_back) end_dt = datetime.now() + timedelta(days=days_forward) start_date = start_dt.strftime("%Y-%m-%d") end_date = end_dt.strftime("%Y-%m-%d") safe_query = safe_applescript_string(query.lower()) script = f''' tell application "Calendar" set startD to current date set year of startD to {start_date[:4]} set month of startD to {int(start_date[5:7])} set day of startD to {int(start_date[8:10])} set hours of startD to 0 set minutes of startD to 0 set seconds of startD to 0 set endD to current date set year of endD to {end_date[:4]} set month of endD to {int(end_date[5:7])} set day of endD to {int(end_date[8:10])} set hours of endD to 23 set minutes of endD to 59 set seconds of endD to 59 set output to "" repeat with c in calendars set calName to name of c set evts to (every event of c whose start date >= startD and start date <= endD) repeat with e in evts set eSummary to summary of e if eSummary contains "{safe_query}" then set eStart to (start date of e) as string set eEnd to (end date of e) as string set eId to uid of e set output to output & calName & ">>>" & eSummary & "|||" & eStart & "|||" & eEnd & "|||" & eId & linefeed end if end repeat end repeat return output end tell''' raw = run_applescript(script, timeout=60) results = [] for line in raw.strip().split("\n"): if not line.strip(): continue parts = line.split("|||") if len(parts) < 4: continue name_part = parts[0] cal = None if ">>>" in name_part: cal, name_part = name_part.split(">>>", 1) results.append({ "summary": name_part.strip(), "calendar": cal.strip() if cal else None, "start": parse_applescript_date(parts[1].strip()), "end": parse_applescript_date(parts[2].strip()), "id": parts[3].strip(), }) return results def create_event(summary: str, start_date: str, end_date: str | None = None, calendar_name: str | None = None, location: str | None = None, notes: str | None = None, all_day: bool = False) -> dict: """Create a calendar event. Args: summary: Event title. start_date: ISO datetime (e.g., 2026-04-15T10:00:00). end_date: ISO datetime. Defaults to 1 hour after start. calendar_name: Which calendar. Defaults to first calendar. location: Event location. notes: Event notes/description. all_day: Whether this is an all-day event. """ if not end_date: start_dt = datetime.fromisoformat(start_date) end_dt = start_dt + timedelta(hours=1) end_date = end_dt.isoformat() start_dt = datetime.fromisoformat(start_date) end_dt = datetime.fromisoformat(end_date) safe_summary = safe_applescript_string(summary) cal_line = f'set theCal to calendar "{safe_applescript_string(calendar_name)}"' if calendar_name else "set theCal to first calendar" loc_line = "" if location: loc_line = f'set location of newEvent to "{safe_applescript_string(location)}"' notes_line = "" if notes: notes_line = f'set description of newEvent to "{safe_applescript_string(notes)}"' script = f''' tell application "Calendar" {cal_line} set startD to current date set year of startD to {start_dt.year} set month of startD to {start_dt.month} set day of startD to {start_dt.day} set hours of startD to {start_dt.hour} set minutes of startD to {start_dt.minute} set seconds of startD to 0 set endD to current date set year of endD to {end_dt.year} set month of endD to {end_dt.month} set day of endD to {end_dt.day} set hours of endD to {end_dt.hour} set minutes of endD to {end_dt.minute} set seconds of endD to 0 set newEvent to make new event at end of events of theCal with properties {{summary:"{safe_summary}", start date:startD, end date:endD, allday event:{str(all_day).lower()}}} {loc_line} {notes_line} return uid of newEvent end tell''' event_id = run_applescript(script) return {"id": event_id.strip(), "summary": summary, "start": start_date, "end": end_date, "created": True} def delete_event(event_summary: str, event_date: str | None = None, calendar_name: str | None = None) -> dict: """Delete a calendar event by summary (and optionally date).""" safe_summary = safe_applescript_string(event_summary) cal_source = f'calendar "{safe_applescript_string(calendar_name)}"' if calendar_name else "first calendar" if event_date: dt = datetime.fromisoformat(event_date) date_filter = f''' set targetD to current date set year of targetD to {dt.year} set month of targetD to {dt.month} set day of targetD to {dt.day} set hours of targetD to 0 set minutes of targetD to 0 set seconds of targetD to 0 set nextD to targetD + (1 * days) set evts to (every event of theCal whose summary is "{safe_summary}" and start date >= targetD and start date < nextD)''' else: date_filter = f'set evts to (every event of theCal whose summary is "{safe_summary}")' script = f''' tell application "Calendar" set theCal to {cal_source} {date_filter} if (count of evts) > 0 then delete item 1 of evts return "deleted" else return "not_found" end if end tell''' result = run_applescript(script) if result == "deleted": return {"summary": event_summary, "deleted": True} raise RuntimeError(f"Event '{event_summary}' not found")