A lightweight Rust tool that fetches, filters, modifies, and merges iCal calendars from the web, exposing them via HTTP endpoints.
Configuration files can be in either JSON or TOML format. If no config is specified, the tool will auto-detect config.toml or config.json in the current directory.
The configuration defines a set of virtual calendars. Each has an ID which exposes the calendar at the /ical/<id> http endpoint. Each virtual calendar is composed of a set of sources which are either calendars available from a url (http, https, webcal and webcals protocols supported) or an existing virtual calendar can be used as a source.
Processing steps are applied to every event, these steps can modify and potentially reject events. Each calendar source can define a set of steps to be applied to every event from that source and then a set of global steps can be defined for the virtual calendar which will be applied to every event from every source for that calendar. The global steps apply after the steps for each source have been applied. Steps are applied sequentially and remaining steps are skipped if a step rejects an event.
See the example configuration files for complete, documented examples:
- JSON format: config.example.json
- TOML format: config.example.toml
Allow - Only keep events matching patterns:
{
"type": "allow",
"patterns": ["(?i)meeting", "(?i)standup"],
"mode": "any",
"fields": ["summary", "description"]
}patterns: Regex patterns to match (required)mode:"any"(default) or"all"- whether any or all patterns must matchfields: Fields to search (defaults to["summary", "description"])
Deny - Reject events matching patterns:
{
"type": "deny",
"patterns": ["(?i)optional", "(?i)cancelled"],
"mode": "any",
"fields": ["summary"]
}- Same parameters as allow
Replace - Modify event text with regex:
{
"type": "replace",
"pattern": "^Meeting:",
"replacement": "[WORK] ",
"field": "summary"
}pattern: Regex pattern to find (required)replacement: Replacement text, supports capture groups like$1(defaults to"")field: Field to modify -"summary","description", or"location"(defaults to"summary")
Case - Transform text case:
{
"type": "case",
"transform": "title",
"field": "summary"
}transform:"lower","upper","sentence", or"title"(required)field: Field to transform (defaults to"summary")
Strip - Remove event components:
{
"type": "strip",
"field": "reminder"
}field:"reminder"(only supported field currently)
Run the server (auto-detects config.toml or config.json):
cargo runOr specify a config file explicitly:
cargo run -- -c config.jsonRun with custom bind/port:
cargo run -- serve --bind 0.0.0.0 --port 9090Show events from a calendar:
cargo run -- show my-calendarOutput calendar as iCal format:
cargo run -- ical my-calendar > output.icsAccess merged calendars via HTTP:
curl http://localhost:8080/ical/my-calendarUsing docker run:
docker run -d \
-p 8080:8080 \
-v $(pwd)/config.toml:/app/config.toml:ro \
-e RUST_LOG=ical_merge=info \
ghcr.io/mossop/ical-merge:latestUsing docker-compose:
docker-compose up -dThe docker-compose.yml is already configured to:
- Mount your local config file
- Expose port 8080
- Auto-restart on failure
Simply edit your config file (config.toml or config.json) and save - changes are automatically detected and applied within ~2 seconds. No server restart needed!
CLI arguments can also be set via environment variables:
# Specify config file (optional - auto-detects config.toml or config.json if not set)
export ICAL_MERGE_CONFIG=/path/to/config.toml
# Override server settings
export ICAL_MERGE_BIND=0.0.0.0
export ICAL_MERGE_PORT=9090
cargo runRun all tests:
cargo testOr with mise:
mise run testThe project includes Docker-based integration tests that verify config hot-reload works correctly in containerized environments. These tests use testcontainers to:
- Build and start the app in a Docker container
- Mount a config file as a bind mount
- Verify initial configuration works
- Modify the config file on the host
- Verify the container detects changes and reloads configuration
To run only these tests:
cargo test --test docker_config_reloadNote: These tests require Docker to be running and take longer to execute because they build the Docker image and start containers. The tests automatically build the Docker image using the project's Dockerfile before running.
MIT