Writing a Window Manager From Scratch Part I
Tags: programming, tutorial, linux, beginners
Published: 2024-01-23 21:09:12

A window manager is a software that, as the name says, manage windows. Controlling the placement, states, and, sometimes, the appearance of these windows.

In this article, we will explore [cwm](https://github.com/heiytor/cwm), a minimal yet "usable" window manager implemented in C for the X Window System.

## The X window system

The [X Window System](https://en.wikipedia.org/wiki/X_Window_System#) operates on a "client-server" model, where the "server" provides hardware I/O services, and the "clients" can register on this server to utilize these services. Communication between the client and server is done using the [X protocol](https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html).


![X server flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/378o3shlx43psf1nzjso.png)


The diversity among [X Window Managers](https://en.wikipedia.org/wiki/X_window_manager) exists because a window manager is a regular X client and is not mandatory. The distinguishing factor between a window manager and other regular clients is that the window manager must register for a special list of events (as detailed below). These events redirect some actions from the original client to the window manager. This is a notable distinction from other operating systems like [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) and [macOS](https://en.wikipedia.org/wiki/macOS), where the window manager is "integrated" with the kernel.

### Xlib vs Xcb

There are numerous ways to communicate over the X protocol, but the most renowned and stable methods involve bindings such as [Xlib](https://en.wikipedia.org/wiki/Xlib) (X library) or [Xcb](https://en.wikipedia.org/wiki/XCB) (X C bindings).

These two libraries exhibit several differences. While Xlib strives to maintain simplicity and user-friendliness, Xcb is more explicit about the X protocol. However, the primary distinction lies in how these libraries handle function calls: Xlib is predominantly synchronous, whereas Xcb is consistently asynchronous. For a more in-depth understanding of these libraries and their nuances, you can consult the [official wiki](https://www.x.org/wiki/guide/xlib-and-xcb/).

For your specific case, we will be using Xlib due to its simplicity and comprehensive documentation.

### Specifications

As the X Window System does not impose any specific window manager, a client should be capable of communicating with any window manager. Consequently, there are distinct standard specifications outlining how X clients, including window managers, should function. The initial specification was the [ICCM](https://x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html), followed by the [EWMH](https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#).

For instance, a dock adhering to the EWMH specification must comply with the [_NET_CURRENT_DESKTOP](https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm46201142945104) value of the window manager to determine the user's current virtual desktop. It's worth noting that *cwm* does not adhere to any of these specifications.

## Initial Setup

To start, we must need to initiate a connection with the X server. This is made with a `Display`, which represents an X connection in Xlib, use `XOpenDisplay` to receive a display. We will also be responsable for closing this connection when the code is out of bounds, you can use `XCloseDisplay` passing the display connection. The `root` is, as the name says, the root window of the display.

```c
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>

/**
 * Simple panic util
 */
void panic(char *msg) {
  puts(msg);
  exit(EXIT_FAILURE);
}

/* X11 connection */
Display *dpy;
/* Display's root */
Window root;

int main(void) {
  // Open a display
  dpy = XOpenDisplay(NULL);
  if (dpy == NULL) {
    panic("Unable to open a X display.");
  }

  // Retrieves a root window of the display.
  root = DefaultRootWindow(dpy);

  // Close the display
  XCloseDisplay(dpy);
}
```

In an X Window System, each window is associated with a parent, defining its containment within the window hierarchy. The root window represents the root of this hierarchy.

Also, create the following *Makefile*:
```Makefile
all: build

build:
	gcc main.c -o main -Wall -Wextra -std=c17 -lX11
```

## The Event Loop

In an X system, everything is driven by events. However, to handle these events, we need to specify which events we want to receive, and `XSelectInput` serves this purpose precisely. For instance, if we want to receive an `EnterNotify` event, we must inform X to emit this event, and we can achieve this using `XSelectInput(Display, Window, EnterWindowMask)`.

> The **XSelectInput()** function requests the X server to report events associated with the specified event mask. Initially, X will not report any of these events. [...]
> [Source](https://tronche.com/gui/x/xlib/event-handling/XSelectInput.html)

Due to the asynchronous nature of Xlib, it is also necessary to flush this selection, blocking the server until it has processed all the requests. This can be accomplished with `XSync(Display, 0)`.

As window manager is responsible for enforcing its own window layout. In our case, two masks play a particularly important role: `SubstructureRedirectMask` and `SubstructureNotifyMask`. These masks redirect and notify any attempts by other clients to alter the configuration of windows to the specified window. The window manager has the liberty to decide how to handle these requests. It can either call the same routine with the same arguments as the clients requested, invoke the same routine with different arguments, or outright deny the request.

![X server flow with window manager](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rg9kxan7kbc7hgc7t4v.png)

```c
// [...]
#include <X11/X.h> // New!

int main(void) {
  // [...]
  
  // Request the X server to send events related to `SubstructureRedirectMask`,
  // `ResizeRedirectMask` and `SubstructureNotifyMask`.
  // > a. Select the event mask for the root.
  XSelectInput(dpy, root, SubstructureRedirectMask | SubstructureNotifyMask);
  // > b. Synchronize the changes.
  XSync(dpy, 0);

  XEvent e;
  for (;;) {
    XNextEvent(dpy, &e);
    switch (e.type) {
    default:
      puts("Unexpected event.");
      break;
    }
    // Ensures that X will proccess the event.
    XSync(dpy, 0);
  }

  // [...]
}
```

The code appears to be correct; we are initiating an X connection, listening for the expected events, and subsequently closing the connection. However, if you attempt to run it as is, it will result in a panic.

### Testing and debugging

The reason for this is that the X server only permits one window to register for Substructure Redirect at a time. Any other client attempting to do so will receive an error message: `X Error of failed request: BadAccess (attempt to access private resource denied)`. To avoid this issue, we need to use a program like [Xephyr](https://en.wikipedia.org/wiki/Xephyr), which allows users to run a nested X application without conflicts with the active one.

The first step is to create an executable file:

```sh
#!/bin/sh

set -e

make build

XEPHYR=$(command -v Xephyr) # Absolute path of Xephyr's bin
xinit ./xinitrc -- \
    "$XEPHYR" \
        :100 \
        -ac \
        -screen 1380x720\
        -host-cursor

```

Then, create an *xinitrc* file in the same location as the executable file:

```sh
#!/bin/sh

xeyes && sleep 2 & # Optionally, initialize some programs. For now, this is unnecessary.
exec ./main # The output file created by the Makefile1
```

Now, you can run the executable, and the program should function properly.

## Mouse and Keyboard Events

You may have noticed that when hovering over the window manager, your mouse disappears. Let's take advantage of this, and before delving into window management, discuss a crucial aspect of window managers: mouse and keyboard events.

The initial step is be able to see the cursor, for this, we start creating and defining it for the root:

```c
// [...]
#include <X11/cursorfont.h> // New

int main(void) {
  // [...]

  // Create and define the cursor.
  // > a. Create a cursor using the standard `X11/cursorfont.h`.
  Cursor cursor = XCreateFontCursor(dpy, XC_left_ptr);
  // > b. Define the cursor for the root window.
  XDefineCursor(dpy, root, cursor);
  // > c. Synchronize the changes.
  XSync(dpy, False);
  
  XEvent e;
  for (;;) {
    XNextEvent(dpy, &e);
    switch (e.type) {
    // [...]
    case ButtonPress:
      puts("Button pressed!");
      break;
    // [...]
  }

  // [...]
}
```

However, when clicking with the mouse around the window manager, the `ButtonPress` event is not triggered. This happens because we have not informed X that we want to receive mouse events yet. Similar to `XSelectInput`, X provides a function to specify the mouse events we expect to receive:

```c
// [...]

int main(void) {
  // [...]

  // Tells X to send `ButtonPress` events on the root.
  XGrabButton(dpy, Button1, 0, root, 0, ButtonPressMask, GrabModeSync,
              GrabModeAsync, NULL, NULL);
  
  // [...]
}
```

Just one more thing: the `XGrabButton` will freeze X from sending more mouse events for the root and all children. To avoid this, we need to "unfreeze" it in the event loop:

```c
// [...]

int main(void) {
  // [...]

  XEvent e;
  for (;;) {
    XNextEvent(dpy, &e);
    switch (e.type) {
    // [...]
    case ButtonPress:
      // Unfreeze X to allow more mouse events for the root and all children.
      XAllowEvents(dpy, ReplayPointer, CurrentTime);
      XSync(dpy, 0);
      puts("Button pressed!");
      break;
    // [...]
    }
  }
  
  // [...]
}
```

Now that we can receive mouse events, grabbing a key is quite similar:

```c
// [...]

int main(void) {
  // [...]
  
  // Grab keys to receive events
  // > a. Get a keysymbol of a string.
  KeySym aKeySym = XStringToKeysym("a");
  // > b. Get the keycode of this keysym.
  KeyCode aKeyCode = XKeysymToKeycode(dpy, aKeySym);
  // > c. Grab the key on the root window.
  XGrabKey(dpy, aKeyCode, ShiftMask, root, false, GrabModeAsync, GrabModeAsync);
  // > d. Synchronize the changes.
  XSync(dpy, false);

  XEvent e;
  for (;;) {
    XNextEvent(dpy, &e);
    switch (e.type) {
    // [...]
    case KeyPress:
      puts("Key pressed!");
      break;
    // [...]
    }
  }
  
  // [...]
}
```

To simplify things, put this logic inside a `grabKey` function:

```c
// [...]

void grabKey(char *key, unsigned int mod) {
  KeySym sym = XStringToKeysym(key);
  KeyCode code = XKeysymToKeycode(dpy, sym);
  XGrabKey(dpy, code, mod, root, false, GrabModeAsync, GrabModeAsync);
  XSync(dpy, false);
}

int main(void) {
  // [...]
  
  // Receive a `KeyPress` event when pressing "a" + "Shift".
  grabKey("a", ShiftMask);
  // Add more keybindings...

  // [...]
}
```

We're receiving the mouse and keyboard events as needed! We can now delve into how to manage windows. But, this will stay for the next part.