khal-notify.lisp 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. (eval-when (:compile-toplevel :load-toplevel :execute)
  2. (ql:quickload '(with-user-abort chronicity
  3. cl-ppcre adopt uiop) :silent t))
  4. (defpackage :khal-notify
  5. (:use :cl)
  6. (:export :toplevel))
  7. (in-package :khal-notify)
  8. (defun run-khal (&rest args)
  9. (multiple-value-bind (out err code)
  10. (uiop:run-program `("khal" . ,args)
  11. :output :string
  12. :error-output nil
  13. :ignore-error-status t)
  14. (declare (ignore err))
  15. (when (= code 0)
  16. out)))
  17. (defun notify-send (summary content &optional (time 0))
  18. (uiop:launch-program `("notify-send"
  19. "-t" ,(princ-to-string time)
  20. ,summary ,content)))
  21. (defun get-calendar-dir-hash-table ()
  22. (let ((output (make-hash-table :test 'equal)))
  23. (dolist (dir (directory #P"~/.local/share/vdirsyncer/*/*") output)
  24. (unless (pathname-match-p dir #P"~/.local/share/vdirsyncer/status/*/")
  25. (let ((dir-name (car (last (pathname-directory dir)))))
  26. (setf (gethash dir-name output) dir))))))
  27. (defun remove-trailing-return (str)
  28. (let ((len (length str)))
  29. (if (eq (char str (1- len)) #\return)
  30. (subseq str 0 (1- len))
  31. str)))
  32. (defun get-ics-file-alarms (path)
  33. (with-open-file (stream path :direction :input :if-does-not-exist nil)
  34. (when stream
  35. (loop for line = (read-line stream nil)
  36. with in-valarm = nil
  37. and alarms = '()
  38. and pattern = (ppcre:create-scanner "(-?)[A-Z]+([0-9]+)([DHSM]).*")
  39. and summary = ""
  40. and current-offset = 0
  41. and current-notice = ""
  42. while line
  43. do (cond
  44. ((and (not in-valarm) (uiop:string-prefix-p "SUMMARY:" line))
  45. (setq summary (subseq (remove-trailing-return line) 8)))
  46. ((uiop:string-prefix-p "BEGIN:VALARM" line)
  47. (setq in-valarm t))
  48. ((uiop:string-prefix-p "END:VALARM" line)
  49. (pushnew (cons current-notice current-offset) alarms)
  50. (setq in-valarm nil
  51. current-offset 0
  52. current-notice ""))
  53. ((and in-valarm (uiop:string-prefix-p "TRIGGER:" line))
  54. (ppcre:register-groups-bind
  55. (negative (#'parse-integer num) unit)
  56. (pattern (subseq line 8))
  57. (setq current-offset
  58. (* (if (equal negative "-") -1 1)
  59. (cond
  60. ((equal unit "S") num)
  61. ((equal unit "M") (* num 60))
  62. ((equal unit "H") (* num 60 60))
  63. ((equal unit "D") (* num 60 60 24))
  64. (t 0))))))
  65. ((and in-valarm (uiop:string-prefix-p "DESCRIPTION:" line))
  66. (setq current-notice (subseq (remove-trailing-return line)
  67. 12))))
  68. finally (return (mapcar (lambda (alarm)
  69. (list (if (uiop:emptyp (car alarm))
  70. summary
  71. (car alarm))
  72. (cdr alarm)
  73. summary)) alarms))))))
  74. (defstruct cal-alarm
  75. hash uid time title event-title event-end)
  76. (defun parse-event-line (calendar-dirs line &optional exclude-before)
  77. (unless (uiop:emptyp line)
  78. (destructuring-bind
  79. (uid calendar start-date start-time end-date end-time &optional alarm-sym)
  80. (uiop:split-string line :separator " ")
  81. (when alarm-sym
  82. (let* ((start (chronicity:parse (concatenate 'string
  83. start-date
  84. " "
  85. start-time)))
  86. (end (chronicity:parse (concatenate 'string
  87. end-date
  88. " "
  89. end-time)))
  90. (file (merge-pathnames (concatenate 'string uid ".ics")
  91. (gethash calendar calendar-dirs)))
  92. (alarms (get-ics-file-alarms file)))
  93. (when (or (not exclude-before)
  94. (local-time:timestamp>= end exclude-before))
  95. (mapcar (lambda (alarm)
  96. (destructuring-bind (title offset event-title) alarm
  97. (let ((alarm-time (local-time:timestamp+ start
  98. offset :sec)))
  99. (make-cal-alarm :hash (format nil "~A~A~A~A~A"
  100. title
  101. alarm-time
  102. event-title
  103. end
  104. uid)
  105. :title title
  106. :time alarm-time
  107. :event-title event-title
  108. :event-end end
  109. :uid uid)))) alarms)))))))
  110. (defun build-alarm-list (calendar-dirs &optional exclude-before)
  111. (let* ((output (run-khal "list"
  112. "--day-format"
  113. ""
  114. "--format"
  115. "{uid} {calendar} {start-date} {start-time} {end-date} {end-time}{alarm-symbol}"
  116. "today"
  117. "tomorrow"))
  118. (lines (uiop:split-string output :separator '(#\newline)))
  119. (result '()))
  120. (dolist (line lines result)
  121. (uiop:if-let ((alarms (parse-event-line calendar-dirs line exclude-before)))
  122. (setq result (concatenate 'list result alarms))))))
  123. (defun main ()
  124. (let* ((calendar-dirs (get-calendar-dir-hash-table))
  125. (already-notified (make-hash-table :test 'equal))
  126. (program-start-now (local-time:now))
  127. (alarms (build-alarm-list calendar-dirs program-start-now))
  128. (last-update (local-time:now)))
  129. (loop
  130. (let ((now (local-time:now)))
  131. (when (local-time:timestamp<= (local-time:timestamp+ last-update 1 :minute)
  132. now)
  133. (setq alarms (build-alarm-list calendar-dirs program-start-now))
  134. (let ((new-notified (make-hash-table :test 'equal)))
  135. (dolist (alarm alarms)
  136. (when (gethash (cal-alarm-hash alarm) already-notified)
  137. (setf (gethash (cal-alarm-hash alarm) new-notified) t)))
  138. (setq already-notified new-notified
  139. last-update (local-time:now))))
  140. (dolist (alarm alarms)
  141. (when (and (not (gethash (cal-alarm-hash alarm) already-notified))
  142. (local-time:timestamp<= (cal-alarm-time alarm) now))
  143. (notify-send (concatenate 'string
  144. "Alarm for "
  145. (cal-alarm-event-title alarm))
  146. (cal-alarm-title alarm))
  147. (setf (gethash (cal-alarm-hash alarm) already-notified) t)))
  148. (sleep 10)))))
  149. ;; interface
  150. (defmacro exit-on-ctrl-c (&body body)
  151. `(handler-case (with-user-abort:with-user-abort (progn ,@body))
  152. (with-user-abort:user-abort () (adopt:exit 130))))
  153. (defun toplevel ()
  154. (sb-ext:disable-debugger)
  155. (exit-on-ctrl-c
  156. (main)))