Window Management in the age of Covid-19

Feb 13, 2021
Emacs with EXWM (Screenshot courtesy of project)

About a year into the pandemic, I've been working at home for quite some time now.
Aside from the obvious negatives, there are some positives to this whole thing:

  • No more commuting! Still relatively new to the San Francisco Bay Area, commuting holds some novelty. I like how it breaks up the day; a kind of life "minigame" if you will. However if I am being honest, I know it harms my productivity, at least in the immediate sense. Compared to days when I am working from home, it takes me a lot longer to "settle down" and start doing stuff in the morning.
  • More time! Now there's no commuting, I have more free time. That and lunchtime can be used for things I would just not be able to do before. I can take a walk, work out in the back yard, do yoga or work on projects!
  • Less disruption - kinda obvious and a double-edged sword, as I benefit from some degree of diversion. Nonetheless, I can more predictably get stuff done now that I have more control over my physical workspace.
  • Ergonomics...okay, here's the deal:

I suck at this! For most of the pandemic, I have been typing away on my tiny 13 inch laptop. I've done some great work that i'm proud of, but DAMN! Particularly without my regular gym visits, my posture has not responded well to this.
Finally I decided enough was enough and DID something about it - I bought a
Heckin' Big Monitor.

SO BIG :O

Yeah you've got a big monitor; so what?

So, many people in my line of work have giant monitors, multiple monitors, curved monitors etc, so this might not seem world-breaking. But while I've typically had a minimum of 2 monitors at work, I just haven't ever really given this kind of thing a second thought at home. I've stuck to the absolute basics. I've really refused to engage with the question of computer ergonomics at home. Call it ADHD, laziness or penny-pinching, I'd just honestly rather think about and spend money on something else.
So this is a BIG DEAL for me.

The time for looking the other way is over; as a 6'4 sciatica-prone hypermobile person, I have to take this stuff seriously.

How do I use this thing?

So how do I make good use of this eyewatering investment?
I don't need a 34" text editor or a 34" terminal or 34" anything most of the time, so I want to be able to have multiple things on screen at once. With multiple smaller monitors, you can drag/snap windows between the monitors as they both provide a place for a window to maximize to. Things just work; it's intuitive, for user and computer program alike.

A now-commonplace feature in window managers (even Windows 10) is being able to snap a window to half of the screen, or even a quarter, by dragging the window to that side/corner. You can do this with all your windows and boom - you have something like the intuitive flexibility of multiple monitors, but with one monitor. This is pretty great and makes having a giant monitor a whole lot more useful.

Still, it needs to be better though. Moving things around like this feels clunky, and there's no elegant way to take minimized/hidden things and move them to a given part of the screen. I don't want a window limit and I don't want fixed proportions the window can have; it's so much better than nothing, but it's not enough.

Long ago I used AwesomeWM. Awesome is a tiling window manager - its whole deal is breaking the screen up into separate panels which each have one thing in them. I don't remember Awesome very well, but I wanted that kind of thing. My manager strongly suggested another popular tiling window manager - i3wm, but half way through learning the key combinations, I lost the will grind through what was mostly in my head already...

Emacs to the rescue!

I use Emacs! Emacs has all the tiling things. Things which I spent a long time learning and am reasonably comfortable with. I didn't want to do all that hard work again, and I spend a lot of time in Emacs anyway.
I remembered a project EXWM, which makes Emacs a window manager. Basically Emacs plugs into Xorg like any other WM and when a program wants to make a window, emacs makes a buffer for it and there it is. It can be moved around, hidden, whatever, just like any regular Emacs buffer. I tried it long ago, before I was all that familiar with Emacs, and remembered it not working very well. I don't know if that was really true or it was just my inexperience. But now, about a month in, I have to say it's working out pretty well.

Things to take care of

So it turns out there's a bit more to a typical desktop environment than a window manager.

Network Management

While the decisions about what to connect to and other such things are managed by the aptly-named NetworkManager daemon, you need a frontend to control it. Something to give you a list of available wireless networks and let you select one. For most of my Linux usership, this has taken the form of 'nm-applet'. This is a gnome applet that you can right click and basically gives you access to all that stuff. Unless I want a gnome-panel in my Emacs, there's no way I can get nm-applet back. So I googled around and found enwc - an Emacs-based frontend for NetworkManager. It's not too big and clever - it just gives you a list of wifi networks and lets you hit enter to connect to them. Seems to work, though as far as I can tell, doesn't offer any configuration. For that, i'll have to use the 'nmcli' command line tool, which is a rare enough occurrence that I think it's okay.

