From db9a7a2dc1e35991c6410a1cef1ddad1c0b3c155 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 27 Sep 2026 19:52:28 +0200 Subject: Load config with defaults, read and write per-event overrides Co-Authored-By: Claude Opus 5.5 --- cal-notif | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ test_cal_notif.py | 26 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/cal-notif b/cal-notif index e7bd244..3b4556d 100755 --- a/cal-notif +++ b/cal-notif @@ -32,6 +32,24 @@ from pathlib import Path from dateutil.rrule import rrulestr from icalendar import Calendar, vRecur +CONF_DIR = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser() / "cal-notif" +DEFAULTS = { + "calendars_dir": "~/.local/share/calendars", + "snooze": "5m", + "late": "10m", + "rofi_theme": "~/.config/rofi/udt/list.rasi", + "defaults": {"*": []}, + "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, + "say": "Tra {in}: {summary}", + "say_now": "Adesso: {summary}"}, +} +PICK_DAYS = 30 + # ---- durations ---- DUR = re.compile(r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?") @@ -84,6 +102,39 @@ def spoken_it(td): # ---- config ---- +def load_config(path=None): + path = path or CONF_DIR / "config.toml" + cfg = {k: dict(v) if isinstance(v, dict) else v for k, v in DEFAULTS.items()} + if path.exists(): + with open(path, "rb") as f: + user = tomllib.load(f) + for k, v in user.items(): + cfg[k] = {**cfg[k], **v} if k == "voice" else v + for k in ("snooze", "late"): # fail early on bad durations + parse_duration(cfg[k]) + for v in cfg["defaults"].values(): + for o in (v.get("offsets", []) if isinstance(v, dict) else v): + parse_duration(o) + return cfg + + +def load_overrides(path=None): + path = path or CONF_DIR / "overrides.toml" + if not path.exists(): + return {} + with open(path, "rb") as f: + return tomllib.load(f) + + +def save_overrides(ov, path=None): + path = path or CONF_DIR / "overrides.toml" + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + # JSON strings and string arrays are valid TOML, so no TOML writer is needed. + tmp.write_text("".join(f"{json.dumps(uid)} = {json.dumps(offs)}\n" + for uid, offs in sorted(ov.items()))) + os.replace(tmp, path) + # ---- calendars ---- # ---- alarms ---- diff --git a/test_cal_notif.py b/test_cal_notif.py index f025ff4..27f60ba 100644 --- a/test_cal_notif.py +++ b/test_cal_notif.py @@ -53,6 +53,32 @@ def test_durations(): assert cn.number_it(28) == "ventotto" and cn.number_it(45) == "quarantacinque" +def test_overrides_roundtrip(): + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "overrides.toml" + ov = {"a@example.org": ["1h", "10m"], 'q"uote@example.org': []} + cn.save_overrides(ov, p) + assert cn.load_overrides(p) == ov + assert cn.load_overrides(Path(d) / "missing.toml") == {} + + +def test_config(): + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "config.toml" + assert cn.load_config(p)["voice"]["lang"] == "it" # no file: defaults + p.write_text('snooze = "15m"\n[voice]\nlang = "en-us"\n' + '[defaults]\nbd = { offsets = ["1d"], voice = false }\n') + c = cn.load_config(p) + assert c["snooze"] == "15m" and c["late"] == "10m" + assert c["voice"]["lang"] == "en-us" and c["voice"]["lead_silence"] == 0.3 # merged + p.write_text('[defaults]\n"*" = ["soon"]\n') + try: + cn.load_config(p) + raise AssertionError + except ValueError: + pass + + if __name__ == "__main__": for name, fn in list(globals().items()): if name.startswith("test_"): -- cgit v1.2.3