%b %3 shows both day offset and verbose time remaining, giving more context when the reminder fires the day before the event.
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
icalendar— iCal parsertimedesc+timedesc-tzdb.full+timedesc-tzlocal.unix— date/time + timezone handlingcmdliner— CLIppx_deriving.show— 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:
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=WDwithout position (every Monday of the month): not supported.BYSETPOS: not supported.FREQ=YEARLYwithBYMONTH/BYDAYvariants: 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=ENDtriggers: the offset is applied as if it wereRELATED=START(no warning emitted).REPEAT/DURATION(repeating alarms) on all-day events: ignored.
Other
ATTENDEE,CATEGORIES, visibility/status fields: ignored.VTIMEZONEcomponents: ignored; TZID names are resolved directly via IANA or the built-in Windows→IANA CLDR table.