When starting a long process, like building a Docker container (do we really need 60+ layers?), I prefix the shell command with beep, a shell script I wrote years ago to alert when the computer had completed the process. Without it, I might spend the rest of the day on a different
website er, project.
While you may appreciate script while working in the shell, I have found I wanted one for Emacs (especially since Emacs is currently single-threaded). Let’s make one.
In the code below, my goal is both to craft a solution, but make the code readable to people new to Emacs Lisp. I assume you know the basics, but hopefully it is readable nevertheless.
Make a Noise
The first step, in my opinion, is to make an auditory alert. The EmacsSpeak project has a number of good choices for alerts in WAV format.
(defvar beep-alert-sound-file (expand-file-name "~/other/hamacs/beep-notify.wav") "A WAV or AU file used at the completion of a function.")
The idea with this variable, is that I could easily add this to a customization layer if I made this into a proper Emacs package. Regardless, separating a variable like this, will make my life easier as I can set this variable to different values.
Of course, we need a function to use the variable, and to do the actual work of playing a WAV file. Emacs has a
play-sound-file function that I discovered after a web search. Otherwise, I was thinking that I would need to call an executable, like
(defun beep--beep () "Play a default notification sound file. Customize the variable, `beep-alert-sound-file' to adjust the sound." (play-sound-file beep-alert-sound-file))
If you aren’t too familiar with Emacs Lisp and its nomenclature, we typically prefix a short word or two as a namespace to all functions and variables, so in this case, I’m starting most things with
beep. The function,
beep--beep has two dashes, and that is another convention that tells others that the function isn’t intended to be used by other functions, and exists as a short of helper function to this
That said, the code in this essay is intended for you, gentle reader, to steal and modify according to your personal preferences.
Speak a Message
While a beep is useful, we can expand this with a spoken message. Until the fine folks at EmacsSpeaks puts together a smaller package for embedded projects like this, we’ll just call an operating system executable.
We could use the antiquated-sounding espeak project, on a Mac laptop, I’ll call the
(defvar beep-speech-executable "say %s" "An OS-dependent shell string to speak. Replaces `%s' with a phrase.")
I will need to come up with a better alternative for my Linux laptop.1
Like the previous section, pulling out this customization into a variable will make it easier in my
~/.emacs init file to change a functions behavior based on whether I am running this on my Mac or Linux laptops.
This function will actually execute the command when given a phrase to speak:
(defun beep--speak (phrase) "Call a program to speak the string, PHRASE. Customize the variable, `beep-speech-executable'." (let ((command (format beep-speech-executable phrase))) (shell-command command)))
The first thing in that function after the documentation string is
let, which is Lisp’ way of defining some function-local variables, in this case, the variable
command, and I’m assigning it the value of the results of
format, which combines the executable string defined previously, with the
phrase parameter, according to some rules (notably substituting the
%s string with the value of
In Emacs, the
shell-command is a easiest way to run a shell command. In this case, I don’t care about the output results (as the interaction is purely auditory), nor the exit code, or anything like that.
(beep--speak "How do I sound?")
What about a visual clue? Sure, calling
message would be fine, unless I’m using another application other than Emacs. I’ve looked at the built-in notifications-notify function, but that won’t work on my Mac laptop (nor does it work on my Linux system either). Most operating systems have this feature, but of course, I will need to code an abstraction to account for the variations. Up to this point, I’ve been trying to work with stock Emacs, but John Wiegley has done the work with his Alert package.
After installing it, we load it into Emacs with a
require and configure it for my Mac:
(use-package alert :init (setq alert-default-style 'osx-notifier))
Then the following will display a message:
(alert "The function has completed." :title "All Done")
Note: Getting this to work with Linux was a bit more involved.2
When Finished Function
Let’s string these functions together, which as you can see, is pretty trivial:
(defun beep--when-finished (phrase) "Notify us with string, PHRASE, to grab our attention. Useful after a long process has completed, but use sparingly, as this can be pretty distracting." (message phrase) (when (functionp 'alert) (alert phrase :title "Completed")) (beep--beep) (beep--speak phrase))
And let’s see it in action:
(beep--when-finished "I have published the essay!")
So, how do we apply this? I have some calls to
compile that take a long time, so let’s make a new function,
compile-and-notify that acts exactly the same, but alerts after completion. In the code below, instead of calling the
compile function, I use Swiper’s counsel-compile which gets a list of targets from the
Makefile (or equivalent), and as I always use this in a particular way, I’m also going to insist on the project’s root as the base directory (you may have other ideas for starting your compilations, as this is just an idea):
(defun compile-and-notify () "Call `compile' and notify us when finished. See `beep--when-finished' for details." (interactive) (let ((default-directory (projectile-project-root))) (call-interactively 'compile) (beep--when-finished "The compile command has finished.")))
And I suppose we need a
recompile-and-notify command as well:
(defun recompile-and-notify () "Call `recompile' and notify us when finished. See `beep--when-finished' for details." (interactive) (let ((default-directory (projectile-project-root))) (call-interactively 'recompile) (beep--when-finished "The compile command has finished.")))
As opposed to many programming languages, Emacs Lisp expands the user interaction with functions with the
interactive macro, and for some functions, like the original
compile function, acquiring all the required parameters would be a lot of copy/pasting. Fortunately, Emacs supplies a function,
call-interactively which will take care of whatever user interaction the function would normally process.
So let’s add another simple helper function to make creating wrapper functions easy.
(defun beep--after-function (func) "Call the function, FUNC, interactively, and notify us when completed." (call-interactively func) (beep--when-finished (format "The function, %s, has finished." func)))
But… if the function immediately returns, why bother getting alerted in such an obtrusive way? Let’s make a slight modification:
(defvar beep-func-too-long-time 5 "The number of seconds a function runs before it is considered taking too much time, and needing to be alerted when it has finished.") (defun beep--after-function (func) "Call the function, FUNC, interactively, and notify us when completed." (let ((start-time (current-time)) duration) (call-interactively func) (setq duration (thread-first (current-time) (time-subtract start-time) decode-time first)) (when (> duration beep-func-too-long-time) (beep--when-finished (format "The function, %s, has finished." func)))))
With this in place, our
recompile function could be written:
(defun recompile-and-notify () "Call `recompile' and notify us when finished. See `beep--when-finished' for details." (interactive) (beep--after-function 'recompile))
Since I use these often, I have keybindings:
(global-set-key (kbd "C-c c") 'recompile-and-notify) (global-set-key (kbd "C-c C") 'compile-and-notify)
You can imagine the functions we could wrap this way, but Emacs has a another way to make this new function of ours even easier to use.
Attaching the Beep
While a nifty function to have, a goal I had in writing this essay was to compare the Emacs version with the Shell version. On the command line, I would have to type something like:
beep -m "The make process" make -k docker-image
In other words, I need to remember to call it. If I forget, I can type in the terminal
beep, and get a notification when it is done. In Emacs, however, we have a better approach.
Instead of creating a bunch of wrapper functions as we did in the previous section, in Emacs we can attach our
beep--when-finished function to existing functions using
advice-add. This approach means that when I change the behavior of other functions, which also means I don’t have to change any keybindings as well.
First, I need a slightly different format for our
beep--after-function function that doesn’t make the assumption that the function we want to wrap is called interactively:
(defun beep-when-runs-too-long (orig-function &rest args) "Notifies us about the completion of ORIG-FUNCTION. Useful as after advice to long-running functions, for instance: (advice-add 'org-publish :around #'beep-when-runs-too-long)" (let ((start-time (current-time)) duration) (apply orig-function args) (setq duration (thread-first (current-time) (time-subtract start-time) decode-time first)) (when (> duration beep-func-too-long-time) (beep--when-finished (format "The function, %s, has finished." (beep--extract-function-name orig-function))))))
Now, we attach it to existing function like this:
(advice-add 'rg-project :around 'beep-when-runs-too-long)
After running this code, if you look at a functions documentation (using
C-h f), it should mention this change in its behavior:
When called with
orig-function is a
lambda expression, so we need to extract the original name with this oddly complicated function:
(defun beep--extract-function-name (expr) "Extracts the original function from a lambda expression, EXPR." (cond ((listp expr) (if (equal (car expr) 'lambda) (car (cadr expr)) (car expr))) ((stringp expr) expr) (t "")))
Let’s try it out interactively:
(defun beep-tester (wait-amount) "Sit around twiddling our thumbs." (sit-for wait-amount)) (advice-add 'beep-tester :around 'beep-when-runs-too-long) (beep-tester 6)
To type less, we can create a loop, using
dolist with a list of all the long-ish functions I want to advise:
(dolist (func '(org-publish org-publish-all org-publish-project compile ;; ... as many as we want ... )) (advice-add func :around #'beep-when-runs-too-long))
By the way, I don’t recommend attaching this function to both
org-publish, as Emacs calls the former for each file in the project, and it gets annoying quick.
But the great thing about this Emacs approach is it is a configure and forget approach.
According to this discussion, Festival looks pretty good, so after installing it:
sudo apt-get install -y festival festvox-us-slt-hts
I can add the following code:
(let ((say-exists-exit-code (with-temp-buffer (call-process "which" nil (current-buffer) nil "say")))) (setq beep-speech-executable (if (= say-exists-exit-code 0) "say %s" "festival -b '(voice_cmu_us_slt_arctic_hts)' '(SayText \"%s\")'")))
This code may be a little more complicated. The
call-process command executes the program,
type with the parameter
say using a temporary buffer as the output. Granted, I’m not interested in the output, only in the exit code the that it returns, which I assign to the variable,
My purpose is to over-write the
beep-speech-executable variable to a new value. What value? Well, if
say-exists-exit-code is 0, then I am on a Mac, and I use the
say executable, otherwise, I assume I am on my Linux laptop, and I set it to a call to
If you are new to Lisp, you might be surprised at that syntax, but in functional programming, everything returns a value, and in this case, the
if returns one of two strings, so why not have that as the value of the
setq, otherwise, I would have had more code:
(if (= say-exists-exit-code 0) (setq beep-speech-executable "say %s") (setq beep-speech-executable "festival -b '(voice_cmu_us_slt_arctic_hts)' '(SayText \"%s\")'"))
(let ((dunst-exists-exit-code (with-temp-buffer (call-process "which" nil (current-buffer) nil "notify-send")))) (setq alert-default-style (if (= dunst-exists-exit-code 0) 'libnotify 'osx-notifier)))
On my Ubuntu system, I also needed to install it:
sudo apt install dunst
dunst system has a lot of options for customizing it, but the defaults seem to be good enough for the moment.