Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# egg-timer.el

## Introduction to the fork

This is a fork of the excellent and original egg-timer.el because I really liked the simplicity of the interface here but I wanted to be able to free form enter text for choosing a timer's time as well as select from a list with completion, also I wanted to make it work on systems where I didn't have access to DBUS notifications (remote terminal machines, windows machines, etc) so I wanted to
make the notification system more customisable if I wanted too, and then I wanted the ability to list/modify the timers running... so I sort of took the original incredibly simple and beautiful code and mangled it with complexity.

But the users experience of it should remain very simple.

This version exposes four functions to the user:

* `egg-timer-schedule` lets you schedule a timer either from a completing read or entering any time string that looks right enough according to `timer-duration-words` so writing "3 seconds" or "3 hours" would give you a timer for "3 second" or "3 hour" because it trims off -s and things. If there is any text after the second word this is appended to the label of your timer as a reason for its existence, so "25 minutes turn down oven" would let you know why you got a notification at that point with the context.
* `egg-timer-p` tells you if you have running timers
* `egg-timer-list` lists your running timers
* `egg-timer-cancel` lets you choose a timer to cancel

There is also a variable `egg-timer-notification-method` that lets you choose now notifications work, by default it uses `notifications-notify` but it can be set to `'buffer` to pop up buffer showing when the timer completes or `'message` to just display a message. If it is linked to a function then it will call that function and pass it the label of the timer to display, this can be either a lambda or a #'my-function

Everything below here is the original documentation

---

