Skip to content

Instantly share code, notes, and snippets.

@guruprasadah
Last active May 23, 2023 03:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guruprasadah/8e386c97a27705ecdb6faa344c6a620d to your computer and use it in GitHub Desktop.
Save guruprasadah/8e386c97a27705ecdb6faa344c6a620d to your computer and use it in GitHub Desktop.
The path to writing a stable window manager

Warning

I'm assuming how to setup a build system and link with libraries. If not, learn that first.

Initial steps:

Every window manager starts out like this:

#include <iostream>
#include <string>
#include <X11/Xlib.h>

void die(const std::string& reason) {
  std::cout << reason << "\n";
  exit(1);
}

int wm_detect(Display* dpy, XErrorEvent* ev) {
  die("Another WM detected");
  return 0;
}

int x_error(Display* dpy, XErrorEvent* ev) {
  std::cout << "X error\n";
  return 0
}

int main() {
  Display* dpy = XOpenDisplay(0x0);
  Window root = DefaultRootWindow(dpy);
  
  XSetErrorHandler(wm_detect);
  XSelectInput(dpy, root, SubstructureRedirectMask | SubstructureNotifyMask);
  XSync(dpy, false);
  XSetErrorHandler(x_error);
  
  // Insert event loop code here in the future
  
  XCloseDisplay(dpy);
}

This begins by opening display 0, getting the default root window, and setting a temporary error handler. This is only called if another WM is detected running.

Then, we select for SubstructureRedirectMask and SubstructureNotifyMask on the root window. This intercepts create, configure and map requests by the windows and routes them to us. Only one can select this on one window at a time, and typically only window managers do this. Thus, if this fails and the callback is called - we assume another WM is running and exit.

Then, we register the real handler which does not do anything for now.

How you organise your code is your choice, but I recommend something like this:

struct {
public:
  Display* dpy;
  Window root;
} wm;

This will prevent clunky passing of the display as an argument to every function, and this will be how I will refer to these important variables henceforth. Now, the above code would be:

#include <iostream>
#include <string>
#include <X11/Xlib.h>

struct {
public:
  Display* dpy;
  Window root;
} wm;

void die(const std::string& reason) {
  std::cout << reason << "\n";
  exit(1);
}

int wm_detect(Display* dpy, XErrorEvent* ev) {
  die("Another WM detected");
  return 0;
}

int x_error(Display* dpy, XErrorEvent* ev) {
  std::cout << "X error\n";
  return 0
}

int main() {
  wm.dpy = XOpenDisplay(0x0);
  wm.root = DefaultRootWindow(dpy);
  
  XSetErrorHandler(wm_detect);
  XSelectInput(wm.dpy, wm.root, SubstructureRedirectMask | SubstructureNotifyMask);
  XSync(wm.dpy, false);
  XSetErrorHandler(x_error);
  
  // Insert event loop code here in the future
  
  XCloseDisplay(wm.dpy);
}

The event loop:

Your pretty bog standard event loop. Get the next event, and react to it if required:

// This is in place of the comment "Insert event loop code here in the future"

XEvent xev;
while(1) {
  if(XPending(wm.dpy) {
    XNextEvent(wm.dpy, &xev);
    
    switch(xev.type) {
      
    }
  }
}

We wrap the entire switch and event retreive call with an if XPending - because XNextEvent blocks until an event it received, and we might want to do something consistently in the background. Hence, this skips the event loop if no events are queued.

There are a few events I like to call "core" events, since they concern the creation, showing, and destruction of windows. They are:

  • CreateNotify - X server informs you about the creation of a window. It is not visible (mapped) yet though. Usually you don't care about this one
  • DestroyNotify - X Server informs you about the destruction of a window. Use it to cleanup your internal book-keeping state for that window
  • MapRequest - Make the window visible. This is where I like to initialize my book-keeping state for that window.

Thus, the updated event loop code:

XEvent xev;
while(1) {
  if(XPending(wm.dpy) {
    XNextEvent(wm.dpy, &xev);
    
    switch(xev.type) {
      case CreateNotify:
        break;
      case MapRequest:
        break;
      case DestroyNotify:
        break;
    }
  }
}

Great! Now we've added switch cases, but are not actually doing anything. We will tackle them one by one.

Also, some of the intellectuals might be screaming "YOU FORGOT ConfigureRequest!!!". Yes, I have intentionally left it out - as literally every program I have tested does not send this. Plus, it is just more boilerplate. Look into the blog post series I referred you to in 02-sources.md for a simple implementation

You need not do anything for this switch case. Leave it blank (aside from the break statement)

Define a function like this:

void on_map(const XMapRequestEvent& xev) {

}

Later, in the event loop MapRequest switch case, call it like so:

case MapRequest:
  on_map(xev.xmaprequest);
  break;
  

XEvent is a union consisting of all possible event types. Based on its type member, we access the corresponding event type. This is done for polymorphism.

on_map() is where I am going to initialize my book-keeping structures for the window. However, we dont have any book-keeping structures at the moment. So, I present the following code:

struct client {
public:
 Window xwin;
};

And in the wm unnamed-struct variable, add the following member:

std::list<client*> clients;

And don't forget to include:

#include <list>

We use std::list instead of something like std::vector because we require fast deletion without wasting memory resources (which is possible because std::list is a linked list).

And now, to the actual implementation of the on_map() function:

void on_map(const XMapRequestEvent& xev) {
  client* c = new client;
  c->xwin = xev.window;
  XMapWindow(wm.dpy, c->xwin);
}

If you open a Xephyr session open at :1, like this:

Xephyr -screen 1600x900 :1

And if you run the executable (say, foowm) like this:

DISPLAY=:1 ./foowm

Nothing bad should happen!

Now, test your code by running an app like so:

DISPLAY=:1 st

I used st here, but pretty much any simple app with only one window works.

Now that you've implemented map requests, time to handle destruction as well. The only thing you have to do is destroy your book-keeping structures and change any internal wm state if it was pointing to the destroyed window. The following works for now:

void on_destroy(const XDestroyWindowEvent& xev) {
  for(auto it = wm.clients.begin(); it != wm.clients.end(); it++) {
    auto c = *it;
    if(c->xwin == xev.window) {
      delete c;
    }
  }
}

And in the event loop DestroyNotify switch case:

case DestroyNotify:
  on_destroy(xev.xdestroywindow);
  break;

Test it out, and hopefully you're not hit with a segfault!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment