Paolo Donadeo 3e106a0b44 fix: use %b %3 instead of %1 in timed alarm MSG
%b %3 shows both day offset and verbose time remaining, giving more
context when the reminder fires the day before the event.
2026-06-20 00:19:54 +02:00
2025-09-01 00:00:00 +02:00
2025-09-01 00:00:00 +02:00
2025-09-01 00:00:00 +02:00
2025-09-01 00:00:00 +02:00

ical2rem

Convert iCalendar (.ics) files into 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.

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 FROM … 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 and OCaml >= 5.0.

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

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:

curl https://example.com/calendar.ics | ical2rem - > calendar.rem
ical2rem work.ics - personal.ics > all.rem

Examples

Convert a single calendar:

ical2rem personal.ics > personal.rem

Convert multiple calendars into one file:

ical2rem work.ics personal.ics > all.rem

Sort output chronologically (oldest first):

ical2rem --sort asc personal.ics > personal.rem

Run on a server in UTC, output in a specific timezone:

ical2rem --timezone Europe/Rome personal.ics > personal.rem

Strip all metadata lines from output:

ical2rem --no-uuid --no-source --no-location --no-description --no-conference-url personal.ics

Use stdin as input calendar:

curl https://example.com/calendar.ics | ical2rem - > calendar.rem

Override calendar name in INFO lines (single file only):

ical2rem --source "Work" work.ics > work.rem

Show diagnostic warnings (skipped events, unsupported rules, etc.):

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%" (%1)

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%" (%1)
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.
Description
ical2rem reads iCalendar (.ics) files and produces reminders in the Remind format. It handles recurring events, exceptions (EXDATE/RECURRENCE-ID), alarms (VALARM), timezones, and Windows timezone names.
Readme MIT 999 KiB
2026-06-20 10:10:11 +00:00
Languages
OCaml 83.9%
Standard ML 14.2%
Dune 1.9%