DEV Community

Cover image for Writing a Window Manager From Scratch Part I
Heitor Danilo
Heitor Danilo

Posted on

Writing a Window Manager From Scratch Part I

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, a minimal yet "usable" window manager implemented in C for the X Window System.

The X window system

The 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.

X server flow

The diversity among X Window Managers 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 and 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 (X library) or 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.

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, followed by the EWMH.

For instance, a dock adhering to the EWMH specification must comply with the _NET_CURRENT_DESKTOP 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.

#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);
}
Enter fullscreen mode Exit fullscreen mode

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:

all: build

build:
    gcc main.c -o main -Wall -Wextra -std=c17 -lX11
Enter fullscreen mode Exit fullscreen mode

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

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

// [...]
#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);
  }

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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, which allows users to run a nested X application without conflicts with the active one.

The first step is to create an executable file:

#!/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

Enter fullscreen mode Exit fullscreen mode

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

#!/bin/sh

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

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:

// [...]
#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;
    // [...]
  }

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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:

// [...]

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

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

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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:

// [...]

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;
    // [...]
    }
  }

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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

// [...]

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;
    // [...]
    }
  }

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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

// [...]

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...

  // [...]
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)