crecher.net / blog / WM 2026-10-27
WM -----
So my window manager has actually gotten to a usable point! Which is why i decided to write this. I will try and explain the window manager, how it works, what i used to make it and what i'm planning for it in the future.
What even is it? ================
My window manager (for now just called wm cause i cannot come up with a good name), is my window manager that i started writing start of this year (2026). It's for Xorg, because i did not want to deal with wayland, and tiling. Right now it has a basic vertical and horizontal tiling layout and a basic master tiling layout. I want to also do some more layouts, like scrolling, an infinite 2d canvas, monocle, etc. but that's for later.
What is it made in? ===================
For some reason, i decided to go with go for it. It's actually my first time using the language, which is why the early code was not that good tbh. But i learned a lot just from this project alone, and i have to say that i like the language. I'm also using BurntSushi's xgb and toml libraries.
How does it work? =================
This is mostly gonna just be how i handle for example events, not how for example xorg works, so yeah: All the talking to X is done through BurntSushi's xgb library.
Startup -------
On startup, the wm does these things (in order):
1. Load the config. It tries to read the config in ~/.config/wm/config.toml, if there is no file there, it just copies the default one. Each keybinding is just a simple string, for example "'mod+shift+Return' = 'zoom'", they get turned into a Key{Mod, Sym, Fn} where Fn is a Go function. 2. Become the window manager. It asks the X server to send it requests to map and configure windows on the root window, instead of letting the server handle them directly. 3. Grab the bindings. Every key and mouse bind is grabbed four times: plain, with caps lock, with num lock and with both. That way the binds work when capslock or numlock is on. 4. Create the bar and start a goroutine that reads lines from stdin. The lines get used as the status text, so for example 'date | ./wm' would show the output of date on the right. 5. Restore state or run startup commands. Checks whether it's a restart, if not it runs the startup commands from the config.
Event loop ----------
The core of the wm is a select:
for {
select {
case s := <-statusChan:
statusText = s
drawBar(conn)
case ev, ok := <-events:
handleEvent(conn, ev)
}
}
One goroutine sits in conn.WaitForEvent() and pushes X events into events, another reads stdin and pushes status lines into statusChan. The main loop is the only code that changes state, so an event and a status update can never run at the same time.
The status channel has a buffer of one and drops old values, if stdin produces lines faster than the bar can redraw, the wm only shows the latest one.
Handling events ---------------
handleEvent is a type switch:
+-------------------+----------------------------------------------+ | MapRequest | A new window wants to appear, manage it | |-------------------|----------------------------------------------| | DestroyNotify | A window is gone, unmanage it | |-------------------|----------------------------------------------| | ButtonPress | | | MotionNotify | Start, update and end a mouse drag | | ButtonRelease | | |-------------------|----------------------------------------------| | EnterNotify | The mouse entered a client, focus it | |-------------------|----------------------------------------------| | ConfigureRequest | A window asked to resize itself, depends | |-------------------|----------------------------------------------| | PropertyNotify | The focused window changed, redraw bar | |-------------------|----------------------------------------------| | Expose | Redraw the bar | +-------------------+----------------------------------------------+ (i love tables)
ConfigureRequest depends on the window. Unmanaged windows get exactly what they want, a floating client's request gets saved and applied, a tiled client gets ignored.
Client state ------------
Every managed window is a Client:
type Client struct {
window xproto.Window
X, Y int16
Width, Height uint16
Floating bool
Fact float64
Tags BitMask
}
Clients are stored in two places: - a map[Window]*Client - a clientList slice, which is used for the tiling order New windows get added to the front of clientList, so the newest window becomes the master. Zoom just moves a client to the front of that slice.
Tags ----
Tags are bitmasks. Each client has a Tags mask, and GlobalTags is the mask of the tags you're currently viewing. A client is visible when client.Tags & GlobalTags != 0. This makes all the tag actions small bit operations: - view sets GlobalTags - toggle XORs a bit into it - tag sets the focused client's Tags
Restarting ----------
By default, restarting is under mod+shift+r. It restarts in place, which means that all the windows, tags, etc stay the same. This is useful if you are for example configuring or if something breaks. It works by: 1. Serializing the client list (window ID, floating flag, tags), the selected tags, the layout and the focused window to JSON 2. Putting that into the WM_STATE environment variable 3. Calling syscall.Exec on itself The windows stay open because they actually belong to the server. On startup, the new process sees WM_STATE, applies the state and skips the startup commands. So, for example when adding a patch you can just do go build followed by mod+shift+r (or whatever your restart key is) to apply the changes.
Anyways, thats all :D Thank you for reading this ig, if you did, and didn't just skip to the bottom.
<- back