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
.
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;
2
3#[derive(Error, Debug)]
4pub enum MiniWMError {
5 #[error("display {0} not found")]
6 DisplayNotFound(String),
7
8 #[error("{0}")]
9 NulString(#[from] NulError),
10}
11
12pub struct MiniWM {
13
14}
15
16impl MiniWM {
17 pub fn new(display_name: &str) -> Result<Self, MiniWMError>> {
18 Ok(MiniWM {})
19 }
20
21 pub fn init(&self) -> Result<(), MiniWMError> {
22 Ok(())
23 }
24
25 pub fn run(&self) {
26 println!("miniwm running");
27 }
28}
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:
new
will connect to the X server and create a new wm instanceinit
will instruct the X server that we want to listen to some eventsrun
will be our infinite loop that listens for X server eventsLet’s now use our MiniWM
in main.rs
1mod miniwm;
2
3use std::error::Error;
4
5use miniwm::MiniWM;
6
7fn main() -> Result<(), Box<dyn Error>> {
8 let display_name = std::env::var("DISPLAY")?;
9
10 let wm = MiniWM::new(&display_name)?;
11
12 wm.init()?;
13 wm.run();
14
15 Ok(())
16}
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.
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,
3}
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()) };
4
5 if display.is_null() {
6 return Err(MiniWMError::DisplayNotFound(display_name.into()));
7 }
8
9 Ok(MiniWM { display })
10 }
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 }
9
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);
6
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.
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.
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:
tinywm
to rustYou 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