aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-27 20:05:55 +0200
committerDanilo M. <danix@danix.xyz>2026-09-27 20:05:55 +0200
commitb928c3e5bdb01536b60d43f1bc3dd5a9ddb41057 (patch)
tree2b8b6c0fd0455f6c9a8ee297110b260b233aa76b
parent223292883da497b46caa0e01be0f4a1a3f134ea8 (diff)
downloadcal-notif-b928c3e5bdb01536b60d43f1bc3dd5a9ddb41057.tar.gz
cal-notif-b928c3e5bdb01536b60d43f1bc3dd5a9ddb41057.zip
Add the udt-themed rofi picker for per-event alarms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rwxr-xr-xcal-notif51
-rw-r--r--test_cal_notif.py12
2 files changed, 63 insertions, 0 deletions
diff --git a/cal-notif b/cal-notif
index c8ed33e..792d754 100755
--- a/cal-notif
+++ b/cal-notif
@@ -394,10 +394,61 @@ def daemon():
# ---- picker ----
+def rofi(lines, prompt, theme, mesg=None, index=False):
+ # udt theming: list.rasi keeps the inputbar (free-text durations) and
+ # the message widget (-mesg); menu.rasi has neither.
+ cmd = ["rofi", "-dmenu", "-i", "-theme", str(Path(theme).expanduser()), "-p", prompt]
+ cmd += ["-mesg", mesg] if mesg else []
+ cmd += ["-format", "i"] if index else []
+ r = subprocess.run(cmd, input="\n".join(lines), capture_output=True, text=True)
+ return r.stdout.strip() if r.returncode == 0 else None # Escape: cancel
+
+
+def parse_choice(s):
+ """Returns a list of offsets, or None for 'reset'. Raises ValueError."""
+ s = s.strip()
+ if s == "reset":
+ return None
+ if s == "none":
+ return []
+ return [fmt_duration(parse_duration(p)) for p in s.split(",")]
+
+
+def pick():
+ cfg, ov = load_config(), load_overrides()
+ now = datetime.now().astimezone()
+ occs = [o for o in load_events(cfg["calendars_dir"], now, now + timedelta(days=PICK_DAYS))
+ if o.start >= now]
+ fmt = lambda o: f"{o.start:%d.%m} " if o.allday else f"{o.start:%d.%m %H:%M}"
+ lines = [f"{fmt(o)} · {o.cal} · {o.summary} · [{', '.join(current_offsets(o, cfg, ov))}]"
+ for o in occs]
+ i = rofi(lines, "evento", cfg["rofi_theme"], index=True)
+ if i is None or not i.lstrip("-").isdigit() or int(i) < 0:
+ return
+ o, mesg = occs[int(i)], lines[int(i)]
+ presets = ["10m", "30m", "1h", "1d", "1d, 1h", "none", "reset"]
+ while (s := rofi(presets, "avvisi", cfg["rofi_theme"], mesg=mesg)) is not None:
+ try:
+ offs = parse_choice(s)
+ except ValueError as e:
+ mesg = f"{lines[int(i)]}\n{e}"
+ continue
+ if offs is None:
+ ov.pop(o.uid, None)
+ else:
+ ov[o.uid] = offs
+ save_overrides(ov)
+ return
+
+
def main():
cmd = sys.argv[1:2]
if cmd == ["daemon"]:
daemon()
+ elif cmd == ["pick"]:
+ if not shutil.which("rofi"):
+ sys.exit("cal-notif: rofi not found")
+ pick()
elif cmd == ["say"] and len(sys.argv) > 2:
say(" ".join(sys.argv[2:]), load_config()["voice"])
else:
diff --git a/test_cal_notif.py b/test_cal_notif.py
index f22fbc3..0bb647c 100644
--- a/test_cal_notif.py
+++ b/test_cal_notif.py
@@ -193,6 +193,18 @@ def test_due():
assert cn.announce(o, t + 5 * M, cn.DEFAULTS["voice"]) == "Adesso: x"
+def test_choice():
+ assert cn.parse_choice("1d, 1h") == ["1d", "1h"]
+ assert cn.parse_choice("90m") == ["1h 30m"]
+ assert cn.parse_choice("none") == []
+ assert cn.parse_choice("reset") is None
+ try:
+ cn.parse_choice("soon")
+ raise AssertionError
+ except ValueError:
+ pass
+
+
if __name__ == "__main__":
for name, fn in list(globals().items()):
if name.startswith("test_"):