214 lines
6.3 KiB
Markdown
214 lines
6.3 KiB
Markdown
# 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 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](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%" (%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.
|
|
|