[![MELPA](https://melpa.org/packages/egg-timer-badge.svg)](https://melpa.org/#/egg-timer)
Use Emacs to set timers.

Expand Down
300 changes: 284 additions & 16 deletions egg-timer.el
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

;; Author: William Carroll <wpcarro@gmail.com>
;; URL: https://github.com/wpcarro/egg-timer.el
;; Version: 0.0.1
;; Version: 0.0.2
;; Package-Requires: ((emacs "25.1"))

;; This file is NOT part of GNU Emacs.
Expand Down Expand Up @@ -34,6 +34,21 @@
;; message when the timer completes. Since this depends on
;; `notifications-notify', ensure that your Emacs is compiled with dbus
;; support.
;;

;; 2023-12-04 https://github.com/twitchy-ears/egg-timer.el
;;
;; I really liked the simplicity of the interface here but I wanted to
;; be able to free form enter text for choosing a timer's time as well
;; as select from a list with completion, also I wanted to make it
;; work on systems where I didn't have access to DBUS notifications
;; (remote terminal machines, windows machines, etc) so I wanted to
;; make the notification system more customisable if I wanted too, and
;; then I wanted the ability to list/modify the timers running... so I
;; sort of took the original incredibly simple and beautiful code and
;; mangled it with complexity.
;;
;; But the users experience of it should remain very simple.

;;; Code:

Expand Down Expand Up @@ -68,27 +83,280 @@
:type '(alist :key-type string :value-type integer)
:group 'egg-timer)

(defun egg-timer-do-schedule (minutes &optional label)
"Schedule a timer to go off in MINUTES.
Provide LABEL to change the notifications, which defaults to \"MINUTES
minutes\"."
(cl-defstruct egg-timer-event
"An egg timer event that is stored in the list of events"
id
timer-item
label)

(defvar egg-timer-notification-method 'notify
"Backup notification method if 'notifications-notify' isn't
available, can be one of:
* 'notify (use notifications-notify if possible)
* 'buffer (pop up a buffer with a message in)
* 'message (just use the message function)
* #'your-function in which case it'll run the function and
pass it the label

It will fall back to message if nothing else works.")

(defvar egg-timer-running-timers nil "Stores the running timers for egg-timer.el")

(defun egg-timer-p ()
"Return nil if an egg timer isn't running, t if one is."
(interactive)
(unless label (setq label (format "%s minutes" minutes)))
(run-at-time
(format "%s minutes" minutes)
nil
(lambda ()
(notifications-notify
:title "egg-timer.el"
:body (format "%s timer complete." label))))
(message "%s timer scheduled." label))
(if (and (not (equal egg-timer-running-timers nil))
(listp egg-timer-running-timers)
(not (seq-empty-p egg-timer-running-timers)))

;; Yes timers
(progn
(if (called-interactively-p 'any)
(message "egg-timer-p: t"))
t)

;; No timers
(progn
(if (called-interactively-p 'any)
(message "egg-timer-p: nil"))
nil)))

(defun egg-timer-list ()
"Returns a list of all currently running egg-timer-events if
called interactively messages this list formatted with
'<label> (ID: <id>)' when called programatically returns an
alist of events each of (label id)."
(interactive)
(when (egg-timer-p)
(if (called-interactively-p 'any)
(let ((tlist (mapcar (lambda (x)
(format "%s (ID: %s)"
(egg-timer-event-label x)
(egg-timer-event-id x)))
egg-timer-running-timers)))
(message "%s" tlist))

(mapcar (lambda (x)
(cons (egg-timer-event-label x)
(egg-timer-event-id x)))
egg-timer-running-timers))))

(defun egg-timer-cancel (&optional timer-choice)
"Offers the user a choice of egg-timer-event to cancel, if called programmatically requires an ID"
(interactive)
(when (egg-timer-p)

;; If we don't get passed one then ask for one
(if (and (not timer-choice)
(called-interactively-p 'any))
(let* ((choices (mapcar
(lambda (x)
(concat
(format "%s" (egg-timer-event-label x))
;; Lets us have an ID after the label but not
;; clutter the view in the completing-read
(propertize (format " (%s)" (egg-timer-event-id x))
'invisible t)))
egg-timer-running-timers)))
(setq timer-choice (completing-read "Cancel: " choices))))

;; If we get one:
(when timer-choice

;; Select the ID which is listed in the brackets after the label
(string-match "(\\([[:graph:]]+\\))$"
timer-choice)

(let ((timerid (match-string 1 timer-choice)))

;; if we get a timerid then search for it
(when timerid
(let ((tstruct (seq-find
(lambda (x)
(equal (egg-timer-event-id x)
timerid))
egg-timer-running-timers)))

;; If we found an egg-timer-event struct then cancel its
;; timer and remove it from the list by ID.
(when tstruct
(cancel-timer (egg-timer-event-timer-item tstruct))
(setq egg-timer-running-timers
(seq-remove (lambda (x)
(equal (egg-timer-event-id x)
timerid))
egg-timer-running-timers)))))))))

(cl-defun egg-timer--timedesc-checker (timedesc)
"Accepts a string in the format of \"<unit> <measure>\" where the unit
should be an integer and the measure should be from 'timer-duration-words', it will also accept these words pluralised with an s on the end and remove it.

Returns a list of '(<unit> <measure> <additional-commentary>)
or nil in the event of bad formatting.

Anything after a detected measure is deemed a part of
additional-commentary and added to the label of the timer so its
clear what the timer is for."
(cond

;; Just get an int? Assume minutes
((if (integerp timedesc)
(cl-return-from egg-timer--timedesc-checker (cons timedesc "minute"))))

;; Get a string we can parse? Parse and return, this will attempt
;; to drop s from the end of strings because none of the
;; timer-duration-words has it by default. This is a dangerous
;; assumption based on English named units.
((if (string-match "^\\([[:digit:]]+\\)[[:space:]]+\\([[:graph:]]+?\\)s??\\([[:space:]]+.+\\)??$"
timedesc)
(let* ((unit (string-to-number (match-string 1 timedesc)))
(measure (match-string 2 timedesc))
(commentary (if (match-string 3 timedesc)
(string-trim (match-string 3 timedesc))
nil))
(exists (assoc measure timer-duration-words)))
(message "got '%s' '%s' '%s'" unit measure commentary)
(if (and (not (equal unit nil))
(not (equal exists nil)))
(list unit measure commentary)))))

;; Otherwise try and extract a number from whatever we get and assume its
;; minutes
((stringp timedesc)
(progn
;; (message "Guessing based on a string and doing string-to-number")
(let ((num (string-to-number timedesc)))
(if num
(cl-return-from egg-timer--timedesc-checker
(list num "minute" nil))))))

;; Unsure? Return nil
t nil))

(cl-defun egg-timer--display-message (id)
"Takes an 'id' argument that must match one of the :id elements of an
egg-timer-event stored in 'egg-timer-running-timers'.

Attempts to display the message of that event using the method
specified in the 'egg-timer-notification-method' variable.

Then attempts to remove the egg-timer-event from the list of
running timers regardless of if it thinks it notifed or not, the
moment has passed."
(unless (egg-timer-p)
;; (message "egg-timer--display-message: egg-timer-p says nil so failing early")
(cl-return-from egg-timer--display-message nil))

;; Retrieve our egg-timer
(let ((tstruct (seq-find (lambda (x)
(equal (egg-timer-event-id x)
id))
egg-timer-running-timers)))
(when tstruct
(let ((label (egg-timer-event-label tstruct)))
(cond

;; If the user has bound
((functionp egg-timer-notification-method)
(funcall egg-timer-notification-method label))

;; w32 notification, need IDs storing and all sorts of faff.
;; ((string-equal system-type "windows-nt")
;; ;; (message "Attempting a w32 notification")
;; (w32-notification-notify :title "egg timer complete" label))

((equal egg-timer-notification-method 'buffer)
(let ((eggwin (with-temp-buffer-window
"*egg-timer*"
nil
(lambda (win bodyres)
win)
(princ label))))
(shrink-window-if-larger-than-buffer eggwin)))

((equal egg-timer-notification-method 'message)
(message "EGG-TIMER: %s" label))

;; Have dbus? Lets go, this should be default for
;; *nix GUI emacs
((and (equal egg-timer-notification-method 'notify)
(not (equal (notifications-get-capabilities) nil)))

(notifications-notify
:title "egg-timer.el"
:body (format "%s timer complete." label)))

;; Fallback
(t (message "EGG-TIMER: %s" label)))

;; Cleanup
(setq egg-timer-running-timers
(seq-remove (lambda (x)
(equal (egg-timer-event-id x)
id))
egg-timer-running-timers))))))

(defun egg-timer-do-schedule (timedesc &optional label)
"Schedule a timer to go off in TIMEDESC. This should be a string in the
format of units and a measure featured in 'timer-duration-words', can be
given as an integer which is presumed to be minutes.
Provide LABEL to change the notifications, which defaults to \"TIMEDESC\"
with the correct amount of units on the end."
;; (interactive)
(let ((timer-details (egg-timer--timedesc-checker timedesc)))
(when (and (not (equal timer-details nil))
(= (length timer-details) 3))
(let* ((units (nth 0 timer-details))
(measure (nth 1 timer-details))
(additional (nth 2 timer-details))
(time-spec (format "%i %s" units measure))
(actual-label
(cond ((not (equal label nil))
label)
(additional
(format "%i %s %s" units measure additional))
(t (format "%i %s" units measure)))))

;; (message "%s / %s / %s" units measure actual-label)

;; Set the timer creating an egg-timer-event struct and
;; pushing it onto the egg-timer-running-timers list.
(let* ((timer-id (md5
(format "%s-%s"
(format-time-string "%s" (current-time))
(recent-keys))))
(event (run-at-time
time-spec ;; When do we run
nil ;; No repeat

;; Pass the ID so it can find the label and remove it
`(lambda () (egg-timer--display-message ,timer-id))))

;; construct the structure.
(tstruct (make-egg-timer-event
:id timer-id
:timer-item event
:label actual-label)))
(push tstruct egg-timer-running-timers))

;; Tell the user we did it
(message "%s timer scheduled." actual-label)))))

(defun egg-timer-schedule ()
"Select and schedule a timer for a given set of time intervals."
"Select and schedule a timer for a given set of time intervals.

Offers a choice of strings from 'egg-timer-intervals' but is happy to try and parse what the user requested in the form of strings that look like this:
'<unit> <measure> [<optional description>]'

e.g. '3 seconds' or '3 hours' or '25 minutes turn down oven'
produce timers that run for 3 second, or 3 hour, or 25 minutes
and has the option description of 'turn down oven'."
(interactive)
(let* ((key (completing-read "Set timer for: " egg-timer-intervals))
(val (alist-get key egg-timer-intervals nil nil #'string=)))
(egg-timer-do-schedule val key)))
(if val
(egg-timer-do-schedule val key)
(egg-timer-do-schedule key))))

(provide 'egg-timer)
;;; egg-timer.el ends here