# ical2rem Convert iCalendar (`.ics`) files into [Remind](https://dianne.skoll.ca/projects/remind/) format. Designed to work on real-world calendar data (Google Calendar, SOGo, Outlook/Teams exports). Full RFC 5545 coverage is intentionally out of scope — see [Limitations](#limitations). ## Features ### Event types supported | iCalendar pattern | Remind output | |---|---| | All-day single | `REM YYYY-MM-DD MSG …` | | All-day multi-day | `REM date THROUGH date MSG …` | | Timed event (UTC, local, TZID) | `REM date AT HH:MM DURATION HH:MM MSG …` | | Yearly recurrence (`FREQ=YEARLY`) | `REM MMM DD MSG …` | | Weekly recurrence (`FREQ=WEEKLY`) | `REM Mon Wed YYYY-MM-DD *7 UNTIL … MSG …` | | Daily recurrence (`FREQ=DAILY`) | `REM date *N UNTIL … MSG …` | | Monthly by day-of-month (`BYMONTHDAY`) | `REM N FROM … UNTIL … MSG …` | | Monthly by nth weekday (`BYDAY=nWD`) | `REM Wd N FROM … UNTIL … MSG …` | | Exceptions (`EXDATE`) | `PUSH-OMIT-CONTEXT` + `OMIT` + `SKIP` + `POP-OMIT-CONTEXT` | | Overrides (`RECURRENCE-ID`) | OMIT on original slot + single `REM` with new date/time | | Cancellations (`STATUS:CANCELLED`) | OMIT only, no REM | | Duration via `DURATION` instead of `DTEND` | Handled transparently | | Alarms (`VALARM ACTION:DISPLAY/AUDIO`) | `AT … +n` / `++n` / `SCHED` / `WARN` | | Conference URL (Google Meet, Teams) | `INFO "Url: …"` | | Windows timezone names (Outlook) | Resolved to IANA via CLDR table | ### Metadata lines Each reminder can include optional `INFO` lines (all suppressible via flags): ``` REM \ INFO "UID: …" \ INFO "Calendar: MYCAL" \ INFO "Location: …" \ INFO "Description: …" \ INFO "Url: …" \ … ``` ## Installation ### From source Requires [opam](https://opam.ocaml.org/) and OCaml >= 5.0. ```bash git clone https://git.donadeo.net/pdonadeo/ical2rem cd ical2rem opam install . --deps-only dune build dune install ``` The binary is installed as `ical2rem`. ### Dependencies - [`icalendar`](https://github.com/roburio/icalendar) — iCal parser - [`timedesc`](https://github.com/daypack-dev/timere) + `timedesc-tzdb.full` + `timedesc-tzlocal.unix` — date/time + timezone handling - [`cmdliner`](https://erratique.ch/software/cmdliner) — CLI - [`ppx_deriving.show`](https://github.com/ocaml-ppx/ppx_deriving) — debug printers ## Usage ``` ical2rem [OPTION]… FILE… ``` Output goes to stdout and can be redirected to a `.rem` file. Pass `-` as a filename to read from standard input. `-` may appear at most once, but can be freely mixed with regular files: ```bash curl https://example.com/calendar.ics | ical2rem - > calendar.rem ical2rem work.ics - personal.ics > all.rem ``` ### Examples Convert a single calendar: ```bash ical2rem personal.ics > personal.rem ``` Convert multiple calendars into one file: ```bash ical2rem work.ics personal.ics > all.rem ``` Sort output chronologically (oldest first): ```bash ical2rem --sort asc personal.ics > personal.rem ``` Run on a server in UTC, output in a specific timezone: ```bash ical2rem --timezone Europe/Rome personal.ics > personal.rem ``` Strip all metadata lines from output: ```bash ical2rem --no-uuid --no-source --no-location --no-description --no-conference-url personal.ics ``` Use stdin as input calendar: ```bash curl https://example.com/calendar.ics | ical2rem - > calendar.rem ``` Override calendar name in `INFO` lines (single file only): ```bash ical2rem --source "Work" work.ics > work.rem ``` Show diagnostic warnings (skipped events, unsupported rules, etc.): ```bash ical2rem --verbose personal.ics > personal.rem ``` ### All options | Option | Description | |---|---| | `FILE…` | One or more `.ics` files to convert; use `-` for standard input (at most once) | | `-z`, `--timezone TZ` | Target timezone for output (default: local) | | `--sort asc\|desc\|original` | Sort order by date (default: `desc`); `original` preserves processing order (sorted by UID within each file, last file first) | | `--source NAME` | Override calendar name (single file only) | | `-v`, `--verbose` | Print diagnostic messages on stderr | | `--no-uuid` | Omit `INFO "UID: …"` lines | | `--no-source` | Omit `INFO "Calendar: …"` lines | | `--no-location` | Omit `INFO "Location: …"` lines | | `--no-description` | Omit `INFO "Description: …"` lines | | `--no-conference-url` | Omit `INFO "Url: …"` lines | | `--version` | Print version and exit | | `--help` | Print help and exit | ## Output format examples ### All-day single event ``` REM \ INFO "UID: abc@google.com" \ INFO "Calendar: PERSONAL" \ 2026-06-15 MSG Birthday party ``` ### Timed event with alarm ``` REM \ INFO "UID: xyz@google.com" \ INFO "Calendar: WORK" \ 2026-05-27 AT 10:00 +30 DURATION 01:00 MSG %"Team standup%" (%b %3) ``` ### Weekly recurrence with exception ``` PUSH-OMIT-CONTEXT OMIT 6 Oct 2025 REM \ INFO "UID: …" \ INFO "Calendar: WORK" \ Mon 2025-09-01 *7 UNTIL 2025-12-31 SKIP AT 09:00 +15 MSG %"Standup%" (%b %3) POP-OMIT-CONTEXT ``` ### Recurring event with modified occurrence ``` PUSH-OMIT-CONTEXT OMIT 3 Jun 2015 REM \ INFO "UID: …" \ INFO "Calendar: PERSONAL" \ Wed 1 FROM 2015-06-03 UNTIL 2015-06-09 SKIP MSG Monthly payment POP-OMIT-CONTEXT REM \ INFO "UID: …" \ INFO "Calendar: PERSONAL" \ 2015-06-09 MSG Monthly payment ``` ## Limitations ### Recurrence rules (`RRULE`) - `FREQ=WEEKLY INTERVAL=N` (N > 1): supported (maps to `*7N`). - `FREQ=MONTHLY INTERVAL=N` (N > 1): **not supported**, event skipped with warning. - `FREQ=MONTHLY BYDAY=WD` without position (every Monday of the month): **not supported**. - `BYSETPOS`: **not supported**. - `FREQ=YEARLY` with `BYMONTH`/`BYDAY` variants: **not supported**, only simple yearly (same day every year). - `RDATE` (additional isolated dates): **not supported**, warning emitted. ### Alarms (`VALARM`) - `ACTION:EMAIL`: ignored silently. - `TRIGGER;VALUE=DATE-TIME` (absolute datetime trigger): ignored silently. - Positive triggers (after the event): ignored silently. - `RELATED=END` triggers: the offset is applied as if it were `RELATED=START` (no warning emitted). - `REPEAT`/`DURATION` (repeating alarms) on all-day events: ignored. ### Other - `ATTENDEE`, `CATEGORIES`, visibility/status fields: ignored. - `VTIMEZONE` components: ignored; TZID names are resolved directly via IANA or the built-in Windows→IANA CLDR table.