Managing emacsclient windows in StumpWM
I’m still working on getting my GuixSD machine configured, including working on getting familiar with StumpWM – a windows manager written in Common Lisp – which is the desktop paradigm I’ve decided upon for this Lisp-centric machine.
I’m somewhat habituated to (my) AwesomeWM keybindings, which involve
the Super key in combination with various other keys, including say
s-1
for tag/workspace 1, s-3
for tag/workspace 3, &c., and
s-E
(i.e. hold Super and Shift and press e
) to launch an
emacsclient (see below on the Emacs client/daemon
configuration). StumpWM could be configured in a somewhat similar
fashion (though it doesn’t seem to quite use tag/workspaces in the
same fashion), but the ‘tradition’ seems to be to use a prefix, which
is by default C-t
(that is, hold Control and press t
), which
is then released and followed with another key or key combination. I
don’t really like using Control for windows management since it tends
to conflict with bindings in Emacs and elsewhere, so I’m testing out
s-F
(hold Super, press f
) as a prefix (though whether I’ll
stick with prefixed bindings or go back to single action bindings, I’m
not yet certain).
From browsing other stumpwm configs, I came across a useful bit of configuration:
(defun run-or-raise-prefer-group (cmd win-cls)
"If there are windows in the same class, cycle in those. Otherwise call
run-or-raise with group search t."
(let ((windows (group-windows (current-group))))
(if (member win-cls (mapcar #'window-class windows) :test #'string-equal)
(run-or-raise cmd `(:class ,win-cls) nil T)
(run-or-raise cmd `(:class ,win-cls) T T))))
This function can then be used with specific applications, e.g.:
(defcommand run-or-raise-icecat () ()
(run-or-raise-prefer-group "icecat" "Icecat"))
The above function leverages run-or-raise-prefer-group
to either
launch Icecat, if it is not already running, or else focus the Icecat
window, and successive calls will cycle through multiple Icecat
windows/windows if more than one Icecat window exists. This is extremely
useful as it’s much less cognitively-tasking than figuring out which
window number Icecat currently is associated with.
This can then be assigned a binding(s) like:
(define-key *root-map* (kbd "W") "run-or-raise-icecat")
(define-key *root-map* (kbd "C-W") "run-or-raise-icecat")
(define-key *root-map* (kbd "s-W") "run-or-raise-icecat")
This means that one first presses the prefix (s-f
for me) followed
by either W
or C-W
or s-W
(that is, Shift+w
,
Control-Shift+w
or Super-Shift+w
) to either launch Icecat or
focus/cycle through existing Icecat windows/windows.
Now, the way I typically use Emacs is to invoke it as a daemon
(i.e. emacs --daemon
) and then connect Emacsclients to this daemon
(i.e. emacsclient -c
for the ‘windowed’ gtk-application,
emacsclient -t
in the terminal). However, if we define a parallel
function for Emacs, a particular edge-case arises:
(defcommand run-or-raise-emacsclient () ()
(run-or-raise-prefer-group "emacs" "Emacs"))
This works rather like the Icecat one as long as at least one
Emacsclient window exists (so if there are multiple window,
successive calls of run-or-raise-emacsclient
will cycle through
them). However, if no emacsclient is currently open, it will launch an
undaemon’ed Emacs (requiring loading the entire init.el
), even
if/though an Emacs daemon is currently running and thus could be
attached to.
At least one solution to this is the following function (with relevant keybindings):
(defcommand decide-on-emacsclient () ()
(if (equal (run-shell-command "pgrep \"emacsclient\"" t) "")
(run-shell-command "emacsclient -c")
(run-or-raise-emacsclient)))
(define-key *root-map* (kbd "s-e") "decide-on-emacsclient")
(define-key *root-map* (kbd "C-e") "decide-on-emacsclient")
(define-key *root-map* (kbd "e") "decide-on-emacsclient")
The above function executes the shell-command pgrep "emacsclient"
and evaluates whether the output of that shell-command is equal to the
empty string (which will be the case only when no emacsclient is
running). Where at least one emacsclient is running, it executes the
run-or-raise-emacsclient
function defined earlier,
focussing/cycling through running emacsclients. Where no emacsclient
is currently running, it executes instead emacsclient -c
, opening
a windowed emacsclient. And the bindings let me press the prefix and
then either Super+e
, Control+e
or simply e
to execute this
new function.
I also have the following definitions:
(defcommand emacsclient-launch () ()
(run-shell-command "emacsclient -c"))
(define-key *root-map* (kbd "s-E") "emacsclient-launch")
(define-key *root-map* (kbd "C-E") "emacsclient-launch")
(define-key *root-map* (kbd "E") "emacsclient-launch")
So that if I want to launch a new emacsclient window instead of switching to an
existing one, I can do so using one of series of keybindings parallel
to the previous set, but with a capital E
rather than a lowercase
one.
This is working well for me, and is a nice example of the power of using Lisp-based ‘desktop environment’.