├── .gitignore ├── README.org ├── images └── sample-alert.png └── org-timed-alerts.el /.gitignore: -------------------------------------------------------------------------------- 1 | /#org-timed-alerts.el# 2 | /#org-timer-alerts.el# 3 | /#README.org# 4 | /org-timed-alerts.elc 5 | /.#org-timed-alerts.el 6 | /#ert-tests.el# 7 | /#repeaters.el# 8 | /ert-tests.el 9 | /repeaters.el 10 | /test.org 11 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 2 | * org-timed-alerts.el 3 | Receive warnings and alerts via =alert.el= for upcoming events in your day. 4 | 5 | Ever look at your clock 10 minutes before a scheduled phone call, keep working on something else, and then not look at your clock until 2 minutes after the phone call was supposed to begin? 6 | 7 | I do it all the time. It must stop. 8 | 9 | =org-timed-alerts= scans your agenda files, finds any timestamps (active, deadline, or scheduled) with a time-of-day specification, and sends event warnings via =alert.el= according to whatever intervals you specify with custom alert messages. 10 | 11 | WARNING: This package is under development. It is close to complete and stable for my daily use. 12 | 13 | ** Usage 14 | *** Install the dependencies: 15 | - alert :: https://github.com/jwiegley/alert. 16 | Test =alert= by evaluating: =(alert "test")=. Did you see a notification? If so, proceed. 17 | - dash.el :: https://github.com/magnars/dash.el 18 | - org-ql :: https://github.com/alphapapa/org-ql 19 | - ts.el :: https://github.com/alphapapa/ts.el 20 | *** Installation 21 | Clone this repository into your load path. 22 | #+begin_src emacs-lisp :results silent 23 | git clone https://github.com/legalnonsense/org-timed-alerts.git 24 | #+end_src 25 | *** Use-package example 26 | Use-package declaration with all custom variables set to the default values: 27 | #+begin_src emacs-lisp :results silent 28 | (use-package org-timed-alerts 29 | :after (org) 30 | :custom 31 | (org-timed-alerts-alert-function # 'alert) 32 | (org-timed-alerts-tag-exclusions nil) 33 | (org-timed-alerts-default-alert-props nil) 34 | (org-timed-alerts-warning-times '(-10 -5)) 35 | (org-timed-alerts-agenda-hook-p t) 36 | (org-timed-alert-final-alert-string "IT IS %alert-time\n\n%todo %headline") 37 | (org-timed-alert-warning-string (concat "%todo %headline\n at %alert-time\n " 38 | "it is now %current-time\n " 39 | "*THIS IS YOUR %warning-time MINUTE WARNING*")) 40 | :config 41 | (add-hook 'org-mode-hook #'org-timed-alerts-mode)) 42 | #+end_src 43 | *** Without use-package 44 | There is no reason not to use =use-package=, but if you refuse, you can minimally use: 45 | #+begin_src emacs-lisp :results silent 46 | (require 'org-timed-alerts) 47 | (add-hook 'org-mode-hook #'org-timed-alerts-mode) 48 | #+end_src 49 | Adding =(org-timed-alerts-mode)= to =org-mode-hook= will activate the alert timers the next time you run =org-agenda=. It will not add timers until you run =org-agenda=. 50 | ** Customization 51 | | Custom variables | Description | Default value | 52 | |------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------| 53 | | org-timed-alerts-todo-exclusions | List of TODO states to ignore when generating the alert list. E.g., '("DONE") | nilb | 54 | | org-timed-alerts-warning-times | List of integers representing the intervals of warnings preceding the event. E.g., '(-10 -5) means you want to be warned 10 minutes, 5 minutes, and 2 minutes before the event. =nil= means no warning. There is no difference between positive and negative numbers, i.e., 10 and -10 both mean to send an alert 10 minutes before the event. A final notification is automatically sent at the time the event begins. | '(-10 -5) | 55 | | org-timed-alert-final-alert-string | Message to display in the alert shown at the time event begins (see below) | "IT IS %alert-time\n\nTIME FOR:\n%todo %headline" | 56 | | org-timed-alert-warning-string | Message to be displayed for warnings that precede the event (see below) | "%todo %headline\n at %alert-time\n it is now %current-time\n * THIS IS YOUR %warning-time MINUTE WARNING *" | 57 | 58 | 59 | 60 | 61 | 62 | | Less frequently needed custom variables | Description | Default value | 63 | |-----------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------| 64 | | org-timed-alerts-agenda-hook-p | Automatically add =org-timed-alerts-set-all-timers= to =org-agenda-hook=? If you turn this off, the minor mode is effectively meaningless and you’ll need to find another suitable way to call =org-timed-alerts-set-all-timers=. | t | 65 | | org-timed-alerts-default-alert-props | See the documentation for the function =alert=. This plist will be used to set the default for any of those properties. Any value of this list can be a function which will be called with the point at the org-heading. See description below. | nil | 66 | | org-timed-alerts-alert-command | Function to call when invoking the alert. See =alert.el= for other possibilities, e.g., =#'alert-libnotify-notify=, =#'alert-growl-notify=. Use these specific functions only if you don’t want to use the default alert specified in =alert-default-style=. | #'alert | 67 | *** alert strings 68 | =org-timed-alert-final-alert-string= and =org-timed-alert-warning-string= are strings that allow the following substitutions: 69 | 70 | | string | substitution | 71 | |---------------+---------------------------------------------------------------------------| 72 | | %todo | the TODO state of the the heading, if any | 73 | | %headline | the headline text of the heading | 74 | | %alert-time | the time of the event | 75 | | %warning-time | the current number of minutes before the event | 76 | | %current-time | the time the alert is actually sent to the user | 77 | | %category | the category property of the org heading, or the name of the file if none | 78 | 79 | For example, consider the heading: 80 | #+begin_src org 81 | * TODO phone conference I don't want to have 82 | :PROPERTIES: 83 | :CATEGORY: annoying-client 84 | :END: 85 | <2020-11-23 Mon 15:45> 86 | #+end_src 87 | The following string: 88 | =%todo %headline\n at %alert-time\n it is now %current-time\n * THIS IS YOUR %warning-time MINUTE WARNING *= 89 | Will use these substitutions when it send a 5 minute warning: 90 | | string | substitution | 91 | |---------------+-----------------------------------------| 92 | | %todo | "TODO" | 93 | | %headline | "phone conference I don't want to have" | 94 | | %alert-time | "20:05" | 95 | | %warning-time | "5" | 96 | | %current-time | "20:00" | 97 | | %category | "annoying-client" | 98 | 99 | And will display a warning that looks like this: 100 | [[./images/sample-alert.png]] 101 | 102 | Unless the =:title= property is overridden by =org-timed-alerts-default-alert-props=, the title of an alert defaults to the =category= property of the org heading. 103 | ** Special property for custom alert intervals 104 | Any heading can set custom alert intervals by setting the property =:ORG-TIMED-ALERTS:= For example: 105 | #+begin_src org 106 | * Lunch meeting 107 | :PROPERTIES: 108 | :ORG-TIMED-ALERTS: 5 4 3 2 1 109 | :END: 110 | <2020-11-29 Sun 11:36> 111 | #+end_src 112 | Will override =org-timed-alerts-warning-times= and send alert notifications 5, 4, 3, 2, and 1 minute before the appointment time. 113 | ** Note about =org-timed-alerts-default-alert-props= 114 | As stated above, the value of any property can be a function that is run at the underlying org heading. If you want more advanced customization of the alert properties, you can take advantage of this. For example, suppose you wanted the title of each alert to show the text of the root heading in the tree: 115 | #+begin_src emacs-lisp :results silent 116 | (setq org-timed-alerts-default-alert-props 117 | '(:title 118 | (lambda () (save-excursion 119 | ;; Move to the root heading 120 | (while (org-up-heading-safe)) 121 | ;; Return its headline, without tags, todo, etc. 122 | (org-get-heading t t t t))))) 123 | #+end_src 124 | Or suppose you wanted to customize the icon for an alert depending on the priority of the heading: 125 | #+begin_src emacs-lisp :results silent 126 | (setq org-timed-alerts-default-alert-props 127 | '(:icon 128 | (lambda () 129 | (if (string= "A" (org-entry-get (point) "PRIORITY")) 130 | "/path/to/some/icon" 131 | "/path/to/some/other/icon")))) 132 | #+end_src 133 | ** Updating the timers 134 | =org-timed-alerts= updates itself via =org-agenda-hook=. This is fast enough that I don't notice much speed difference when generating an agenda. You can turn this off by setting =org-timed-alerts-agenda-hook-p= to nil. If you do that, you can update manually with =org-timed-alerts-set-all-timers= or find another suitable hook (the package only schedules timers for the current day, so you'll need to update at least daily and after any relevant timestamp changes). 135 | * How it works 136 | 1. Run an org-ql query to get all active timestamps, scheduled timestamps, and deadlines on the current date. 137 | 2. For each of these events which has an associated time: 138 | 1. Create a timer to send an alert at that time via alert.el. This alert will use the string =org-timed-alert-final-alert-string= 139 | 2. Create warning timers according to the intervals specified in =org-timed-alerts-warning-times= and using the string =org-timed-alert-warning-string= 140 | 3. Update all timers any time the user runs =org-agenda=. You can update manually with =org-timed-alerts-set-all-timers=. You can disable all timers with =org-timed-alerts-cancel-all-timers= or by disabling the minor mode. 141 | * Other efforts 142 | This pacakge is meant to do what I want and and nothing more; I tried to abstract a bit so others might find it useful. I have included my notes on other similar packages. Apologies to the authors if they are not accurate. 143 | 144 | =org-alert=. /See/ https://github.com/spegoraro/org-alert. Org-alert checks for items which are scheduled or with deadlines for the current date, and then sends notification of those items immediately and simultaneously. It will resend notifications if you run =org-alert-check=. It serves a different purpose than this package. 145 | 146 | =org-notify=. /See/ https://code.orgmode.org/bzg/org-mode/raw/master/contrib/lisp/org-notify.el. Org-notify allows notifications to be scheduled and customized, but requires a special property to be set for each org heading for which an alert is desired. It also does not seem to use =alert.el= by default but appears it could be customized to do so. There are a lot of nice customization options here, but it asks a lot of the user with regard to setting special properties. By contrast, the goal of =org-timed-alerts= is to stay out of the way of the user by not requiring setting any special properties. In short, I feared getting this package set up for my purposes would be less enjoyable than writing a custom solution. 147 | 148 | =org-wild-notifier=. /See/ https://github.com/akhramov/org-wild-notifier.el. Org-wild-notifier is the closest to =org-timed-alerts=. The biggest drawback I saw was the inability to customize the alert message. /See/ /https://github.com/akhramov/org-wild-notifier.el/issues/43. Otherwise, this package may serve your purposes. 149 | * Changelog 150 | - [2020-12-03 Thu] Add support for repeating timestamps 151 | - [2020-12-04 Fri] Add support for excluding TODO states in =org-timed-alerts-todo-exclusions= 152 | -------------------------------------------------------------------------------- /images/sample-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/legalnonsense/org-timed-alerts/ba499f4471800754c75657d92c3150cb0b5deea2/images/sample-alert.png -------------------------------------------------------------------------------- /org-timed-alerts.el: -------------------------------------------------------------------------------- 1 | ;;; org-timed-alerts.el --- Automatiic org timers for upcoming events -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2020 Jeff Filipovits 4 | 5 | ;; Author: Jeff Filipovits 6 | ;; Url: https://github.com/legalnonsense/org-timed-alerts 7 | ;; Version: 0.0.1 8 | ;; Package-Requires: ((emacs "26.1") (org "9.0") (s "1.12.0") 9 | ;; (ts "0.2") (org-ql "0.5-pre") (dash "2.16.0")) 10 | ;; Keywords: Org, agenda, calendar, alert 11 | 12 | ;; This file is not part of GNU Emacs. 13 | 14 | ;;; Commentary: 15 | 16 | ;; Receive alerts for events (i.e., active timestamps, deadlines, schedules) which 17 | ;; have an associated time of day timestamp. Alerts are sent via `alert'. Timers 18 | ;; are updated every time you load your agenda. 19 | 20 | ;;; License: 21 | 22 | ;; This program is free software; you can redistribute it and/or modify 23 | ;; it under the terms of the GNU General Public License as published by 24 | ;; the Free Software Foundation, either version 3 of the License, or 25 | ;; (at your option) any later version. 26 | 27 | ;; This program is distributed in the hope that it will be useful, 28 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | ;; GNU General Public License for more details. 31 | 32 | ;; You should have received a copy of the GNU General Public License 33 | ;; along with this program. If not, see . 34 | 35 | ;;;; Installation 36 | 37 | ;; Install these required packages: 38 | 39 | ;; + org-ql 40 | ;; + ts.el 41 | ;; + alert 42 | ;; + dash.el 43 | 44 | ;; Then put this file in your load-path, and put this in your init 45 | ;; file. See the README for a use-package declaration. 46 | 47 | ;; (require 'org-timed-alerts) 48 | 49 | ;; Then: 50 | ;; (org-timed-alerts-mode) 51 | ;; Or, add a hook: 52 | ;; (add-hook 'org-mode-hook #'org-timed-alerts-mode) 53 | 54 | ;;;; Usage 55 | 56 | ;; Type M-x org-timed-alerts-mode RET to enable the package. 57 | ;; 58 | ;; To update all timers, open your org-agenda or type: 59 | ;; M-x org-timed-alerts-set-all-timers RET 60 | 61 | ;;;; Tips 62 | 63 | ;; You can customize settings in the `org-timed-alerts' group. 64 | 65 | ;;; License: 66 | 67 | ;; This program is free software; you can redistribute it and/or modify 68 | ;; it under the terms of the GNU General Public License as published by 69 | ;; the Free Software Foundation, either version 3 of the License, or 70 | ;; (at your option) any later version. 71 | 72 | ;; This program is distributed in the hope that it will be useful, 73 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 74 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 75 | ;; GNU General Public License for more details. 76 | 77 | ;; You should have received a copy of the GNU General Public License 78 | ;; along with this program. If not, see . 79 | 80 | ;;; Code: 81 | 82 | ;;;; Requirements 83 | 84 | (require 'alert) 85 | (require 'dash) 86 | (require 'ts) 87 | (require 'org-ql) 88 | 89 | ;;;; Customization 90 | 91 | (defgroup org-timed-alerts nil 92 | "org-timed-alerts options" 93 | :tag " org-timed-alerts" 94 | :group 'org 95 | :group 'org-timed-alerts 96 | :prefix "org-timed-alerts-") 97 | 98 | (defcustom org-timed-alerts-files nil 99 | "If nil, use (org-agenda-files). Otherwise, specify a file or list 100 | of files to search for events." 101 | :type 'list 102 | :group 'org-timed-alerts) 103 | 104 | (defcustom org-timed-alerts-alert-function #'alert 105 | "Alert function. Default is #'alert. See `alert' for more possibilities." 106 | :type 'function 107 | :group 'org-timed-alerts) 108 | 109 | (defcustom org-timed-alerts-final-alert-string 110 | "IT IS %alert-time\n\nTIME FOR:\n%todo %headline" 111 | "String for the final alert message, which which can use the following substitutions: 112 | %todo : the TODO state of the the heading, if any 113 | %headline : the headline text of the heading 114 | %category : the category property of the org heading, or the name of the file if none 115 | %alert-time : the time of the event 116 | %warning-time : the number of minutes before the event the warning will be shown 117 | %current-time : the time the alert is sent to the user" 118 | :type 'string 119 | :group 'org-timed-alerts) 120 | 121 | (defcustom org-timed-alerts-warning-string 122 | "%todo %headline\n at %alert-time\n it is now %current-time\n *THIS IS YOUR %warning-time MINUTE WARNING*" 123 | "String for alert warning messages, which can use the following substitutions: 124 | %todo : the TODO state of the the heading, if any 125 | %headline : the headline text of the heading 126 | %category : the category property of the org heading, or the name of the file if none 127 | %alert-time : the time of the event 128 | %warning-time : the number of minutes before the event the warning will be shown 129 | %current-time : the time the alert is sent to the user" 130 | :type 'string 131 | :group 'org-timed-alerts) 132 | 133 | (defcustom org-timed-alerts-default-alert-props nil 134 | "Plist used for default properties for alert messages. 135 | Accepts any properties used by `alert': 136 | :title 137 | :icon 138 | :category 139 | :buffer 140 | :mode 141 | :data 142 | :style 143 | :persistent 144 | :never-persist 145 | :id 146 | The value of each key should be whatever value is acceptable 147 | to `alert'. Alternatively, the value may be a function with 148 | no arguments which runs at each org heading and returns the 149 | appropriate val. 150 | 151 | For example to set the title of each alert 152 | to the root heading, you could use: 153 | '(:title (lambda () 154 | (save-excursion 155 | (while (org-up-heading-safe)) 156 | (org-get-heading t t t t))) 157 | 158 | Which moves up to the root header and sets the value of 159 | :title to that headline." 160 | :type '(plist :key-type symbol :value-type sexp) 161 | :group 'org-timed-alerts) 162 | 163 | (defcustom org-timed-alerts-agenda-hook-p t 164 | "Update all alerts whenever you generate an agenda?" 165 | :type 'boolean 166 | :group 'org-timed-alerts) 167 | 168 | (defcustom org-timed-alerts-refile-hook-p nil 169 | "Update all alerts whenever you refile an org entry?" 170 | :type 'boolean 171 | :group 'org-timed-alerts) 172 | 173 | (defcustom org-timed-alerts-warning-times '(-10 -5) 174 | "List of minutes before an event when a warning will be sent. 175 | There is no difference between positive and negative values, 176 | i.e., -10 and 10 both mean to send an alert 10 minutes before 177 | the event." 178 | :type '(list integer) 179 | :group 'org-timed-alerts) 180 | 181 | (defcustom org-timed-alerts-todo-exclusions '() 182 | "If a heading has any of these TODO states, do not schedule it for 183 | an alert." 184 | :type '(list) 185 | :group 'org-timed-alerts) 186 | 187 | ;;;; Variables 188 | 189 | (defvar org-timed-alerts--timer-list nil 190 | "Internal list of timer objects.") 191 | 192 | ;;;; Org-ql predicate 193 | 194 | ;; `org-ql' recently renamed `org-ql--defpred' 195 | ;; to `org-ql-defpred' 196 | (if (fboundp 'org-ql--defpred) 197 | (org-ql--defpred ts-repeat () 198 | "Find entries with timestamp repeats" 199 | :body (org-get-repeat)) 200 | (org-ql-defpred ts-repeat () 201 | "Find entries with timestamp repeats" 202 | :body (org-get-repeat))) 203 | 204 | ;;;; Functions 205 | 206 | (defun org-timed-alerts--string-substitute (string map marker) 207 | "MAP is an alist in the form of '((PLACEHOLDER . REPLACEMENT)) 208 | STRING is the original string. PLACEHOLDER is a symbol or a string that will 209 | be converted to a string prefixed with a %: \"%PLACEHOLDER\". 210 | REPLACEMENT can be a string, a number, symbol, or function. Replace all 211 | occurrences of %placeholder with replacement and return a new string." 212 | (cl-loop for (holder . replacement) in map 213 | when replacement 214 | do (setq string 215 | (replace-regexp-in-string 216 | (concat "%" 217 | (pcase holder 218 | ((pred symbolp) (symbol-name holder)) 219 | ((pred stringp) holder) 220 | ((pred numberp) (number-to-string holder)))) 221 | (pcase replacement 222 | ((pred stringp) replacement) 223 | ((pred numberp) (number-to-string replacement)) 224 | ((pred functionp) (org-timed-alerts--run-func-at-point 225 | replacement marker)) 226 | (_ "")) 227 | string)) 228 | finally return string)) 229 | 230 | (defun org-timed-alerts--run-func-at-point (func marker) 231 | "Call FUNC with point at MARKER." 232 | (with-current-buffer (marker-buffer marker) 233 | (org-with-wide-buffer 234 | (save-excursion (goto-char (marker-position marker)) 235 | (funcall func))))) 236 | 237 | (defun org-timed-alerts--get-default-prop (prop marker) 238 | "Get val for PROP from `org-timed-alerts-default-alert-props'. 239 | If val is a function, call it with point at MARKER; 240 | otherwise, return val." 241 | (let ((val (plist-get 242 | org-timed-alerts-default-alert-props 243 | prop))) 244 | (if (functionp val) 245 | (org-timed-alerts--run-func-at-point val marker) 246 | val))) 247 | 248 | (defun org-timed-alerts--org-ql-action () 249 | "Parsing function to be run as the `org-ql' :action. 250 | Adds a marker to `org-entry-properties' and returns 251 | an alist." 252 | (append (org-entry-properties) 253 | `(("MARKER" . ,(copy-marker 254 | (org-element-property 255 | :begin 256 | (org-element-at-point))))))) 257 | 258 | (defun org-timed-alerts--has-time-of-day-p (timestamp) 259 | "Does TIMESTAMP contain a time of day specification? 260 | TIMESTAMP is string in the form of an org timestamp." 261 | (when timestamp 262 | (string-match "[[:digit:]]\\{2\\}:[[:digit:]]\\{2\\}.*>" timestamp))) 263 | 264 | (defun org-timed-alerts--update-repeated-event (timestamp-string) 265 | "If TIMESTAMP-STRING has a repeat, update according to the 266 | repeat interval to show the next occurrence and return a 267 | an TS object the new date." 268 | (when-let* ((repeat (org-get-repeat timestamp-string)) 269 | (amount (string-to-number 270 | (if (= (length repeat) 4) 271 | (substring repeat 1 -1) 272 | (substring repeat 0 -1)))) 273 | (unit (pcase (substring repeat -1) 274 | ("w" (prog1 'day 275 | (setq amount (* 7 amount)))) 276 | ("h" 'hour) 277 | ("m" 'month) 278 | ("d" 'day) 279 | ("y" 'year))) 280 | (timestamp (ts-parse-org timestamp-string))) 281 | (while (ts< timestamp (ts-now)) 282 | (setq timestamp (ts-adjust unit amount timestamp))) 283 | timestamp)) 284 | 285 | (defun org-timed-alerts--parser (entry) 286 | "Process data from `org-ql' query and create 287 | timers by calling `org-timed-alerts--add-timer'." 288 | (-let (((&alist "ITEM" headline 289 | "TIMESTAMP" timestamp 290 | "DEADLINE" deadline 291 | "SCHEDULED" scheduled 292 | "TODO" todo 293 | "CATEGORY" category 294 | "MARKER" marker 295 | "ORG-TIMED-ALERTS" custom-alert-intervals) 296 | entry)) 297 | (when custom-alert-intervals 298 | (setq custom-alert-intervals 299 | (mapcar #'string-to-number (split-string custom-alert-intervals)))) 300 | (cl-loop 301 | for time in (list timestamp deadline scheduled) 302 | when (and time (org-timed-alerts--has-time-of-day-p time)) 303 | do 304 | ;; If the timestamp repeats, updated it and convert to ts, 305 | ;; otherwise, just convert it. 306 | (if (org-get-repeat time) 307 | (setq time (org-timed-alerts--update-repeated-event time)) 308 | (setq time (ts-parse-org time))) 309 | ;; Make sure the timestamp is between now and tomorrow 310 | (when (and (ts> time (ts-now)) 311 | (ts< time (ts-adjust 'day 1 (ts-now)))) 312 | (cl-loop 313 | with current-time = nil 314 | ;; Make sure there are no duplicates in the warning 315 | ;; intervals. 316 | for warning-time in (-distinct (-snoc 317 | (or custom-alert-intervals 318 | org-timed-alerts-warning-times) 319 | ;; 0 means send an alert at the 320 | ;; time of the event 321 | 0)) 322 | do 323 | (setq current-time (ts-adjust 'minute (* -1 (abs warning-time)) time)) 324 | (when (ts> current-time (ts-now)) 325 | (setq current-time (ts-format "%H:%M" current-time)) 326 | (org-timed-alerts--add-timer 327 | (ts-adjust 'minute (* -1 (abs warning-time)) time) 328 | ;; Create the message string 329 | (org-timed-alerts--string-substitute 330 | (if (= warning-time 0) 331 | org-timed-alerts-final-alert-string 332 | org-timed-alerts-warning-string) 333 | `((todo . ,(or todo "")) 334 | (headline . ,headline) 335 | (current-time . ,current-time) 336 | (alert-time . ,(ts-format "%H:%M" time)) 337 | (warning-time . ,(abs warning-time)) 338 | (category . ,category)) 339 | marker) 340 | marker 341 | :title (or (org-timed-alerts--get-default-prop 342 | :title marker) 343 | category)))))))) 344 | 345 | (defun org-timed-alerts--add-timer (time message marker &optional &key 346 | title icon category buffer mode 347 | severity data style persistent 348 | never-persist id) 349 | "Create timers via `run-at-time' and add them to 350 | `org-timed-alerts--timer-list'. TIME is the time to run the alert. 351 | MESSAGE is the alert body. Optional keys are those accepted by `alert'." 352 | (push (run-at-time 353 | ;; `run-at-time' only accepts times associated with the 354 | ;; current day. Ohterwise, we have to convert the 355 | ;; future time to seconds. 356 | (ts-difference time (ts-now)) 357 | nil 358 | org-timed-alerts-alert-function 359 | message 360 | :title 361 | (or title (org-timed-alerts--get-default-prop :title marker)) 362 | :icon 363 | (or icon (org-timed-alerts--get-default-prop :icon marker)) 364 | :category 365 | (or category (org-timed-alerts--get-default-prop :category marker)) 366 | :buffer 367 | (or buffer (org-timed-alerts--get-default-prop :buffer marker)) 368 | :mode 369 | (or mode (org-timed-alerts--get-default-prop :mode marker)) 370 | :data 371 | (or data (org-timed-alerts--get-default-prop :data marker)) 372 | :style 373 | (or style (org-timed-alerts--get-default-prop :style marker)) 374 | :severity 375 | (or severity (org-timed-alerts--get-default-prop :severity marker)) 376 | :persistent 377 | (or persistent (org-timed-alerts--get-default-prop :persistent marker)) 378 | :never-persist 379 | (or never-persist (org-timed-alerts--get-default-prop :never-persist marker)) 380 | :id (or id (org-timed-alerts--get-default-prop :id marker))) 381 | org-timed-alerts--timer-list)) 382 | 383 | ;;;; Commands 384 | 385 | (defun org-timed-alerts-list-timers () 386 | "Print list of active timers to the message buffer." 387 | (interactive) 388 | (message 389 | (cl-loop for timer in org-timed-alerts--timer-list 390 | for x from 1 to (length org-timed-alerts--timer-list) 391 | concat (concat "Timer #" 392 | (number-to-string x) 393 | "; set for: " 394 | (let ((time 395 | (decode-time 396 | (timer--time 397 | timer)))) 398 | (concat 399 | (number-to-string (nth 2 time)) 400 | ":" 401 | (s-pad-left 2 "0" 402 | (number-to-string (nth 1 time))) 403 | " on " 404 | (number-to-string (nth 5 time)) 405 | "-" 406 | (number-to-string (nth 4 time)) 407 | "-" 408 | (number-to-string (nth 3 time)))) 409 | "; with message: " 410 | (pp (car (elt timer 6))) 411 | "\n\n")))) 412 | 413 | ;;;###autoload 414 | (defun org-timed-alerts-set-all-timers () 415 | "Run `org-ql' query to get all headings with today's timestamp." 416 | (interactive) 417 | (org-timed-alerts-cancel-all-timers) 418 | (cl-loop for entry in (org-ql-select (or org-timed-alerts-files 419 | (org-agenda-files)) 420 | `(and 421 | (or (ts-repeat) 422 | (ts-active 423 | :from ,(ts-format "%Y-%m-%d" (ts-now)) 424 | :to ,(ts-format "%Y-%m-%d" 425 | (ts-adjust 'day 1 (ts-now))))) 426 | (not (todo ,@org-timed-alerts-todo-exclusions))) 427 | :action #'org-timed-alerts--org-ql-action) 428 | do (org-timed-alerts--parser entry)) 429 | (message "Org-timed-alerts: timers updated.")) 430 | 431 | ;;;###autoload 432 | (defun org-timed-alerts-cancel-all-timers () 433 | "Cancel all the timers." 434 | (interactive) 435 | (cl-loop for timer in org-timed-alerts--timer-list 436 | do (cancel-timer timer)) 437 | (setq org-timed-alerts--timer-list nil)) 438 | 439 | ;;;###autoload 440 | (define-minor-mode org-timed-alerts-mode 441 | "Get alerts before orgmode events." 442 | nil 443 | " alerts" 444 | nil 445 | (if org-timed-alerts-mode 446 | (progn 447 | (when org-timed-alerts-agenda-hook-p 448 | (add-hook 'org-capture-after-finalize-hook #'org-timed-alerts-set-all-timers) 449 | (add-hook 'org-agenda-mode-hook #'org-timed-alerts-set-all-timers)) 450 | (when org-timed-alerts-refile-hook-p 451 | (add-hook 'org-after-refile-insert-hook #'org-timed-alerts-set-all-timers) 452 | (add-hook 'org-after-refile-insert-hook #'org-timed-alerts-set-all-timers))) 453 | (org-timed-alerts-cancel-all-timers) 454 | (remove-hook 'org-capture-after-finalize-hook #'org-timed-alerts-set-all-timers) 455 | (remove-hook 'org-agenda-mode-hook #'org-timed-alerts-set-all-timers) 456 | (remove-hook 'org-after-refile-insert-hook #'org-timed-alerts-set-all-timers))) 457 | 458 | ;;;; Footer 459 | 460 | (provide 'org-timed-alerts) 461 | 462 | ;;; org-timed-alerts.el ends here 463 | 464 | 465 | 466 | --------------------------------------------------------------------------------