alphapapa / hammy.el

Programmable, interactive interval timers (e.g. for working/resting)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Hammy.el

Hammy provides programmable, interactive interval timers for Emacs. They can be used, e.g. to alternate between working and resting periods, to remind yourself to stretch your legs, etc. Timers, known as “hammys”, are easily defined by the user to behave as desired, and they can be integrated into other programs by calling Lisp functions in the hammys’ definitions.

Contents

Installation

Hammy is distributed on MELPA, so it can be installed with M-x package-install RET hammy RET.

Other than that, the recommended way to install it is to use quelpa-use-package, like this:

(use-package hammy
  :quelpa (hammy :fetcher github :repo "alphapapa/hammy.el"))

Usage

First, define a hammy timer using the domain-specific language Hammy offers for this purpose. (The ones in the following examples are defined by default, so you can use them upon installation of the package.)

Let’s look at a classic example, the “pomodoro timer.” Here’s how it’s implemented in Hammy:

;; We name the timer with the Unicode TOMATO character, and propertize
;; it with a tomato-colored face.
(hammy-define (propertize "🍅" 'face '(:foreground "tomato"))
  :documentation "The classic pomodoro timer."
  :intervals
  (list
   (interval :name "Work"
             :duration "25 minutes"
             :before (do (announce "Starting work time.")
                         (notify "Starting work time."))
             :advance (do (announce "Break time!")
                          (notify "Break time!")))
   (interval :name "Break"
             :duration (do (if (and (not (zerop cycles))
                                    (zerop (mod cycles 3)))
                               ;; If a multiple of three cycles have
                               ;; elapsed, the fourth work period was
                               ;; just completed, so take a longer break.
                               "30 minutes"
                             "5 minutes"))
             :before (do (announce "Starting break time.")
                         (notify "Starting break time."))
             :advance (do (announce "Break time is over!")
                          (notify "Break time is over!")))))

Then you can use these commands, which will prompt you to choose a defined hammy:

hammy-startStart a hammy.
hammy-start-org-clock-inCall org-clock-in, then hammy-start. (The Org task will then automatically be clocked out during the hammy’s next interval (and when the hammy is stopped), and back in when the first interval resumes.)
hammy-nextAdvance to the next interval of a hammy (when it’s defined to not advance automatically).
hammy-togglePause or unpause a hammy.
hammy-adjustAdjust the durations of a hammy’s intervals (which can be reset later).
hammy-resetReset a hammy (useful when a hammy is defined to behave differently the longer it runs).
hammy-stopStop a hammy.

You may also use these commands:

hammy-modeShow the current hammy in the mode-line.
hammy-view-logShow the log buffer.

The mode-line looks like this (when no hammy is active, when one is active, and when one is “overdue”, waiting for the user to manually advance to the next interval):

images/mode-line.png

You can see the lighter prefix (🐹), the status (:, or ! when overdue), the current hammy’s name (Move), the current interval’s name (💺), and the time elapsed (prefixed by - when counting down, and + when overdue and counting up).

Of course, if you have something against hamsters, the lighter prefix can be customized, as well as various faces for parts of the mode line. See M-x customize-group RET hammy RET.

Examples

These examples have more detailed comments to explain how a hammy is defined.

This timer reminds you to stand up every so often (e.g. to prevent RSI):

