aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-27 20:09:41 +0200
committerDanilo M. <danix@danix.xyz>2026-09-27 20:09:41 +0200
commitd6a988f86d84ae955140a42f91b811c48c3ea1c5 (patch)
tree7acf0577b357f0c27185c3f4323e7202678593ab
parentb928c3e5bdb01536b60d43f1bc3dd5a9ddb41057 (diff)
downloadcal-notif-d6a988f86d84ae955140a42f91b811c48c3ea1c5.tar.gz
cal-notif-d6a988f86d84ae955140a42f91b811c48c3ea1c5.zip
Add README and example config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--README.md52
-rw-r--r--config.example.toml25
2 files changed, 77 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..09eb80a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# cal-notif
+
+Calendar notifications for the desktop. Reads the `.ics` files vdirsyncer
+keeps in sync, shows a popup at each alarm (with a Snooze button) and says
+it out loud with a local Kokoro voice. Nothing leaves the machine.
+
+- Each event's own alarms (VALARM) set when it notifies.
+- Events without alarms can get per-calendar defaults, or per-event
+ alarms chosen from a rofi menu. A per-event choice replaces the event's
+ own alarms, and `none` silences it.
+
+## Requirements
+
+- Python 3.11+ with `icalendar`, `python-dateutil`, `numpy`, `kokoro-onnx`
+- Kokoro model files (`kokoro-v1.0.onnx`, `voices-v1.0.bin`)
+- `notify-send` and a notification server that supports actions
+- `aplay`, `rofi`
+- vdirsyncer with a `filesystem` storage for the calendars
+
+## Usage
+
+ cal-notif daemon # start from your compositor's autostart
+ cal-notif pick # bind to a key: set alarms for one event
+ cal-notif say TEXT # test the voice
+
+Config: `~/.config/cal-notif/config.toml`, see `config.example.toml`.
+Per-event alarms from `pick` go to `~/.config/cal-notif/overrides.toml`
+(`"<event UID>" = ["1h", "10m"]`), which can also be edited by hand. The
+daemon picks up changes to both files, and to the calendars, within 30 s.
+
+In the picker, type durations like `1d 2h` (one alarm) or `1d, 1h` (two
+alarms), `none` to silence the event, `reset` to go back to its own alarms.
+
+With Do Not Disturb on, the notification server may close the popup into
+its drawer without reporting an action, so that alarm can no longer be
+snoozed. The voice still plays.
+
+## Tests
+
+ /usr/bin/python3 test_cal_notif.py
+
+## License
+
+GPLv2 only, see `LICENSE`.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
diff --git a/config.example.toml b/config.example.toml
new file mode 100644
index 0000000..7fe4d8a
--- /dev/null
+++ b/config.example.toml
@@ -0,0 +1,25 @@
+# cal-notif example config. Copy to ~/.config/cal-notif/config.toml.
+# Every key is optional; these are the defaults unless marked "example".
+
+calendars_dir = "~/.local/share/calendars" # vdirsyncer filesystem storage
+snooze = "5m" # the popup's Snooze delay
+late = "10m" # deliver alarms missed by at most this much
+rofi_theme = "~/.config/rofi/udt/list.rasi"
+
+# Alarms for events that have no VALARM, per calendar. The calendar name is
+# its vdirsyncer `displayname`, else its directory name. Durations are
+# "<n>d <n>h <n>m"; a list gives several alarms.
+[defaults]
+"*" = [] # silent unless the event has a VALARM
+# birthdays = ["1d", "9h"] # example
+# work = { offsets = ["15m"], voice = false } # example: popup only
+
+[voice]
+enabled = true
+model = "/data/voice-models/kokoro/kokoro-v1.0.onnx"
+voices = "/data/voice-models/kokoro/voices-v1.0.bin"
+lang = "it"
+blend = { if_sara = 0.8, af_bella = 0.2 }
+lead_silence = 0.3 # raise if the first word gets clipped
+say = "Tra {in}: {summary}"
+say_now = "Adesso: {summary}"