nix

configuration files that power my machines
Log | Files | Refs | README | LICENSE

commit 67b1187cf9666454d919051757df83c4be34b6c8
parent 4c64d1986900b1f5223b6141724b68cb15fa7f97
Author: mtmn <miro@haravara.org>
Date:   Sun, 24 May 2026 11:35:33 +0200

chore: redo khal_notifier, add alias

Diffstat:
Mmodules/mixins/dotfiles/bin/khal_notifier | 254+++++++++++++++++++++++++------------------------------------------------------
Mmodules/mixins/dotfiles/config/shell/rc/aliased-short-names | 1+
2 files changed, 82 insertions(+), 173 deletions(-)

diff --git a/modules/mixins/dotfiles/bin/khal_notifier b/modules/mixins/dotfiles/bin/khal_notifier @@ -5,14 +5,11 @@ [babashka.process :refer [shell]] [babashka.fs :as fs])) -(defn run-cmd - "Returns {:status :ok, :out s, :code n} or {:status :error, :reason r}" - [cmd args & {:keys [timeout] :or {timeout 30000}}] +(defn run-cmd [cmd args & {:keys [timeout] :or {timeout 30000}}] (try (if-not (fs/which cmd) {:status :error :reason :enoent} - (let [result (apply shell {:out :string :err :string :continue true :timeout timeout} - cmd args)] + (let [result (apply shell {:out :string :err :string :timeout timeout} cmd args)] {:status :ok :out (:out result) :code (:exit result)})) (catch java.util.concurrent.TimeoutException _ {:status :error :reason :timeout}) @@ -23,219 +20,130 @@ (println "[INFO] Syncing calendars with vdirsyncer") (let [result (run-cmd "vdirsyncer" ["sync"] :timeout 60000)] (cond - (and (= :ok (:status result)) - (zero? (:code result))) - (do (println "[INFO] vdirsyncer sync complete") - true) + (and (= :ok (:status result)) (zero? (:code result))) + (do (println "[INFO] vdirsyncer sync complete") true) (= :ok (:status result)) (do (println (format "[WARN] vdirsyncer exited with code %d: %s" - (:code result) - (str/trim (or (:out result) "")))) + (:code result) (str/trim (or (:out result) "")))) false) - (= :enoent (:reason result)) - (do (println "[ERROR] vdirsyncer not found") - false) - - (= :timeout (:reason result)) - (do (println "[ERROR] vdirsyncer timed out after 60 seconds") - false) - - :else - (do (println (format "[ERROR] vdirsyncer failed: %s" - (ex-message (:reason result)))) - false)))) + (= :enoent (:reason result)) (do (println "[ERROR] vdirsyncer not found") false) + (= :timeout (:reason result)) (do (println "[ERROR] vdirsyncer timed out") false) + :else (do (println (format "[ERROR] vdirsyncer failed: %s" (ex-message (:reason result)))) + false)))) + +(defn parse-event-line [line] + (let [parts (str/split line #"\|" 4) + cnt (count parts)] + (case cnt + 4 {:start-time (nth parts 0) :end-time (nth parts 1) + :title (nth parts 2) :organizer (nth parts 3)} + 3 {:start-time (nth parts 0) :end-time (nth parts 1) + :title (nth parts 2) :organizer ""} + 2 {:start-time (nth parts 0) :end-time "" + :title (nth parts 1) :organizer ""} + {:start-time "" :end-time "" :title (nth parts 0) :organizer ""}))) (defn get-upcoming-events [lookahead] (println (format "[INFO] Checking khal for events in the next %s" lookahead)) - (let [result (run-cmd "khal" ["list" "now" lookahead "--format" "{start-time}|{end-time}|{title}|{organizer}"])] + (let [result (run-cmd "khal" ["list" "now" lookahead "--format" + "{start-time}|{end-time}|{title}|{organizer}"])] (cond - (and (= :ok (:status result)) - (zero? (:code result))) - (let [lines (->> (:out result) - str/split-lines - (map str/trim) - (remove str/blank?) - (filter #(str/includes? % "|"))) - events (mapv (fn [line] - (let [parts (str/split line #"\|" 4) - cnt (count parts)] - (cond - (= cnt 4) - {:start-time (str/trim (nth parts 0)) - :end-time (str/trim (nth parts 1)) - :title (str/trim (nth parts 2)) - :organizer (str/trim (nth parts 3))} - - (= cnt 3) - {:start-time (str/trim (nth parts 0)) - :end-time (str/trim (nth parts 1)) - :title (str/trim (nth parts 2)) - :organizer ""} - - (= cnt 2) - {:start-time (str/trim (nth parts 0)) - :end-time "" - :title (str/trim (nth parts 1)) - :organizer ""} - - :else - {:start-time "" - :end-time "" - :title (str/trim (nth parts 0)) - :organizer ""}))) - lines) - events (remove #(str/blank? (:title %)) events)] + (and (= :ok (:status result)) (zero? (:code result))) + (let [events (->> (:out result) + str/split-lines + (map str/trim) + (remove str/blank?) + (filter #(str/includes? % "|")) + (map parse-event-line) + (remove #(str/blank? (:title %))) + vec)] (println (format "[INFO] Found %d upcoming event(s)" (count events))) events) (= :ok (:status result)) (do (println (format "[WARN] khal exited with code %d: %s" - (:code result) - (str/trim (or (:out result) "")))) + (:code result) (str/trim (or (:out result) "")))) []) - (= :enoent (:reason result)) - (do (println "[ERROR] khal not found") - []) + (= :enoent (:reason result)) (do (println "[ERROR] khal not found") []) + (= :timeout (:reason result)) (do (println "[ERROR] khal timed out") []) + :else (do (println "[ERROR] khal execution failed") [])))) - (= :timeout (:reason result)) - (do (println "[ERROR] khal timed out") - []) +(defn now-time [] + (-> (shell {:out :string} "date" "+%H:%M") :out str/trim)) - :else - (do (println "[ERROR] khal execution failed") - [])))) - -(defn now-time - "Get current time as HH:MM string" - [] - (-> (shell {:out :string :continue true} "date" "+%H:%M") - :out - str/trim)) - -(defn event-started? - "True if the event is already under way (or over) at `now` (HH:MM strings). - When end < start the event spans midnight, so an end earlier than `now` - may still be in the future." - [start end now] +(defn event-started? [start end now] (cond (str/blank? start) false - (str/blank? end) (<= (compare start now) 0) - (<= (compare start end) 0) (<= (compare start now) 0) - :else (or (<= (compare start now) 0) - (<= (compare now end) 0)))) - -(defn filter-events - "Filter out events that have already started" - [events] + (str/blank? end) (<= (compare start now) 0) + (<= (compare start end) 0) (<= (compare start now) 0) + :else (or (<= (compare start now) 0) (<= (compare now end) 0)))) + +(defn filter-events [events] (let [now (now-time)] (remove #(event-started? (:start-time %) (:end-time %) now) events))) -(defn format-event - "Format an event for notification body" - [event] - (let [time-str (let [{:keys [start-time end-time]} event] - (cond - (and (str/blank? start-time) (str/blank? end-time)) - "All day" - (str/blank? end-time) - start-time - :else - (str start-time " - " end-time)))] - (if (not (str/blank? (:organizer event))) - (str time-str "\nOrganizer: " (:organizer event)) - time-str))) - -(defn send-notification - "Send a desktop notification using notify-send" - [summary body urgency] +(defn format-event [event] + (let [time-str (cond + (and (str/blank? (:start-time event)) (str/blank? (:end-time event))) "All day" + (str/blank? (:end-time event)) (:start-time event) + :else (str (:start-time event) " - " (:end-time event)))] + (if (str/blank? (:organizer event)) + time-str + (str time-str "\nOrganizer: " (:organizer event))))) + +(defn send-notification [summary body] (println (format "[INFO] Notifying: %s — %s" summary body)) (let [result (run-cmd "notify-send" ["--app-name" "khal_notifier" - "--urgency" urgency - summary - body])] + "--urgency" "critical" + summary body])] (cond - (and (= :ok (:status result)) - (zero? (:code result))) - true - + (and (= :ok (:status result)) (zero? (:code result))) true (= :ok (:status result)) (do (println (format "[ERROR] notify-send failed (code %d): %s" - (:code result) - (str/trim (or (:out result) "")))) - false) - - (= :enoent (:reason result)) - (do (println "[ERROR] notify-send not found") + (:code result) (str/trim (or (:out result) "")))) false) + (= :enoent (:reason result)) (do (println "[ERROR] notify-send not found") false) + :else (do (println "[ERROR] notify-send failed") false)))) - :else - (do (println "[ERROR] notify-send failed") - false)))) - -(defn run-once - "Run a single check cycle" - [lookahead urgency skip-sync] +(defn run-once [lookahead skip-sync] (when-not skip-sync (when-not (sync-vdirsyncer) (println "[WARN] Sync failed — continuing with local calendar data"))) - (let [events (get-upcoming-events lookahead) filtered (filter-events events)] (if (empty? filtered) (println "[INFO] No upcoming events") (doseq [event filtered] - (let [summary (:title event) - body (format-event event)] - (send-notification summary body urgency)))))) - -(defn run-with-refresh - "Run continuously, checking every N seconds" - [seconds lookahead urgency skip-sync] - (println (format "[INFO] Running in refresh mode: checking every %d second(s)" seconds)) - (loop [iteration 0] - (when (pos? iteration) - (println (format "[INFO] Waiting %d second(s) before next refresh" seconds)) - (Thread/sleep (* seconds 1000))) - (println (format "[INFO] Run %d" (inc iteration))) - (run-once lookahead urgency skip-sync) - (recur (inc iteration)))) + (send-notification (:title event) (format-event event)))))) + +(defn parse-opts [args] + (loop [remaining args + opts {:lookahead "60m" :skip-sync false :help false}] + (if (empty? remaining) + opts + (let [arg (first remaining)] + (cond + (= arg "--help") (recur (rest remaining) (assoc opts :help true)) + (= arg "--skip-sync") (recur (rest remaining) (assoc opts :skip-sync true)) + (= arg "--lookahead") (recur (drop 2 remaining) (assoc opts :lookahead (second remaining))) + :else (recur (rest remaining) opts)))))) (defn print-usage [] (println "Usage: khal_notifier [options]") (println) (println "Options:") - (println " --lookahead How far ahead to check (default: 15m)") - (println " --skip-sync Skip vdirsyncer sync") - (println " --urgency Notification urgency (default: critical)") - (println " --refresh Run continuously, re-checking every N seconds (default: 300)") - (println " --help Show this help")) - -(defn parse-args [args] - (loop [pairs (seq args) - acc {}] - (if (empty? pairs) - acc - (let [k (first pairs) - v (second pairs)] - (if (and v (not (str/starts-with? v "--"))) - (recur (nnext pairs) - (assoc acc k v)) - (recur (next pairs) - (assoc acc k true))))))) + (println " --lookahead TIME How far ahead to check (default: 60m)") + (println " --skip-sync Skip vdirsyncer sync") + (println " --help Show this help")) (defn -main [& args] - (let [opts (parse-args args)] - (if (get opts "--help") - (do (print-usage) - (System/exit 0)) - (let [lookahead (get opts "--lookahead" "15m") - urgency (get opts "--urgency" "critical") - skip-sync (contains? opts "--skip-sync") - refresh (parse-long (get opts "--refresh" "300"))] - (run-with-refresh refresh lookahead urgency skip-sync))))) + (let [{:keys [lookahead skip-sync help]} (parse-opts args)] + (if help + (do (print-usage) (System/exit 0)) + (run-once lookahead skip-sync)))) (apply -main *command-line-args*) diff --git a/modules/mixins/dotfiles/config/shell/rc/aliased-short-names b/modules/mixins/dotfiles/config/shell/rc/aliased-short-names @@ -67,6 +67,7 @@ alias obsno='obsidian "obsidian://open?vault=nota" & disown' alias obscpt='obsidian "obsidian://open?vault=counterpoint" & disown' alias kat='khal at' +alias khn='khal_notifier' alias khw='khal list now 7d' alias khi='khal interactive'