(hammy-define "Move"
  :documentation "Don't forget to stretch your legs."
  :intervals
  ;; A list of intervals, each defined with the `interval' function.
  (list (interval
         ;; The name of the interval is a string, used when selecting
         ;; hammys and shown in the mode line.
         :name "💺"
         ;; The duration of the interval: a number of seconds, a string
         ;; passed to `timer-duration', or a function which returns such.
         :duration "45 minutes"
         ;; Optionally, a face in which to show the
         ;; interval's name in the mode line.
         :face 'font-lock-type-face
         ;; A list of actions to take before starting the interval
         ;; (really, one or a list of functions to call with the hammy
         ;; as the argument).  The `do' macro expands to a lambda,
         ;; which the interval's `before' slot is set to.  In its
         ;; body, we call two built-in helper functions.
         :before (do (announce "Whew!")
                     (notify "Whew!"))
         ;; We want this interval to not automatically advance to the
         ;; next one; rather, we want the user to call the
         ;; `hammy-next' command to indicate when the standing-up is
         ;; actually happening.  So we provide a list of actions to
         ;; take when it's time to advance to the next interval.  We
         ;; wrap the list in a call to the built-in `remind' function,
         ;; which causes the actions to be repeated every 10 minutes
         ;; until the user manually advances to the next interval.
         :advance (remind "10 minutes"
                          ;; Every 10 minutes, while the hammy is waiting
                          ;; to be advanced to the next interval, remind
                          ;; the user by doing these things:
                          (do (announce "Time to stretch your legs!")
                              (notify "Time to stretch your legs!")
                              (play-sound-file "~/Misc/Sounds/mooove-it.wav"))))
        (interval :name "🤸"
                  :duration "5 minutes"
                  :face 'font-lock-builtin-face
                  :before (do (announce "Mooove it!")
                              (notify "Mooove it!"))
                  ;; Again, the interval should not advance automatically
                  ;; to the next--the user should indicate when he's
                  ;; actually sat down again.  (If we omitted the
                  ;; `:advance' slot, the interval would automatically
                  ;; advance when it reached its duration.)
                  :advance (do (announce "Time for a sit-down...")
                               (notify "Time for a sit-down...")
                               (play-sound-file "~/Misc/Sounds/relax.wav")))))

Here’s a more interesting example, a “flywheel timer” (so called because it helps to build momentum), which interleaves rest periods with gradually lengthening work periods.

(hammy-define "Flywheel"
  :documentation "Get your momentum going!"
  :intervals
  (list
   (interval :name "Play"
             :face 'font-lock-type-face
             ;; The play/rest interval will always be 5 minutes long.
             :duration "5 minutes"
             ;; Before starting the interval, announce and notify, to
             ;; show the user that the interval has begun.
             :before (do (announce "Play time!")
                         (notify "Play time!"))
             ;; When the interval's time is up, remind the user every
             ;; 5 minutes to get back to work.
             :advance (remind "5 minutes"
                              (do (announce "Play time is over!")
                                  (notify "Play time is over!")
                                  (play-sound-file "~/Misc/Sounds/get-to-workin.wav"))))
   (interval :name "Work"
             :face 'font-lock-builtin-face
             ;; For the work interval, rather than the duration being
             ;; the same each time, it "climbs" from 5 minutes to 45
             ;; minutes in 5-minute steps, and then descends back to 5
             ;; minutes.  For this, we use the built-in helper
             ;; function `climb', which returns a lambda function
             ;; that, when called at "hammy time," returns the
             ;; appropriate duration each time this interval begins.
             :duration (climb "5 minutes" "45 minutes"
                              :descend t :step "5 minutes")
             :before (do (announce "Work time!")
                         (notify "Work time!"))
             :advance (remind "10 minutes"
                              (do (announce "Work time is over!")
                                  (notify "Work time is over!")
                                  (play-sound-file "~/Misc/Sounds/relax.wav")))))
  ;; The `complete-p' predicate returns non-nil when a full session
  ;; has been completed (i.e. when at least one cycle has been
  ;; completed, the work interval is active and has reached its
  ;; duration, and its duration is 5 minutes, in which case the work
  ;; interval will have "climbed" to 45 minutes and back down).

  ;; Note that the `do' macro expands to a lambda within the body of
  ;; which certain special forms are bound, including `hammy',
  ;; `cycles', `interval-name', and `current-duration', allowing
  ;; introspection at runtime.  The predicate is called at "hammy
  ;; time", i.e. when the timer's code is run between intervals to
  ;; determine what to do next.
  :complete-p (do (and (> cycles 1)
                       (equal interval-name "Work")
                       ;; The built-in `duration' function converts
                       ;; its argument to a number of seconds.
                       (equal current-duration (duration "5 minutes"))))
  ;; Then, when the hammy has completed all of its cycles, play this
  ;; sound to celebrate.
  :after (do (play-sound-file "~/Misc/Sounds/all-done.wav")))

Note the use of :descend t in the arguments to the climb function in the Work interval’s :duration slot: because of that, it may also be known as a “ladder timer” or a “hill-climb timer”, because after the work interval reaches the maximum duration of 45 minutes, it begins decreasing until it reaches the minimum duration. In this way, the user “spins up”, gaining momentum to build endurance, and then “spins down” to finish the session. This kind of timer may be helpful when working on large projects that are difficult to get started on.

