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’.