Note: I no longer use Emacs, so this code is no longer maintained. Use inside your configuration at your own risk. It’s still a valuable example of Lisp macros!
As much as I love Emacs, there’s one thing that I’ve never been
satisfied with: terminal support. There are multiple offerings built
into Emacs, such as
term-mode
and the more Emacs-y
eshell. eshell
in particular has some great features like being able to use Emacs
Lisp to write shell commands and being able to pipe to Emacs
buffers. However, as a web developer in 2019, much of my terminal use
involves running complicated CLI tools such as
Jest and
Storybook. eshell
doesn’t work at all
with these, and term-mode
is significantly slower than a native
shell.
For a long time, I’ve just been tabbing over to
iTerm for managing these tasks. This
works, but it’s frustrating having to do undergo such a big context
shift whenever switching to my terminal. Luckily, iTerm
has great
Applescript
support which
allows for easily automating things like creating windows and running
programs.
Applescript in Emacs
Emacs has a built in function do-applescript
which takes a string
containing an Applescript and executes it. This is great until you
actually start writing Applescript. To make a long story short: it’s
very bad. Plus, writing long strings everywhere is less than
optimal. This opportunity presented a perfect opportunity to mess
around with using Lisp macros to write a simple DSL.
Here’s what Applescript looks like:
tell application "iTerm"
tell current window
create tab with default profile
end tell
end tell
We can then use this script to open a new tab right from Emacs!
(defun iterm-new-tab ()
(interactive)
(do-applescript "
tell application \"iTerm\"
tell current window
create tab with default profile
end tell
end tell"))
This works, but man is it ugly. The indentation is a pain to deal with
in a string literal, and the end tell
s just seem redundant. Wouldn’t
it be great if we could express this program in a Lisp form? How about this:
(defun iterm-new-tab ()
(interactive)
(applescript-do
(:tell
"application \"iTerm\""
(:tell "current window" "create tab with default profile"))))
Since we’ll be writing a bunch of iTerm
commands, it would even be better to be able to condense it to something like
(defun iterm-new-tab ()
(interactive)
(iterm (:tell "current window" "create tab with default profile")))
Much better. And it turns out, it only takes a few macros and some recursion to implement!
Embracing the magic
First, let’s define the applescript-do
macro which will be the user-facing API.
(defmacro applescript-do (form)
`(with-temp-buffer
(applescript--eval-form (quote ,form))
(do-applescript (buffer-string))))
This sends an unevaluated version of the form passed to
applescript--eval-form
, which will be a recursive function that
parses the form.
(defun applescript--eval-form (form)
(cond ((eq (car-safe form) :tell)
(insert "\ntell ")
(insert (eval (cadr form)))
(applescript--eval-form (car (last form)))
(insert "\nend tell"))
((stringp form)
(insert (concat "\n" form)))
((listp form) (insert (concat "\n" (eval form))))
((symbolp form) (insert (concat "\n" (symbol-value form))))
(t (error "invalid form"))))
This function checks the three parts of every form, the :tell
, the
target, and the command. The command can be another tell
form, so if
it’s a list, send that through the function again. There are some
extra checks to allow for embedding any Emacs Lisp form in the DSL.
Now we can define another simple macro for writing iTerm
commands,
(defmacro iterm (form)
`(applescript-do (:tell "application \"iTerm\"" ,form)))
And we’re done! Now you can write iTerm
Applescripts in Lisp and
manage your shell from Emacs. For example, run a command using
(defun iterm-run-command (cmd)
(interactive "MRun command in iTerm: ")
(iterm (:tell "current session of current window" (format "write text \"%s\"" cmd))))
This a been a nice boost in my work flow, and it was a lot of fun
exploring the weirdness of Lisp macros. Lots of things could be
improved, but this works for my simple uses. See the complete code in
my emacs.d
Github repo:
applescript.el
and
iterm.el.