Finally, an example of a “third time” timer, in which break periods are one-third as long as the last work interval:

(hammy-define "⅓-time"
  :documentation "Breaks that are ⅓ as long as the last work interval."
  :intervals
  (list
   (interval :name "Work"
             ;; It's intended that the user manually end this interval
             ;; when ready, but we specify a maximum of 90 minutes by
             ;; default.
             :duration "90 minutes"
             :before (do (announce "Starting work time (advance to break when ready).")
                         (notify "Starting work time (advance to break when ready)."))
             :advance (remind "10 minutes"
                              (do (let* ((current-duration (ts-human-format-duration
                                                            (float-time
                                                             (time-subtract (current-time) current-interval-start-time))))
                                         (message (format "You've worked for %s!" current-duration)))
                                    (announce message)
                                    (notify message)
                                    (when hammy-sound-end-work
                                      (play-sound-file hammy-sound-end-work))))))
   (interval :name "Break"
             :duration (do (pcase-let* ((`(,_interval ,start ,end) (car history))
                                        (work-seconds (float-time (time-subtract end start))))
                             (* work-seconds 0.33)))
             :before (do (let ((message (format "Starting break for %s."
                                                (ts-human-format-duration current-duration))))
                           (announce message)
                           (notify message)))
             :advance (remind "5 minutes"
                              (do (announce "Break time is over!")
                                  (notify "Break time is over!")
                                  (when hammy-sound-end-break
                                    (play-sound-file hammy-sound-end-break)))))))

Tips

Show hammy-mode in tab-bar

If you use tab-bar-mode, rather than showing the Hammy status in each window’s mode line, you might prefer to show it once, in the tab bar. This can easily be enabled like this:

(use-package tab-bar
  :config
  (setf mode-line-misc-info
        ;; When the tab-bar is active, don't show global-mode-string
        ;; in mode-line-misc-info, because we now show that in the
        ;; tab-bar using `tab-bar-format-align-right' and
        ;; `tab-bar-format-global'.
        (remove '(global-mode-string ("" global-mode-string))
                mode-line-misc-info))
  (unless (member 'tab-bar-format-global tab-bar-format)
    ;; Show `global-mode-string' in the tab bar.
    (setf tab-bar-format (append tab-bar-format '(tab-bar-format-align-right tab-bar-format-global)))))

Changelog

0.3

Additions

  • Built-in “1-shot” timer that prompts for a name and duration on each use.
  • Command hammy-status shows information about active hammys in the echo area.
  • Option hammy-mode-lighter-pie shows a “progress pie” next to the time remaining. (See associated faces for customization.)
    • Option hammy-mode-lighter-pie-height sets the size of the progress pie.
  • Option hammy-mode-lighter-seconds-format, used when the time remaining is less than one minute.
  • Faces hammy-mode-lighter-name and hammy-mode-lighter-interval.

Changes

  • Add 10-minute reminders to default pomodoro timer.
  • Command hammy-next interprets a numeric prefix argument as a number of minutes (rather than a number of seconds).
  • Internal optimization of mode line lighter.

Fixes

  • Command hammy-toggle now works correctly. (#11. Thanks to Edgaras1 for reporting.)
  • “⅓-time” timer toggles and resets properly. (#11. Thanks to Edgaras1 for reporting.)
  • Listify hammys’ stopped slot. (#13. Thanks to Rickyfs for reporting.)
  • Update mode line immediately when stopping a hammy.

0.2.1

Fixes

  • Command hammy-stop unsets the hammy’s current interval.
  • Command hammy-adjust accepts empty input to not change an interval’s duration.

0.2

Changes

  • Log “Stopped” when a hammy is stopped.

Fixes

  • Command hammy-start-org-clock-in when a hammy is not yet running.

0.1

First tagged version.

FAQ

Why are timers called hammys? Isn’t that silly?

Probably, but is it sillier than calling them tomatoes? Besides, it helps to distinguish them from Emacs’s timers, which are used in the implementation.

License

GPLv3

About

Programmable, interactive interval timers (e.g. for working/resting)

License:GNU General Public License v3.0


Languages

Language:Emacs Lisp 100.0%