‘The half minute which we daily devote to the winding-up of our watches is an exertion of labour almost insensible; yet, by the aid of a few wheels, its effect is spread over the whole twenty-four hours.’

Managing emacsclient windows in StumpWM

Benjamin Slade

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")

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