(require 'enwc)
(setq enwc-default-backend 'nm)
(condition-case nil
    (enwc)
  (error nil))
enwc

Audio

When you hit Volume up, Volume down or Mute on your keyboard something has to be listening for that, and know what to do. I googled around and found the pulseaudio-control package, which provides a means for telling Pulseaudio to do those things from Emacs.

; Tell EXWM to send these keys to Emacs
(dolist (k '(XF86AudioLowerVolume
             XF86AudioRaiseVolume
             XF86AudioMute))
  (pushnew k exwm-input-prefix-keys))

; Setup pulseaudio-control's "C-/" keybinding 
(require 'pulseaudio-control)
(pulseaudio-control-default-keybindings)

; Bind the 'media keys' to relevant pulseaudio-control functions
(global-set-key (kbd "<XF86AudioRaiseVolume>") 'pulseaudio-control-increase-volume)
(global-set-key (kbd "<XF86AudioLowerVolume>") 'pulseaudio-control-decrease-volume)
(global-set-key (kbd "<XF86AudioMute>") 'pulseaudio-control-toggle-current-sink-mute)
pulseaudio-control

The first block is needed because if an X window is active in EXWM, it will send these keys to that window. Most applications will not know what to do with these. So adding these to the exwm-input-prefix-keys list means that EXWM will intercept these and let Emacs handle them directly, with the bindings I set up afterward. Simples!

Backlight stuff

Sigh, this was a little annoying to get right. I ended up writing a helper program to do the talking-to-sysfs part of it for me. Seriously rough stuff:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

#define BL_PATH "/sys/class/backlight/intel_backlight/brightness"

int main(int argc, char **argv)
{
    if(argc == 1)
    {
        int fd = open(BL_PATH, O_RDONLY);

        char buff[10];
        int chars = read(fd, buff, 10);
        buff[chars-1] = '\0';
        puts(buff);
    }
    else
    {
        int value = atol(argv[1]);
        char buff[10];
        int s = snprintf(buff, 10, "%d\n", value);
        buff[9] = '\0';

        int fd = open(BL_PATH, O_WRONLY);
        write(fd, buff, s);
    }

    return 0;
}

I built this, and made it setuid root so it could write to the 'brightness' file.
Now for the second piece:

(defvar backlight-level 4000)
(defconst backlight-step 500)
(defconst backlight-max 7000)

(defun backlight-fn (value)
  (*
   (expt (/ backlight-level (float backlight-max)) 3)
   backlight-max))

(defun backlight-set ()
  (start-process "backlight-set" nil "set_backlight"
                 (format "%d" (backlight-fn backlight-level)))
  (message (format "Backlight: %d/%d" (backlight-fn backlight-level) backlight-max)))


(defun backlight-inc () (interactive)
  (setq backlight-level (min backlight-max (+ backlight-level backlight-step)))
  (backlight-set))

(defun backlight-dec () (interactive)
  (setq backlight-level (max 0 (- backlight-level backlight-step)))
  (backlight-set))

(global-set-key (kbd "<XF86MonBrightnessUp>") 'backlight-inc)
(global-set-key (kbd "<XF86MonBrightnessDown>") 'backlight-dec)

PHEW! What's going on here is I define backlight-set, which calls my 'set_backlight' program above, passing the value of backlight-level to it. Then I define backlight-inc and backlight-dec to add/subtract this level before calling backlight-set. Finally, I add keybindings for it (I need to add these keys to the exwm-input-prefix-keys list too). backlight-fn is also there to give me a nonlinear curve to my backlight setting. It works without this but is less useful - there's a lot more visible difference between levels 1000-1500, and 6500-7000. So this corrects for that and an exponent of 3 feels about right.

Time, Date, Battery

It took me a couple of days to realise the time, date and battery level weren't anywhere to be seen! Fortunately, Emacs has something for this:

(setq display-time-day-and-date t)
(setq display-time-default-load-average nil)
(display-time-mode)

(setq battery-mode-line-format " [ 🔋 %p%% ]")
(setq battery-update-interval 5)
(display-battery-mode)

Pretty self-explanatory; shows time, date and battery in mode line. Works fine.

Window Layouts

Not a requirement, but a lot more useful if you have it:

(winner-mode)

When you use Emacs for everything, it's easy to accidentally C-x 1 all of your other windows away, which can be frustrating. With winner-mode, you can immediately get the previous layout back with a C-c <left>. This is just great and desktop environments need this in general.

Also, I like to use ace-window , which is a more intuitive way to jump between windows than regular C-x o, which doesn't behave very predictably when dealing with more than 2 open ones. It does this by showing a character in the corner of each window, which you can then press to jump to that window. Pretty great...except it doesn't work for EXWM windows. EXWM covers up/doesn't have text area for this character to be shown in (i'm no Emacs expert so maybe there is an easy fix to this), so I have switched away to using winum-mode . This simply puts a number in the modeline of each window, and lets you jump between them via C-x w <window number> .

VPN

I use a corporate VPN for certain things, so I need a way to switch that on and off. I was pleasantly surprised to learn that NetworkManager handles all of this for me - I just wrote some commands to flip the VPN on and off:

(defun vpn-start () (interactive)
       (shell-command "nmcli c up VPN"))

(defun vpn-stop () (interactive)
       (shell-command "nmcli c down VPN"))

Final Words

There are still some nagging issues, like how I haven't got monitor hotplugging working yet. It's a bit of an edge case, as I spend the vast majority of my time just using my big monitor, but it'd be good to get it right. There's also some weirdness around audio sinks appearing/disappearing as my monitor is plugged in/out, sometimes requiring me to open a shell, type cinnamon-settings sound and click around like some n00b to get things straight.

But it's pretty good and I strongly recommend giving EXWM a shot to anyone who seriously uses Emacs. It's a surprisingly low-friction experience and especially useful if you have a big monitor!