diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-27 19:57:56 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-27 19:57:56 +0200 |
| commit | 16f92566380d46132e7026e22c3c79aed2ddfbbb (patch) | |
| tree | a581f088020e4e9f82d38805ad80618d99ea0438 | |
| parent | ff6736d6ae7a1274587394fba34da34bb2cb1d8a (diff) | |
| download | cal-notif-16f92566380d46132e7026e22c3c79aed2ddfbbb.tar.gz cal-notif-16f92566380d46132e7026e22c3c79aed2ddfbbb.zip | |
Add the daemon loop: rescan on change, notify, speak, snooze
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rwxr-xr-x | cal-notif | 58 |
1 files changed, 57 insertions, 1 deletions
@@ -321,12 +321,68 @@ def voice_ready(vcfg): # ---- daemon ---- +def notify(occ, now, cfg, voice): + when = f"{occ.start:%d.%m}" if occ.allday else f"{occ.start:%d.%m %H:%M}" + body = "\n".join(x for x in (when, occ.location) if x) + urgency = "critical" if now >= occ.start - timedelta(minutes=1) else "normal" + kids = [subprocess.Popen(["notify-send", "--wait", "--app-name=cal-notif", "-u", urgency, + "--action=snooze=Snooze", "--action=dismiss=Dismiss", + occ.summary or "(senza titolo)", body], + stdout=subprocess.PIPE, text=True)] + if voice and cal_entry(cfg, occ.cal)[1]: + kids.append(subprocess.Popen([sys.executable, __file__, "say", + announce(occ, now, cfg["voice"])])) + return [(k, occ) for k in kids] + + +def signature(cfg): + paths = [CONF_DIR / "config.toml", CONF_DIR / "overrides.toml"] + paths += [d for d, _ in calendars(cfg["calendars_dir"])] + # ponytail: directory mtimes catch vdirsyncer's rename-into-place writes; + # an in-place edit of an .ics would go unnoticed until the next change. + return tuple(p.stat().st_mtime if p.exists() else 0 for p in paths) + + +def daemon(): + if not shutil.which("notify-send"): + sys.exit("cal-notif: notify-send not found") + cfg, ov = load_config(), load_overrides() # a bad config at startup exits + voice = voice_ready(cfg["voice"]) + now = datetime.now().astimezone() + last = now - parse_duration(cfg["late"]) + sig, entries, snoozed, kids = None, [], [], [] + while True: + now = datetime.now().astimezone() + if (s := signature(cfg)) != sig: + if sig is not None: + try: + cfg, ov = load_config(), load_overrides() + except Exception as e: # keep the previous config + print(f"cal-notif: config not reloaded: {e}", file=sys.stderr) + occs = load_events(cfg["calendars_dir"], now - timedelta(days=1), now + horizon(cfg, ov)) + entries, sig = schedule(occs, cfg, ov), s + # ponytail: fired alarms live in memory; a restart within `late` + # repeats those popups once. + fire = due(entries, last, now, parse_duration(cfg["late"])) + fire += [e for e in snoozed if e[0] <= now] + snoozed = [e for e in snoozed if e[0] > now] + for _, o in fire: + kids += notify(o, now, cfg, voice) + for k, o in kids: + if k.poll() is not None and k.stdout and k.stdout.read().strip() == "snooze": + snoozed.append((now + parse_duration(cfg["snooze"]), o)) + kids = [(k, o) for k, o in kids if k.returncode is None] + last = now + time.sleep(30) + # ---- picker ---- def main(): cmd = sys.argv[1:2] - if cmd == ["say"] and len(sys.argv) > 2: + if cmd == ["daemon"]: + daemon() + elif cmd == ["say"] and len(sys.argv) > 2: say(" ".join(sys.argv[2:]), load_config()["voice"]) else: sys.exit(__doc__) |
