Writing a Window Manager in Rust - Part 1

This is the first part in writing miniwm, a window manager for X11 in rust.

I recently got my Linux machine working again and I thought I would try and make a window manager in rust. This is something I tried many moons ago but back in the day all you had was C, the xlib documentation and code from other window managers. Let me tell you it wasn’t easy…

I don’t have the list of features defined yet, we can build it as we go I guess. There are things we will need to think about at one point, things like do we want it to be stackable or tiling, how will we configure the window manager etc.

For this series you will need these things:


First we need to create the project, in rust this is easy, open a terminal and write:

$ cargo new miniwm

This will create rust bin project in the miniwm directory.

For our mini window manager we will be using the x11 crate. There are two options for libraries that you can use when you write a window manager for X11:

Both of these libraries provide an API to talk to the X server but are designed quite differently. I won’t get into the details, maybe another time if I decide I want to try and rewrite miniwm using XCB.

The first step is to add the x11 crate to your project:

miniwm $ cargo add x11 --features xlib

For nicer error handling let’s install thiserror too:

miniwm $ cargo add thiserror

Right, we have successfully setup our project, we should be able to build it now

miniwm $ cargo build
Compiling miniwm v0.1.0 (/home/rumpl/dev/miniwm)
Finished dev [unoptimized + debuginfo] target(s) in 0.77s

If the project doesn’t compile you will need to install libx11-dev.

Connecting to the X server

On to the good stuff. The first thing a window manager has to do is to connect to the X server. This is done by calling XOpenDisplay.

We will create a struct for our window manager, first create a directory miniwm and a mod.rs inside that directory

miniwm $ mkdir src/miniwm
miniwm $ touch src/miniwm/mod.rs

We can now create our struct in that file

 1use thiserror::Error;
 3#[derive(Error, Debug)]
 4pub enum MiniWMError {
 5    #[error("display {0} not found")]
 6    DisplayNotFound(String),
 8    #[error("{0}")]
 9    NulString(#[from] NulError),
12pub struct MiniWM {
16impl MiniWM {
17    pub fn new(display_name: &str) -> Result<Self, MiniWMError>> {
18        Ok(MiniWM {})
19    }
21    pub fn init(&self) -> Result<(), MiniWMError> {
22        Ok(())
23    }
25    pub fn run(&self) {
26        println!("miniwm running");
27    }

Let’s see what is going on here, first we define our own error type that we will use later.

The MiniWM has three functions:

Let’s now use our MiniWM in main.rs

 1mod miniwm;
 3use std::error::Error;
 5use miniwm::MiniWM;
 7fn main() -> Result<(), Box<dyn Error>> {
 8    let display_name = std::env::var("DISPLAY")?;
10    let wm = MiniWM::new(&display_name)?;
12    wm.init()?;
13    wm.run();
15    Ok(())

In the first line we ask for the current value of the $DISPLAY environment variable. On POSIX systems the $DISPLAY environment variable holds the current display name.

We can now try and run our window manager. Granted it doesn’t do anything for now but this is a great time to check that our setup works.

To check this we will need 3 terminals.

In the first terminal we will launch Xephyr. Xephyr is a nested X server that runs as an X application. Having a nested X server means we don’t have to go out of our own X session to test the window manager we are making, neat!

$ sudo Xephyr :1 -ac -br -noreset -screen 800x600

This will open a black window, this is the X server that our window manager will connect to.

Note: without sudo I had problems with Xpehyr not detecting the mouse and the keyboard, sudo makes the problems go away, you should try running Xephyr without sudo, it might work for you.

In another terminal launch our window manager

miniwm $ DISPLAY=:1 ./target/debug/miniwm

Note that we redefine the $DISPLAY variable so that our window manager connects to the right X server.

Finally we can try and run an application, I will use urxvt, a VT102 emulator for the X window system.

$ DISPLAY=:1 urxvt

After all this you should see a miniwm running message in the second terminal and also see the urxvt window should be shown inside the Xephyr window.

Image alt

Our window manager doesn’t do anything yet, let’s add some code shall we?

The first thing a window manager should do is to connect to the X server, this is done by calling XOpenDisplay

Once we get the reference to the display we will save it in our struct, to do that we add a display field to our struct:

1pub struct MiniWM {
2    display: *mut xlib::Display,

We can then change the new function and add the call to XOpenDisplay

 1    pub fn new(display_name: &str) -> Result<Self, MiniWMError> {
 2        let display: *mut xlib::Display =
 3            unsafe { xlib::XOpenDisplay(CString::new(display_name)?.as_ptr()) };
 5        if display.is_null() {
 6            return Err(MiniWMError::DisplayNotFound(display_name.into()));
 7        }
 9        Ok(MiniWM { display })
10    }

Listening to events

Next we need to tell the X server that we want to listen to some events. By default the X server doesn’t send any events, it’s up to the window manager to tell it what events we care about.

We will do this in our init function

 1    pub fn init(&self) -> Result<(), MiniWMError> {
 2        unsafe {
 3            xlib::XSelectInput(
 4                self.display,
 5                xlib::XDefaultRootWindow(self.display),
 6                xlib::SubstructureRedirectMask,
 7            );
 8        }
10        Ok(())
11    }

Let’s pause here and see what is going on.

The XSelectInput tells the X server that we want to listen to particular events. Here we use the xlib::SubstructureRedirectMask mask. This mask tells the X server that we want to listen to events of type CirculateRequest, ConfigureRequest and MapRequest. We only care about the last one, in X terms mapping a window means creating a window.

Let’s create our busy loop that will wait for events

 1    pub fn run(&self) {
 2        let mut event: xlib::XEvent = unsafe { zeroed() };
 3        loop {
 4            unsafe {
 5                xlib::XNextEvent(self.display, &mut event);
 7                match event.get_type() {
 8                    xlib::MapRequest => {
 9                        self.create_window(event);
10                    }
11                    _ => {
12                        println!("unknown event {:?}", event);
13                    }
14                }
15            }
16        }
17    }

This is an infinite loop that waits for the next event and then dispatches it. We then call the create_window function if we get a MapRequest from the X server.

Creating a window

The last thing we need to do is to tell the X server to create a window when needed.

Here is the create_window function

1    fn create_window(&self, event: xlib::XEvent) {
2        println!("creating a window");
3        let event: xlib::XMapRequestEvent = From::from(event);
4        unsafe { xlib::XMapWindow(self.display, event.window) };
5    }

It converts the event to the appropriate xlib::XMapRequestEvent type and then calls the XMapWindow function. Here we could have also used the xlib::XMapRaised function to effectively show the window and push it on the top of the stack.

The end

We have now created a basic window manager that can show windows when it receives the event to do so. There is so much more we can do but we can, I think, stop here for the first part. Tap yourself on the back and take a break.

Tune in next time for some exciting window manager explorations. In the meantime you might want to take a look at the xlib documentation and reading some other tiny window manager code can’t hurt, here is a list of some nice ones:

You can also see the full code of this post on github

These are just some of the window managers I looked at but the internet is full of tiny window managers, explore!

This the second part in the “Writing a window manager in Rust” series