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.
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 Client
s. 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:
- Does the current monitor mean the one with the active window or the one with the mouse pointer?
- Opening a new window should favor the current monitor.
- Alt-tabbing should restrict itself to the monitor with the active window.
- Closing or minimizing the active window should switch focus to a window on the same monitor.
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!