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:
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'