blackhole://nilFM

making ryudo multimonitor aware

intro

ryudo started out as a fork of plan9port's rio with some keybinds for window snapping (aka pseudotiling), and as such didn't have any sort of multi-display awareness. It could display across multiple monitors, but it would draw windows willy-nilly when spawned, not adhering to monitor boundaries, and the keybinds would treat the sum of all monitors as a single large screen. During the early releases, version 0.6 or so, it had been brought to my attention that multimonitor support was lacking. Fast forward about a year and I finally got my head around it and went about implementing it.

A multimonitor ryudo session: on the left is the external monitor with two Audacious windows and a terminal with an IRC client; on the right is the laptop display panel with a maximized acme window.

getting monitor info

The first step is to get the monitor info into the program. The Xorg extension Xrandr is our starting point. It provides the function XRRMonitorInfo* XRRGetMonitors(Display *dpy, Window window, Bool get_active, int *nmonitors);.

This function provides 80% of the functionality we need. I wrapped it like this for convenience:

/* monitorinfo and nmonitors are global variables */
void fetchmonitorinfo() {
  if (monitorinfo)
    XRRFreeMonitors(monitorinfo);

  monitorinfo = XRRGetMonitors(dpy, DefaultRootWindow(dpy), 1, &nmonitors);
}

Then, in addition to calling fetchmonitorinfo() during initialization, we also add a case in the mainloop function (ryudo's event loop), for any events having to do with changes in monitor connections:

void mainloop(int shape_event) {
  static XEvent ev;
  static Client* c;
  static int monitor;

  for (;;) {
    getevent(&ev);
    switch (ev.type) {
...
      /* XRANDR related events; idk what the define for this is */
      case 89:
        if (XRRUpdateConfiguration(&ev)) {
          fetchmonitorinfo();
          
          /* for whatever reason this keeps things from crashing sometimes */
          monitor = getmonitorbyclient(c);
          if (monitor < 0 || monitor >= nmonitors) {
            break;
          }
          
          /* wrangle each client */
          for (c = clients; c; c = c->next) {
            monitor = getmonitorbyclient(c);
            wrangle(c, monitorinfo[monitor]);
          }
        }
        break;
...
    }
  }
}

The function wrangle(Client* c, XRRMonitorInfo monitor) simply makes sure a window lies completely within a monitor's boundaries. If the window is larger than the monitor, it will maximize it. The earlier call to getmonitorbyclient(Client* c) returns the primary monitor if the window is not on any monitor, so in the case of a monitor being unplugged or disabled, windows will be shunted to the primary monitor.

using the monitor info

Previously, ryudo used the bounds of the root window as a reference point for positioning and sizing Clients. This was done by calling XGetWindowAttributes(dpy, XDefaultRootWindow(dpy), &ra), where ra is an XWindowAttributes instance, and then using ra.wdith and ra.height. Now, we have to use the bounds of the relevant monitor instead.

The beautiful thing is that since monitorinfo is kept up to date by the event loop, we can just get the relevant monitor number via either of the int getmonitorbyclient(Client* c) or int getmonitorbymouse() helpers and indexing into monitorinfo with it, and use its x, y, width, and height properties. We can also make use of the wrangle(Client*c, int monitor) function from earlier when creating new windows, and I even made a keybind for it to quickly move a window to another monitor (effectively changing the active monitor without needing to touch the mouse).

However, the subtleties of multimonitor workflow revealed themselves to be a little bit more involved:

The first is a difficult question to answer, so I just made it a configuration option. The others were all made possible with the helper functions mentioned earlier, but as it turned out, quite a few bugs were revealed as I implemented these tweaks, and so the patch version number climbed rather quickly soon after the 1.3.0 release.

what's next

I'm not quite sure what's next for ryudo; I'd like to implement a scratchpad terminal and maybe take a stab at dynamic tiling. For the time being, the window manager is quite usable and versatile -- even more so now that it can make sense of literally as many monitors as your video card can drive!