├── alarm.mp3 ├── Cask ├── .gitignore ├── .github └── workflows │ └── test.yml ├── README.md └── alarm-clock.el /alarm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlemuel/alarm-clock/HEAD/alarm.mp3 -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "alarm-clock.el") 5 | 6 | (files "*.mp3") 7 | 8 | (development 9 | (depends-on "f")) 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/elisp 3 | # Edit at https://www.gitignore.io/?templates=elisp 4 | 5 | ### Elisp ### 6 | # Compiled 7 | *.elc 8 | 9 | # Packaging 10 | .cask 11 | 12 | # Backup files 13 | *~ 14 | 15 | # Undo-tree save-files 16 | *.~undo-tree 17 | 18 | # End of https://www.gitignore.io/api/elisp 19 | 20 | # Created by https://www.gitignore.io/api/macos 21 | # Edit at https://www.gitignore.io/?templates=macos 22 | 23 | ### macOS ### 24 | # General 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must end with two \r 30 | Icon 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear in the root of a volume 36 | .DocumentRevisions-V100 37 | .fseventsd 38 | .Spotlight-V100 39 | .TemporaryItems 40 | .Trashes 41 | .VolumeIcon.icns 42 | .com.apple.timemachine.donotpresent 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | 51 | # End of https://www.gitignore.io/api/macos 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - "**.mp3" 8 | - "**.md" 9 | - "**.org" 10 | - ".dir-locals.el" 11 | branches: 12 | - master 13 | - develop 14 | - fix/* 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | emacs_version: 22 | - "25.1" 23 | - "25.3" 24 | - "26.1" 25 | - "26.3" 26 | - "27.2" 27 | - "28.1" 28 | - "snapshot" 29 | cask_version: 30 | - "snapshot" 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: actions/setup-python@v4 34 | with: 35 | python-version: "3.10" 36 | 37 | - uses: purcell/setup-emacs@master 38 | with: 39 | version: ${{ matrix.emacs_version }} 40 | 41 | - uses: conao3/setup-cask@master 42 | with: 43 | version: ${{ matrix.cask_version }} 44 | 45 | - name: lint with cask 46 | run: cask build 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alarm-clock 2 | 3 | [![MELPA](https://melpa.org/packages/alarm-clock-badge.svg)](https://melpa.org/#/alarm-clock) 4 | [![License](http://img.shields.io/:license-gpl3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) 5 | 6 | An alarm clock for Emacs 7 | 8 | 9 | ## Requirements 10 | 11 | - Emacs 24 or higher 12 | - [mpg123](http://mpg123.org) (gnu/linux, Cygwin and Windows) 13 | - [terminal-notifier](https://github.com/julienXX/terminal-notifier) (macOS) 14 | - [notify-send](https://manpages.debian.org/stretch/libnotify-bin/notify-send.1.en.html) (gnu/linux) 15 | 16 | ## Get started 17 | - Via [MELPA](https://melpa.org). 18 | 19 | - Get alarm-clock 20 | - Manually download alarm-clock and set-up your load path. 21 | - To auto-start alarm-clock every time you open Emacs add these lines to your .emacs file: 22 | 23 | (require 'alarm-clock) ; Not needed if you use package.el 24 | 25 | ## Basic Usage 26 | 27 | ### `alarm-clock-set` 28 | 29 | Set an alarm clock with the time following tips. 30 | TIME can be "6"(6 seconds), "11 minutes", "10 hours", "11:40pm", etc. 31 | MESSAGE will be shown when notifying at setting time. 32 | 33 | ### `alarm-clock-list-view` 34 | 35 | Display the alarm clock list. 36 | Use `a` to set a new alarm clock, `d` or `C-k` to delete current alarm 37 | clock, `g` to refresh the view, and space (`' '`) to turn off 38 | a ringing alarm (M-x alarm-clock-stop). 39 | 40 | ### `alarm-clock-stop` 41 | 42 | Turn off the currently ringing alarm. 43 | 44 | ### `alarm-clock-save` 45 | 46 | Save alarm-clock to cache file. 47 | 48 | ### `alarm-clock-restore` 49 | 50 | Restore alarm-clock from cache file. 51 | 52 | 53 | ## Configurations 54 | 55 | ### Enable autosave-and-restore feature 56 | 57 | - Change cache file path by setting the variable `alarm-clock-cache-file` to any file path you like. 58 | - Add these lines to your `.emacs` file: 59 | 60 | (alarm-clock-turn-autosave-on) 61 | 62 | ## Q & A 63 | 64 | - Meet `(wrong-type-argument package-desc nil)` on Mac OSX. 65 | 66 | - Install `gnu-tar`. 67 | 68 | > brew install gnu-tar 69 | 70 | - Try to set `quelpa-build-tar-executable` to the path of `gtar`, (e.g "/usr/local/bin/gtar"). 71 | 72 | - Then reinstall this package. 73 | 74 | ## Appendix 75 | 76 | I'd be glad to receive patches, 77 | comments and your considered criticism. 78 | 79 | _Have fun with alarm-clock!_ 80 | -------------------------------------------------------------------------------- /alarm-clock.el: -------------------------------------------------------------------------------- 1 | ;;; alarm-clock.el --- Alarm Clock -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2018-2022 Steve Lemuel 4 | 5 | ;; Author: Steve Lemuel 6 | ;; Keywords: calendar, tools, convenience 7 | ;; Version: 2019.02.12 8 | ;; Package-Version: 20190212.1 9 | ;; Package-Requires: ((emacs "24.4")) 10 | ;; URL: https://github.com/wlemuel/alarm-clock 11 | 12 | ;; This program is free software; you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation, either version 3 of the License, or 15 | ;; (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | 27 | ;; This program is an alarm management tool for Emacs. 28 | ;; To set an alarm clock, call `M-x alarm-clock-set', then enter time as 29 | ;; the following tips. 30 | ;; To view alarm clock list, call `M-x alarm-clock-list-view', then use 31 | ;; 'a' key to set a new alarm clock, 32 | ;; 'C-k' to kill an alarm clock in the current line. 33 | 34 | ;;; Code: 35 | 36 | (require 'parse-time) 37 | (require 'alert nil t) 38 | 39 | (defgroup alarm-clock nil 40 | "An alarm clock management." 41 | :group 'applications 42 | :prefix "alarm-clock-") 43 | 44 | (defcustom alarm-clock-sound-file 45 | (concat 46 | (file-name-directory (locate-library "alarm-clock")) 47 | "alarm.mp3") 48 | "File to play the alarm sound." 49 | :type 'file 50 | :group 'alarm-clock) 51 | 52 | (defcustom alarm-clock-play-sound t 53 | "Whether to play sound when notifying, only avaiable for osx and linux." 54 | :type 'boolean 55 | :group 'alarm-clock) 56 | 57 | (defcustom alarm-clock-play-sound-repeat 1 58 | "Number of times to repeat the sound when an alarm rings. Use M-x alarm-clock-stop to quiet the alarm." 59 | :type 'integer 60 | :group 'alarm-clock) 61 | 62 | (defcustom alarm-clock-play-auto-view-alarms nil 63 | "If non-nul, display the alarm clock list when ringing an alarm, to allow using SPACE to run alarm-clock-stop" 64 | :type 'boolean 65 | :group 'alarm-clock) 66 | 67 | (defcustom alarm-clock-system-notify t 68 | "Whether to notify via system based notification feature." 69 | :type 'boolean 70 | :group 'alarm-clock) 71 | 72 | (defcustom alarm-clock-alert-notify t 73 | "Whether to notify via system based notification feature." 74 | :type 'boolean 75 | :group 'alarm-clock) 76 | 77 | (defcustom alarm-clock-cache-file 78 | (expand-file-name ".alarm-clock.cache" user-emacs-directory) 79 | "The name of alarm-clock's cache file." 80 | :type 'string 81 | :group 'alarm-clock) 82 | 83 | (defcustom alarm-clock-auto-save t 84 | "If true, auto-save alarm clocks when adding or removing alarms or after alarm timeout." 85 | :type 'boolean 86 | :group 'alarm-clock) 87 | 88 | (defvar alarm-clock--alist nil 89 | "List of information about alarm clock.") 90 | 91 | (defvar alarm-clock--macos-sender nil 92 | "Notification sender for MacOS.") 93 | 94 | (defvar alarm-clock--stopped nil 95 | "If true, stop sounding the alarm. Set to t by M-x alarm-clock-stop or pressing SPACE in alarm-clock-list-view window") 96 | 97 | (define-derived-mode alarm-clock-mode special-mode "Alarm Clock" 98 | "Mode for listing alarm-clocks. 99 | 100 | \\{alarm-clock-mode-map}" 101 | (buffer-disable-undo) 102 | (setq truncate-lines t) 103 | 104 | (define-key alarm-clock-mode-map [(control k)] 'alarm-clock-kill) 105 | (define-key alarm-clock-mode-map "d" 'alarm-clock-kill) 106 | (define-key alarm-clock-mode-map "a" 'alarm-clock-set) 107 | (define-key alarm-clock-mode-map "i" 'alarm-clock-set) 108 | (define-key alarm-clock-mode-map "+" 'alarm-clock-set) 109 | (define-key alarm-clock-mode-map "-" 'alarm-clock-kill) 110 | (define-key alarm-clock-mode-map "g" 'alarm-clock-list-view) 111 | (define-key alarm-clock-mode-map " " 'alarm-clock-stop) 112 | ) 113 | 114 | ;;;###autoload 115 | (defun alarm-clock-set (time message) 116 | "Set an alarm clock at time TIME. 117 | MESSAGE will be shown when notifying at that time. 118 | Auto-save the alarms if alarm-clock-auto-save is true." 119 | (interactive "sAlarm at (e.g: 10:00am, 2 minutes, 30 seconds): \nsMessage: ") 120 | (alarm-clock--set time message) 121 | (alarm-clock--list-prepare) 122 | (alarm-clock--maybe-auto-save)) 123 | 124 | (defun alarm-clock--set (time message) 125 | "Set an alarm clock at time TIME. 126 | MESSAGE will be shown when notifying in the status bar." 127 | (let* ((time (alarm-clock--preparse-time time)) 128 | (message (string-trim message)) 129 | (timer (run-at-time 130 | time 131 | nil 132 | (lambda (message) (alarm-clock--notify "Alarm Clock" message)) 133 | message))) 134 | (push (list :time (timer--time timer) 135 | :message message 136 | :timer timer) 137 | alarm-clock--alist))) 138 | 139 | (defun alarm-clock--preparse-time (time) 140 | "Clean up the time, if it is a string, strip out the spaces at both ends." 141 | (when (stringp time) 142 | (setq time (string-trim time)) 143 | 144 | ;; Handle time abbreviations like "2s 3m 4h 5h6m 7h8m9s", which is equivalent to 145 | ;; "2 seconds, 3 minutes, 4 hours, 5 hours 6 minutes, 7 hours 8 minutes 9 seconds" 146 | (when (string-match 147 | (concat "^[1-9][0-9]*[smh]$\\|" 148 | "^[1-9][0-9]*[mh][1-9][0-9]*[sm]$\\|" 149 | "^[1-9][0-9]*h[1-9][0-9]*m[1-9][0-9]*s$") 150 | time) 151 | (setq time (string-replace "s" "second" time)) 152 | (setq time (string-replace "m" "minute" time)) 153 | (setq time (string-replace "h" "hour" time)))) 154 | time) 155 | 156 | (defun alarm-clock--maybe-auto-save () 157 | "If alarm-clock-auto-save is true, save alarms to alarm-clock-cache-file" 158 | (and alarm-clock-auto-save 159 | (alarm-clock-save))) 160 | 161 | ;;;###autoload 162 | (defun alarm-clock-list-view () 163 | "Display the alarm clocks." 164 | (interactive) 165 | ;;(unless alarm-clock--alist 166 | ;; (user-error "No alarm clocks are set")) 167 | (alarm-clock--list-prepare) 168 | (pop-to-buffer "*alarm clock*")) 169 | 170 | (defun alarm-clock--compare (a b) 171 | "Compare two alarms A and B by date-time" 172 | (let ((time-a (plist-get a :time)) 173 | (time-b (plist-get b :time))) 174 | (time-less-p time-b time-a))) 175 | 176 | (defun alarm-clock--sort-list () 177 | "Sort the alarm in increasing time" 178 | (setq alarm-clock--alist (sort alarm-clock--alist (function alarm-clock--compare)))) 179 | 180 | (defun alarm-clock--list-prepare () 181 | "Prefare the list buffer." 182 | (alarm-clock--remove-expired) 183 | (set-buffer (get-buffer-create "*alarm clock*")) 184 | (alarm-clock-mode) 185 | (let* ((format "%-20s %-12s %s") 186 | (inhibit-read-only t) ) 187 | (erase-buffer) 188 | (setq header-line-format (format format "Time" "Remaining" "Message")) 189 | (dolist (alarm (alarm-clock--sort-list)) 190 | (let* ((alarm-time (plist-get alarm :time)) 191 | (alarm-message (plist-get alarm :message)) 192 | ;; I think alarms are removed from the list when they fire, so no negative remaining values 193 | (remaining (format-time-string "%H:%2M:%2S" (time-subtract alarm-time nil) 0) ) 194 | (start (point)) 195 | (time (format-time-string "%F %X" alarm-time))) 196 | (insert (format format time remaining alarm-message) "\n") 197 | (put-text-property start (1+ start) 'alarm-clock alarm)) 198 | (goto-char (point-min))))) 199 | 200 | ;;;###autoload 201 | (defun alarm-clock-stop () 202 | "Stop sounding the current alarm." 203 | (interactive) 204 | (setq alarm-clock--stopped t) 205 | (message "Alarm stopped.") 206 | ) 207 | 208 | (defun alarm-clock-kill () 209 | "Kill the current alarm clock." 210 | (interactive) 211 | (let* ((start (line-beginning-position)) 212 | (alarm (get-text-property start 'alarm-clock)) 213 | (inhibit-read-only t)) 214 | (unless alarm 215 | (user-error "No alarm clock on the current line")) 216 | (forward-line 1) 217 | (delete-region start (point)) 218 | (cancel-timer (plist-get alarm :timer)) 219 | (setq alarm-clock--alist (delq alarm alarm-clock--alist)) 220 | (alarm-clock--maybe-auto-save))) 221 | 222 | (defun alarm-clock--unexpired-alarms () 223 | "Rerturn a list of unexpired alarms" 224 | (let ((now (current-time))) 225 | (seq-filter (lambda (alarm) 226 | (time-less-p now (plist-get alarm :time))) 227 | alarm-clock--alist))) 228 | 229 | (defun alarm-clock--remove-expired () 230 | "Remove expired alarms." 231 | (setq alarm-clock--alist (alarm-clock--unexpired-alarms))) ;; (length (alarm-clock--unexpired-alarms)) 232 | 233 | (defun alarm-clock--ding-on-timer (program sound repeat) ;; (alarm-clock--ding) 234 | "Play the alarm sound asynchronously until stopped" 235 | ;; (message "(alarm-clock--ding-on-timer %s %s %d)" program sound repeat) 236 | (when (and (not alarm-clock--stopped) 237 | (> repeat 0)) 238 | (start-process "Alarm Clock" nil program sound) 239 | (run-at-time 2 240 | nil 241 | (lambda (repeat) (alarm-clock--ding-on-timer program sound repeat)) 242 | (- repeat 1) 243 | ))) 244 | 245 | (defun alarm-clock--ding () 246 | "Play ding. 247 | In osx operating system, 'afplay' will be used to play sound, 248 | and 'mpg123' in linux or windows" 249 | (let ((program (cond ((eq system-type 'darwin) "afplay") 250 | ((eq system-type 'gnu/linux) "mpg123") 251 | ((eq system-type 'windows-nt) "mpg123") 252 | ((eq system-type 'cygwin) "mpg123") 253 | (t ""))) 254 | (sound (expand-file-name alarm-clock-sound-file))) 255 | (when (and (executable-find program) 256 | (file-exists-p sound)) 257 | (setq alarm-clock--stopped nil) 258 | (run-at-time 259 | "0" 260 | nil 261 | (lambda (repeat) (alarm-clock--ding-on-timer program sound repeat)) 262 | alarm-clock-play-sound-repeat)))) 263 | 264 | (defun alarm-clock--system-notify (title message) 265 | "Notify with formatted TITLE and MESSAGE by the system notification feature." 266 | (let ((program (cond ((eq system-type 'darwin) "terminal-notifier") 267 | ((eq system-type 'gnu/linux) "notify-send") 268 | (t ""))) 269 | (args (cond ((eq system-type 'darwin) `("-title" ,title 270 | ,@(alarm-clock--get-macos-sender) 271 | "-message" ,message 272 | "-ignoreDnD")) 273 | ((eq system-type 'gnu/linux) (list "-u" "critical" title message))))) 274 | (when (executable-find program) 275 | (apply 'start-process (append (list title nil program) args))))) 276 | 277 | (defun alarm-clock--notify (title message) 278 | "Notify in status bar with formatted TITLE and MESSAGE." 279 | (and alarm-clock-play-auto-view-alarms 280 | (alarm-clock-list-view)) 281 | (when alarm-clock-play-sound 282 | (alarm-clock--ding)) 283 | (when (and alarm-clock-alert-notify (fboundp 'alert)) 284 | (alert message :title title)) 285 | (when alarm-clock-system-notify 286 | (alarm-clock--system-notify title message)) 287 | (message (format "[%s] - %s" title message))) 288 | 289 | ;;;###autoload 290 | (defun alarm-clock-restore () 291 | "Restore alarm clocks on startup." 292 | (interactive) 293 | (alarm-clock--kill-all) 294 | (let* ((file alarm-clock-cache-file) 295 | (alarm-clocks (unless (zerop (or (nth 7 (file-attributes file)) 0)) 296 | (with-temp-buffer 297 | (insert-file-contents file) 298 | (read (current-buffer)))))) 299 | (when alarm-clocks 300 | (dolist (alarm alarm-clocks) 301 | ;; call non-interactive alarm clock set to avoid overwriting the alist 302 | (alarm-clock--set (parse-iso8601-time-string (plist-get alarm :time)) 303 | (plist-get alarm :message))))) 304 | (alarm-clock-list-view)) 305 | 306 | (defun alarm-clock--formatted-cache () 307 | "Return the cachable list of alarms" 308 | ;; (pp (alarm-clock--cache-formatted)) 309 | (seq-map (lambda (alarm) (list :time (format-time-string "%FT%T%z" (plist-get alarm :time)) 310 | :message (plist-get alarm :message))) 311 | (alarm-clock--unexpired-alarms))) 312 | 313 | ;;;###autoload 314 | (defun alarm-clock-save () 315 | "Save alarm clocks to the alarm clock cache file." 316 | (interactive) 317 | (let ((alarm-clocks (alarm-clock--formatted-cache))) 318 | (with-current-buffer (find-file-noselect alarm-clock-cache-file) 319 | (kill-region (point-min) (point-max)) 320 | (insert ";; Auto-generated file; don't edit\n") 321 | (pp alarm-clocks (current-buffer)) 322 | (save-buffer) ; use save-buffer so we get a ~ backup file 323 | (kill-buffer (current-buffer))))) 324 | 325 | (defun alarm-clock--kill-all () 326 | "Kill all timers." 327 | (dolist (alarm alarm-clock--alist) 328 | (cancel-timer (plist-get alarm :timer))) 329 | (setq alarm-clock--alist nil)) 330 | 331 | (defalias 'alarm-clock-turn-autosave-on 'alarm-clock--turn-autosave-on) 332 | 333 | (defun alarm-clock--turn-autosave-on () 334 | "Enable saving the alarm when killing emacs" 335 | (add-hook 'kill-emacs-hook #'alarm-clock-save)) 336 | 337 | (defalias 'alarm-clock-turn-autosave-off 'alarm-clock--turn-autosave-off) 338 | 339 | (defun alarm-clock--turn-autosave-off () 340 | "Disable auto-saving the alarm when killing emacs" 341 | (remove-hook 'kill-emacs-hook #'alarm-clock-save)) 342 | 343 | (defun alarm-clock--get-macos-sender () 344 | "Get proper sender for notifying in MacOS" 345 | (when (not alarm-clock--macos-sender) 346 | (let* ((versions (split-string 347 | (shell-command-to-string "sw_vers -productVersion") 348 | "\\." t)) 349 | (major-version (string-to-number (car versions))) 350 | (minor-version (string-to-number (cadr versions)))) 351 | (unless (and (>= major-version 10) 352 | (>= minor-version 15)) 353 | (setq alarm-clock--macos-sender '("-sender" "org.gnu.Emacs"))))) 354 | alarm-clock--macos-sender) 355 | 356 | (provide 'alarm-clock) 357 | ;;; alarm-clock.el ends here 358 | --------------------------------------------------------------------------------