branch: externals/tmr commit 6602891c8f18a9463a9be8ef14db3a09920be124 Author: Damien Cassou <dam...@cassou.me> Commit: Protesilaos Stavrou <i...@protesilaos.com>
Decompose tmr.el into several files This commit also adds 3 hooks: - tmr-timer-created-functions - tmr-timer-completed-functions - tmr-timer-cancelled-functions --- tmr-notification.el | 66 ++++++++++++++++++++++ tmr-sound.el | 64 ++++++++++++++++++++++ tmr.el | 155 +++++++++++++++++++--------------------------------- 3 files changed, 186 insertions(+), 99 deletions(-) diff --git a/tmr-notification.el b/tmr-notification.el new file mode 100644 index 0000000000..d0647dfefd --- /dev/null +++ b/tmr-notification.el @@ -0,0 +1,66 @@ +;;; tmr-notification.el --- Display timers in a notification list -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; Author: Protesilaos Stavrou <i...@protesilaos.com>, +;; Damien Cassou <dam...@cassou.me> +;; Maintainer: Protesilaos Stavrou <i...@protesilaos.com> +;; URL: https://git.sr.ht/~protesilaos/tmr +;; Mailing list: https://lists.sr.ht/~protesilaos/tmr +;; Version: 0.2.3 +;; Package-Requires: ((emacs "27.1")) + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Provides a function to display a desktop notification. This is +;; useful to get a passive popup when a timer completes. Read +;; (info \"(elisp) Desktop Notifications\") for details. + +;;; Code: +(require 'tmr) +(require 'tmr-sound) +(require 'notifications) + +(defcustom tmr-notification-urgency 'normal + "The urgency level of the desktop notification. +Values can be `low', `normal' (default), or `critical'. + +The desktop environment or notification daemon is responsible for +such notifications." + :type '(choice + (const :tag "Low" low) + (const :tag "Normal" normal) + (const :tag "Critical" critical)) + :group 'tmr) + +;;;###autoload +(defun tmr-notification-notify (timer) + "Dispatch a notification for TIMER. + +Read: (info \"(elisp) Desktop Notifications\") for details." + (let ((title "TMR May Ring (Emacs tmr package)") + (body (tmr--long-description-for-completed-timer timer))) + (notifications-notify + :title title + :body body + :app-name "GNU Emacs" + :urgency tmr-notification-urgency + :sound-file tmr-sound-file))) + +(provide 'tmr-notification) +;;; tmr-notification.el ends here diff --git a/tmr-sound.el b/tmr-sound.el new file mode 100644 index 0000000000..be279e02d1 --- /dev/null +++ b/tmr-sound.el @@ -0,0 +1,64 @@ +;;; tmr-sound.el --- Play a sound -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; Author: Protesilaos Stavrou <i...@protesilaos.com>, +;; Damien Cassou <dam...@cassou.me> +;; Maintainer: Protesilaos Stavrou <i...@protesilaos.com> +;; URL: https://git.sr.ht/~protesilaos/tmr +;; Mailing list: https://lists.sr.ht/~protesilaos/tmr +;; Version: 0.2.3 +;; Package-Requires: ((emacs "27.1")) + +;; This file is NOT part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: +;; +;; Provides a function to play a configurable sound file. This is +;; useful to get an audio notification when a timer completes. This +;; feature requires "ffplay" (part of ffmpeg) to be in the path. + +;; Choose the sound file through the `tmr-sound-file' option: if its +;; value is nil or if the file is not found, no sound will be played. + +;;; Code: +(require 'tmr) + +(defcustom tmr-sound-file + "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga" + "Path to sound file used by `tmr--play-sound'. +If nil, don't play any sound." + :type '(choice + file + (const :tag "Off" nil)) + :group 'tmr) + +;; NOTE 2022-04-21: Emacs has a `play-sound' function but it only +;; supports .wav and .au formats. Also, it does not work on all +;; platforms and Emacs needs to be compiled --with-sound capabilities. +;;;###autoload +(defun tmr-sound-play (&optional _timer) + "Play `tmr-sound-file' using the 'ffplay' executable (ffmpeg). +TIMER is unused." + (when-let* ((sound tmr-sound-file)) + (when (file-exists-p sound) + (unless (executable-find "ffplay") + (user-error "Cannot play %s without `ffplay'" sound)) + (call-process-shell-command + (format "ffplay -nodisp -autoexit %s >/dev/null 2>&1" sound) nil 0)))) + +(provide 'tmr-sound) +;;; tmr-sound.el ends here diff --git a/tmr.el b/tmr.el index 47269d8d7b..7648f09095 100644 --- a/tmr.el +++ b/tmr.el @@ -111,32 +111,10 @@ ;;; Code: -(require 'notifications) - (defgroup tmr () "TMR May Ring: set timers using a simple notation." :group 'data) -(defcustom tmr-sound-file - "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga" - "Path to sound file used by `tmr--play-sound'. -If nil, don't play any sound." - :type '(choice file - (const :tag "Off" nil)) - :group 'tmr) - -(defcustom tmr-notification-urgency 'normal - "The urgency level of the desktop notification. -Values can be `low', `normal' (default), or `critical'. - -The desktop environment or notification daemon is responsible for -such notifications." - :type '(choice - (const :tag "Low" low) - (const :tag "Normal" normal) - (const :tag "Critical" critical)) - :group 'tmr) - (defcustom tmr-descriptions-list (list "Boil water" "Prepare tea" "Bake bread") "Optional description candidates for the current `tmr'. These are provided as completion candidates when `tmr' is called @@ -145,11 +123,31 @@ used." :type '(repeat string) :group 'tmr) -(defcustom tmr-notify-function #'tmr-notifications-notify - "Function called to send notification. -It should take two string arguments: the title and the message." - :type 'function - :group 'tmr) +(defcustom tmr-timer-created-functions + (list #'tmr-print-message-for-created-timer) + "Functions to execute when a timer is created. +Each function must accept a timer as argument." + :type 'hook + :options '(tmr-print-message-for-created-timer)) + +(declare-function tmr-sound-play "ext:tmr-sound.el") +(declare-function tmr-notification-notify "ext:tmr-notification.el") + +(defcustom tmr-timer-completed-functions + (list #'tmr-print-message-for-completed-timer + #'tmr-sound-play + #'tmr-notification-notify) + "Functions to execute when a timer is completed. +Each function must accept a timer as argument." + :type 'hook + :options (list #'tmr-print-message-for-completed-timer + #'tmr-sound-play + #'tmr-notification-notify)) + +(defcustom tmr-timer-cancelled-functions nil + "Functions to execute when a timer is created. +Each function must accept a timer as argument." + :type 'hook) (cl-defstruct (tmr-timer (:constructor tmr--timer-create) @@ -187,6 +185,20 @@ It should take two string arguments: the title and the message." (format " [%s]" (propertize description 'face 'bold)) "")))) +(defun tmr--long-description-for-completed-timer (timer) + "Return a human-readable description of completed TIMER. +This includes the creation and completion dates as well as the +optional `tmr--timer-description'." + (let ((start (tmr--format-creation-date timer)) + (end (tmr--format-end-date timer)) + (description (tmr--timer-description timer))) + (format "Time is up!\n%s%s %s\n%s %s" + (if description (format "%s\n" description) "") + (propertize "Started" 'face 'success) + start + (propertize "Ended" 'face 'success) + end))) + (defun tmr--format-creation-date (timer) "Return a string representing when TIMER was created." (tmr--format-time (tmr--timer-creation-date timer))) @@ -221,56 +233,6 @@ It should take two string arguments: the title and the message." ("w" (user-error "TMR Made Ridiculous; append character for [m]inutes, [h]ours, [s]econds")) (_ (* num 60))))))) -;; NOTE 2022-04-21: Emacs has a `play-sound' function but it only -;; supports .wav and .au formats. Also, it does not work on all -;; platforms and Emacs needs to be compiled --with-sound capabilities. -(defun tmr--play-sound () - "Play `tmr-sound-file' using the 'ffplay' executable (ffmpeg)." - (when-let* ((sound tmr-sound-file)) - (when (file-exists-p sound) - (unless (executable-find "ffplay") - (user-error "Cannot play %s without `ffplay'" sound)) - (call-process-shell-command - (format "ffplay -nodisp -autoexit %s >/dev/null 2>&1" sound) nil 0)))) - -(defun tmr-notifications-notify (title message) - "Dispatch notification titled TITLE with MESSAGE via D-Bus. - -Read: (info \"(elisp) Desktop Notifications\") for details." - (notifications-notify - :title title - :body message - :app-name "GNU Emacs" - :urgency tmr-notification-urgency - :sound-file tmr-sound-file)) - -(defun tmr--notify-send-notification (title message) - "Send notification with TITLE and MESSAGE using `tmr-notify-function'." - (funcall tmr-notify-function title message)) - -(defun tmr--notify (timer) - "Send notification for TIMER." - (let* ((description (tmr--timer-description timer)) - (desc-plain (if description - (concat "\n" description) - "")) - (desc-propertized (if description - (concat " [" (propertize description 'face 'bold) "]") - "")) - (start (tmr--format-creation-date timer)) - (end (tmr--format-end-date timer))) - (setf (tmr--timer-donep timer) t) - (tmr--notify-send-notification - "TMR May Ring (Emacs tmr package)" - (format "Time is up!\nStarted: %s\nEnded: %s%s" start end desc-plain)) - (message - "TMR %s %s ; %s %s%s" - (propertize "Start:" 'face 'success) start - (propertize "End:" 'face 'error) end - desc-propertized) - (unless (plist-get (notifications-get-capabilities) :sound) - (tmr--play-sound)))) - (defvar tmr--timers nil "List of timer objects. Populated by `tmr' and then operated on by `tmr-cancel'.") @@ -288,7 +250,8 @@ completion." (if (not timer) (user-error "No `tmr' to cancel") (cancel-timer (tmr--timer-timer-object timer)) - (setq tmr--timers (cl-delete timer tmr--timers)))) + (setq tmr--timers (cl-delete timer tmr--timers)) + (run-hook-with-args 'tmr-timer-cancelled-functions timer))) (defun tmr--read-timer () "Let the user choose a timer among all timers. @@ -304,24 +267,13 @@ there are no timers, return nil." (selection (completing-read "Timer: " timer-descriptions nil t))) (cl-find selection timers :test #'string= :key #'tmr--long-description)))))) -(defun tmr--echo-area (time &optional description) - "Produce `message' for current `tmr' TIME. -Optionally include DESCRIPTION." - (let* ((specifier (substring time -1)) - (amount (substring time 0 -1)) - (start (tmr--format-time (current-time))) - (unit (pcase specifier - ("s" (format "%ss (s == second)" amount)) - ("h" (format "%sh (h == hour)" amount)) - (_ (concat time "m (m == minute)"))))) - (message "`tmr' started at %s for %s%s" - ;; Remember: these are just faces. Don't get caught in the - ;; semantics. - (propertize start 'face 'success) - (propertize unit 'face 'error) - (if description - (format " [%s]" (propertize description 'face 'bold)) - "")))) +(defun tmr-print-message-for-created-timer (timer) + "Show a `message' informing the user that TIMER was created." + (message "%s" (tmr--long-description timer))) + +(defun tmr-print-message-for-completed-timer (timer) + "Show a `message' informing the user that TIMER has completed." + (message "%s" (tmr--long-description-for-completed-timer timer))) (defvar tmr--duration-hist '() "Minibuffer history of `tmr' durations.") @@ -350,6 +302,11 @@ If DEFAULT is provided, use that as a default." tmr-descriptions-list nil nil nil 'tmr--description-hist def))) +(defun tmr--complete (timer) + "Mark TIMER as completed and execute `tmr-timer-completed-functions'." + (setf (tmr--timer-donep timer) t) + (run-hook-with-args 'tmr-timer-completed-functions timer)) + ;;;###autoload (defun tmr (time &optional description) "Set timer to TIME duration and notify after it elapses. @@ -381,10 +338,10 @@ command `tmr-with-description' instead of this one." :duration duration)) (timer-object (run-with-timer duration nil - #'tmr--notify timer))) + #'tmr--complete timer))) (setf (tmr--timer-timer-object timer) timer-object) - (tmr--echo-area time description) - (push timer tmr--timers))) + (push timer tmr--timers) + (run-hook-with-args 'tmr-timer-created-functions timer))) ;;;###autoload (defun tmr-with-description (time description)