Pomodoros with org-timer
A New Year! Create the 2025.org
journal file, channel all that post-break energy, and get ready to be Super Productive™! 💪 Oh, what’s this… a post on how to track Pomodoros using org mode. 👀 Of course, I didn’t bite. Of course not.
Productivity people sure spend a lot of time writing a lot about little; and the Pomodoro technique is no exception. It can be summarized as:
Set a timer, get to work, take a break. Repeat, take a break.
There’s way too much productivity literature out there, and much of it comes down to: find a way to get your butt in the seat and start [writing/typing/reading/editing/…]. The new wave of productivity literature, or “anti-productivity” literature, tells us to face the facts: you’re never going to finish your TODO list, so give up already, be in the present, and focus on what’s most important (or, whatever, just be present).
Sometimes, I get in the flow easily, and don’t need any tricks. But other times, especially with those slightly-mundane-tasks-that-still-need-to-get-done, I need a little nudge not get started and not get distracted (by something more interesting which, in this case, may well be everything).
For me, the Pomodoro method is helpful for that purpose. And, because distraction abounds, it’s helpful to have the timer as well as the topic you’re supposed to be focused on displayed somewhere visible.
When I saw Charl’s post, I was delighted because:
- It relies on
org-mode
, which I already use; - it does everything I used
org-pomodoro
for, but more simply; - it provides an easy path towards integration with waybar (he uses xbar for macOS, but same idea); and,
- as observed by Charl, it does not interfere with other org clocks.
So, what follows here is a slight modification of Charl’s method, which in turn combines previous approaches by David Wilson (System Crafters) and Xiang Ji.
Emacs configuration: basic #
(require 'org-timer)
;; If you want sound when the pomodoro is over
(setq org-clock-sound (expand-file-name "~/sounds/bell.wav"))
;; And if you'd like to have a keyboard shortcut for starting a Pomodoro
(defun org-timer-start-pomodoro ()
(interactive)
(setq current-prefix-arg 25)
(call-interactively 'org-timer-set-timer))
(global-set-key (kbd "C-c P") 'org-timer-start-pomodoro)
Now, when you press C-c P
, a new pomodoro timer is started (you’ll see it in the mode line). You can also start it with org-timer-set-timer
. After 25 minutes, when the Pomodoro is done, a system notification is displayed. Tada!
Display pomodoro status on waybar #
To display org-timer’s status on waybar, we’ll query emacs (via emacsclient1) for its current value.
Here’s the function, org-timer-waybar-repr
, which generates the status text:
(defun html-escape-string (string)
"Escape special characters in STRING for HTML."
(let ((result ""))
(dolist (char (string-to-list string) result)
(setq result
(concat result
(pcase char
(?\& "&")
(?\< "<")
(?\> ">")
(?\" """)
(?\' "'")
(_ (char-to-string char))))))))
(defun org-timer-minutes-to-string ()
"Remaining org-timer minutes, rounded to nearest minute, as string."
(let* ((time-string (org-timer-value-string))
(parts (split-string time-string ":"))
(hours (string-to-number (nth 0 parts)))
(minutes (string-to-number (nth 1 parts)))
(seconds (string-to-number (nth 2 parts)))
(total-minutes (+ (* hours 60) minutes (/ seconds 60.0))))
(number-to-string (round total-minutes))))
(defun org-timer-waybar-repr ()
"Format org-timer status for waybar"
(if (or (not (boundp 'org-timer-countdown-timer)) (not org-timer-countdown-timer))
"🤗"
(html-escape-string
(concat
"🍅 "(org-timer-minutes-to-string)
" 🎯 " (org-link-display-format
(substring-no-properties org-timer-countdown-timer-title))))))
Next, we need a script, say org-timer-remaining
, to access this text:
#/bin/sh
emacsclient --eval '(org-timer-waybar-repr)' | sed 's/"//g'
(That sed
bit is to strip the surrounding quotes from the resulting string.)
To display it in waybar, we need to tell waybar to call the script at a regular interval. In ~/.config/waybar/config
:
{
...,
"modules-left": [..., "custom/org_timer"],
...,
"custom/org_timer": {
"exec": "~/scripts/org-timer-remaining",
"interval": 30,
"signal": 8
}
}
Now, after starting a timer, you should see something like:
One little trick 🪄: do you notice the "signal": 8
above? You don’t need it, but it’s a mechanism provided by waybar
for externally refreshing a widget. In this case, if we send a specific kill signal, the org-timer display will reload:
pkill -RTMIN+8 waybar
You can therefore add the following to your emacs configuration to refresh waybar
the moment a pomodoro is created:
(add-hook 'org-timer-set-hook
(lambda ()
(start-process "waybar-timer-trigger" nil "pkill" "-RTMIN+8" "waybar")))
Things that didn’t work #
I’d have liked a tomato-themed notification for when my pomodoro expired. I can use notify-send
to make that happen:
(defun pomo-notify (MSG &optional TIMEOUT)
"""Display pomodoro notification with notify-send"""
(apply
'start-process
"notify-send" nil "notify-send"
`(,@(when TIMEOUT (list (format "--expire-time=%d" (* 1000 TIMEOUT))))
,MSG)))
(add-hook 'org-timer-done-hook (lambda () (pomo-notify "🍅 org-timer done!")))
However, now I have two notifications popping up: mine, and the org-timer one. I tried to silence the org-timer notification using advice:
(defun suppress-org-notify (orig-fun &rest args)
(cl-letf (
((symbol-function 'org-show-notification) (lambda (&rest _) (ignore)))
)
(apply orig-fun args)))
(advice-add 'org-timer--run-countdown-timer :around #'suppress-org-notify)
Alas, my advice fell on deaf ears. Do you perhaps know how to make it work? Let me know!
The end #
En dit is dit. Geniet die tamaties!
Waybar update #
I didn’t like that the monitoring script talked to emacs every 30s, regardless of whether the timer is running. So, here is a modified version that triggers waybar updates only when the timer is started, and pauses them when the timer ends:
org-timer-remaining
:
#/bin/sh
function print_status() {
REPR=`emacsclient --eval '(org-timer-waybar-repr)' | sed 's/"//g'`
echo $REPR
}
print_status
MONITORING="no"
trap 'MONITORING="yes"' USR1
trap 'MONITORING="no"; print_status' USR2
while true; do
if [ $MONITORING == "yes" ]; then
print_status
fi
sleep 30 &
wait $!
done
Because the script now does the polling internally, you invoke it slightly differently from waybar:
"custom/org_timer": {
"exec": "~/scripts/org-timer-remaining",
}
And, because the script handles the signals (instead of waybar), we need to update our hooks too:
(add-hook 'org-timer-set-hook
(lambda ()
(start-process
"waybar-monitor-start" nil "pkill" "-USR1" "-f" "sh .*org-timer-remaining")))
(add-hook 'org-timer-done-hook
(lambda ()
(start-process
"waybar-monitor-pause" nil "pkill" "-USR2" "-f" "sh .*org-timer-remaining")))
-
Since Emacs 29, the emacsclient server is started automatically! 🙌 ↩︎