Lists And Key Sequences
In Emacs, a key sequence is special key binding that uses multiple
keys in a series. For instance, to store the position of a file in a
named register, you’d type: C-x r SPC
(the space key). You can
then jump back to that stored position with: C-x r j
. Since they
require more effort to type, a sequence is chosen for functions that
interrupt the normal editing flow.
We tend to bind similar functions to sequences with a common
prefix. Along with storing and copying text to registers with the C-x
r
prefix, C-x r
also contains all of the rectangle-related
sequences.
Playing Music Sequence
Once upon a time, I started using the EMMS interface to play some
Internet music streams, but instead of feeble attempts to remember
cryptic URLs, I wrapped it in a play-jazz
function:
(defun play-jazz () "Start up some nice Jazz" (interactive) (emms-play-streamlist "http://thejazzgroove.com/itunes.pls"))
Type M-x play-jazz
to kick off the tunes, and M-x emms-stop
to
pause. While one can not live by jazz alone, it seemed silly to
create a bunch of named functions for URL mappings.
On my Kinesis keyboard, F9
is prominent, easy to reach, and my
go-to key for flow interrupting requests (for instance, F9 g
issues a magit-status
). Key sequences starting with F9 m
start
music streams, with F9 m s
stopping the music.
Below is my code to iterate over a list of letter combinations and an associated URL. First, define a key map:
(define-prefix-command 'personal-music-map) (global-set-key (kbd "<f9> m") 'personal-music-map)
Second, use dolist
to iterate over a list of tuples so that
car
returns the key and cdr
supplies the URL:
(dolist (station '(("a" . "http://stereoscenic.com/pls/pill-hi-mp3.pls") ;; Ambient ("t" . "http://www.1.fm/tunein/trance64k.pls") ;; Trance ("j" . "http://thejazzgroove.com/itunes.pls"))) ;; Jazz (lexical-let ((keystroke (car station)) (stream (cdr station))) (define-key personal-music-map (kbd keystroke) (lambda () (interactive) (emms-play-streamlist stream)))))
For each element in my list, I add to personal-music-map
(using
the define-key
function), and I create an interactive
function
that calls emms-play-streamlist
with the URL.
However, this requires an actual closure that Emacs Lisp doesn’t
grant for free. This is why I use lexical-let
instead of just a
regular let
.
While just an example, if you wish, see my EMMS emacs initialiation file.
Changing Color Themes
For another example of this pattern… I switch my Emacs color theme
depending on the time of day. Since I’m using the Powerline project,
changing the color scheme requires me to call powerline-reset
in
order to get the colors to apply to the mode line.
Another problem is that color themes don’t always specify the
org-block
begin and ending lines. I defined both a dark and
light scheme, and wrap that in a cozy function blanket that works
for most themes.
To change a color theme, I call this function with the theme (as a function) and the org-block style (also as a function):
(defun ha/change-theme (theme org-block-style) "Changes the color scheme and reset the mode line." (funcall theme) (powerline-reset) (funcall org-block-style))
For an example, I can call it like:
(ha/change-theme 'color-theme-sanityinc-tomorrow-night 'org-src-color-blocks-dark)
Since I have few of these themes, I have a key sequence map, to follow a similar pattern:
(define-prefix-command 'personal-theme-map) (global-set-key (kbd "<f9> d") 'personal-theme-map)
My list now needs three items, so tuples are out, and regular lists
are in (just means I have to use car
, cadr
, and caadr
):
(dolist (atheme '(("d" 'color-theme-sanityinc-tomorrow-day 'org-src-color-blocks-light) ("l" 'color-theme-sanityinc-tomorrow-eighties 'org-src-color-blocks-dark) ("m" 'color-theme-sanityinc-tomorrow-bright 'org-src-color-blocks-dark) ("n" 'color-theme-sanityinc-tomorrow-night 'org-src-color-blocks-dark)) (lexical-let ((keystroke (car atheme)) (the-theme (cadr atheme)) (org-block (caadr atheme)) (define-key personal-theme-map (kbd keystroke) (lambda () (interactive) (ha/change-theme the-theme org-block))))))
Helper Sequence Function
Nothing makes a programmer itchy than duplicate code. Sure, both snippets are slightly different…but only slightly.
I would like to be able to define a key sequence series with:
- a name for the key-map (below this is
silly-map
- an initial key prefix, like:
F9 s
- a single function to call for each entry
- a list where each entry starts with a key, and the rest of the elements are given to the function
For instance, we could define a series of key sequences that call
the message
function with a silly message:
(define-sequence 'silly-map "<f9> s" 'message '(("b" "%s is a very %s" "Brian" "naughty boy") ("c" "Yes, this is indeed a cheese shop.") ("p" "%s. Beautiful %s" "Norwegian Blue" "plumage")))
While I would like to define this define-sequence
as a function, like:
(defun define-sequence (map-name prefix func seqs) "A silly attempt at defining symbols and key maps in a normal function. Silly, since this doesn't work." (define-prefix-command map-name) (global-set-key (kbd prefix) map-name) (dolist (el seqs) (lexical-let ((keystroke (car el)) (the-rest (cdr el))) (define-key map-name (kbd keystroke) (lambda () (interactive) (apply func the-rest))))))
For a moment, let’s pretend that this will work. Most of the code is
lifted from the examples aboves. The only real difference the call
to apply
that calls a function converting a list into functional
arguments.
However, I can’t manipulate the symbol table so easily. Instead, I
need to define a macro to do this sort of work. With a backquote
,
this macro mimics the function quite well:
(defmacro define-sequence (map-name prefix func seqs) "Define a collection of key sequences associated with MAP-NAME and begin with PREFIX that call a function, FUNC. The SEQS is a list where each element is a list that begins with a final key binding. The rest of the list is given as parameters to the function, FUNC." `(progn (define-prefix-command ,map-name) (global-set-key (kbd ,prefix) ,map-name) (dolist (el ,seqs) (lexical-let ((keystroke (car el)) (the-rest (cdr el))) (define-key ,map-name (kbd keystroke) (lambda () (interactive) (apply ,func the-rest)))))))
The following is the redefined code for my music playlist:
(define-sequence 'personal-music-map "<f9> m" 'emms-play-streamlist '(("a" "http://stereoscenic.com/pls/pill-hi-mp3.pls") ;; Ambient ("t" "http://www.1.fm/tunein/trance64k.pls") ;; Trance ("j" "http://thejazzgroove.com/itunes.pls"))) ;; Jazz
The Hydra
The example key sequences described above are groovy as long as you don’t have to call them a second time. I’m not quickly cycling through my music or color schemes, so the code above is good enough.
The Hydra Project is nice for key sequences that you may need to call multiple times as you can repeatedly press the last key to call the associated function again.
For instance:
(require 'hydra) (defhydra hydra-font-size (global-map "<f9>") "text-scale" ("<up>" text-scale-increase "larger") ("<down>" text-scale-decrease "smaller"))
This code snippet lets me press F9
and then either the up or down
arrow keys to increase or shrink the font size. If you are
interested (as it is really cool package), read Oleh Krehel’s
introduction to the Hydra project.
Thanks to this suggestion by apella, let’s rework my music list with Hydra:
(defhydra hydra-music-map (global-map "<f9> m" :body-pre (interactive) color :blue) "Start playing msuic from a stream" ("a" (emms-play-streamlist "http://stereoscenic.com/pls/pill-hi-mp3.pls") "ambient") ("t" (emms-play-streamlist "http://www.1.fm/tunein/trance64k.pls") "trance") ("j" (emms-play-streamlist "http://thejazzgroove.com/itunes.pls") "jazz"))
The biggest advantage of using Hydra instead of our function above,
is that after entering the prefix, in this case F9 m
, Hydra will
prompt you with the rest of the options.
Hrm. Perhaps this essay was little more than an introduction to Hydra. ;-)