---
title: Writing a wayland window manager in 2020
date: 2020-12-28
tags: [code, linux, wayland]
description: If a voice in your head is currently screaming "THERE ARE NO WINDOW MANAGERS IN WAYLAND, THEY ARE CALLED COMPOSITORS" you swallowed the bait.
---

If a voice in your head is currently screaming "THERE ARE NO WINDOW MANAGERS IN
WAYLAND, THEY ARE CALLED COMPOSITORS" you swallowed the bait. That is exactly
what I want to write about. But let me start at the beginning:

## A short history of wayland

For the last 30-odd years graphics on Unix were dominated by X11. In 2008 the
people who maintained X11 declared that for the good of the project, starting
from scratch was the best way to go. So they started wayland.

By roughly 2013 GTK and Qt had completed support for the new protocol. A
reference server implementation called Weston was also available. So everything
was looking good. Fedora uses wayland by default since 2016. But most other
distros in 2020 still use an X11 server, which at this point is [largly
unmaintained](https://news.ycombinator.com/item?id=24884988). What went wrong?

## The modular X11 desktop

X11 was about painting pixels to the screen. But it also provided APIs
like [EWMH](https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html)
that allowed different programs like window managers, task bars, screenshot
tools, clipboard managers, and many more to work together. Users were free to
replace any of these as they pleased.

There are of course the integrated desktop environments like Gnome, KDE,
Mate, XFCE, and LXQt. You can use their components individually, but they are
really meant to be used together. But then there is also a rich ecosystem of
standalone projects: Window managers like i3, dwm, or openbox, panels like
tint2 or polybar, and compositors like xcompmgr or picom.

The wayland maintainers conciously decided to only focus on painting pixels to
the screen. They scaled down modularity for the sake of simplicity,
performance, and security.

While I understand the thought process and believe wayland is ultimately the
better protocol, I believe this is also the reason for the low adoption:
Existing tools cannot implement wayland compatibility because there are no
standardized APIs. If you want to port your desktop to wayland, you have to
replace everything all at once. That might be possible for Gnome and KDE, but
not for the ecosystem of modular components.

## The wlroots project

This decision by the wayland maintainers did not neccesarily mean the end of
the modular desktop though. Someone else could step in and define the necessary
APIs. That is exactly what the wlroots project did. Now often when someone asks
"how do I do X with wayland" the answer is "there is a tool Y, but it only
works with wlroots-based compositors".

The wlroots maintainers would like to be the standard for wayland compositors,
but unfortunately they are not. There is some
[coordination](https://lists.freedesktop.org/archives/wayland-devel/2019-February/040076.html)
between implementations in the
[wayland-protocols](https://gitlab.freedesktop.org/wayland/wayland-protocols)
repo, but there is little progress. And even though there are many
wlroots-based compositors, the only one ready for production is
[sway](https://github.com/swaywm/sway).

## Sway

About once a year I look at wayland, mess around a little, get frustrated, and
soon decide that I should wait another year. This year around I tried sway,
which is an i3-compatible wayland compositor.

My first impression: Most things worked! Touchpad worked, clipboard worked,
nothing crashed. But I soon realized the subtle differences: The cursor
acceleration and taps were just slightly off, fonts were not hinted,
environment variables were not set, there was no fully functioning status
indicator implementation. Nothing that could not be fixed with a few days of
work.

But the bigger issue for me is that I am just not a big fan of the i3 concept.
I respect it, but it is nothing I would want to use as my daily driver. I am
very used to my openbox setup and even though I would prefer to get rid of the
X server, switching to sway/i3 is too high a cost.

There are some projects that try to do for openbox what sway has done for i3,
but none of these is really in a usable state. I looked at their code and
quickly decided that writing one myself was also out of the question. So what
could I do?

## Writing a wayland window manager

I had already created a toy X11 window manager (based on
[dwm](https://dwm.suckless.org/)) in the past. It is not actually that hard:
The server notifies you whenever a new window appears and you tell the server
where it shoud be rendered. Then there is also focus handling and some keyboard
shortcuts and that's basically it.

I squinted at the [sway IPC documentation](http://i3ipc-python.readthedocs.io/)
and realized: The most important building blocks are all there. I could run
sway as a generic display server and do all the layout in an IPC client. The
key bindings would still have to be configured in sway config, but the `nop`
command allows to convert any key combination to a generic IPC event.

With the imperative tiling model it is fairly hard to control where a window
will end up, but you can always switch to floating mode and have pixel-perfect
control over its position via `[con_id={id}] move position {x} {y}`.

The biggest issue turned out to be the release of modifier keys: I am used to
cycle through a preview of windows in last-recently-used order via Alt+Tab.
Once the Alt key is released the currently selected window should be focused.
Unfortunately, sway is just not designed to allow for this kind of events. I
was able to patch in the desired behavior, but I doubt this will ever go
upstream.

So here it is:

```python
from i3ipc import Connection
from i3ipc import Event

MAX_STACK_LENGTH = 10


class Manager:
    def __init__(self):
        self.stack = []
        self.wins = []

    def stack_index(self, win):
        try:
            return self.stack.index(win.id)
        except ValueError:
            return MAX_STACK_LENGTH

    def stack_raise(self, win):
        if win.id in self.stack:
            self.stack.remove(win.id)
        self.stack.insert(0, win.id)
        self.stack = self.stack[:MAX_STACK_LENGTH]

    def on_mode(self, con, event):
        if event.change == 'alttab':
            if not self.wins:
                workspace = con.get_tree().find_focused().workspace()
                wins = workspace.leaves() + workspace.floating_nodes
                self.wins = list(sorted(wins, key=self.stack_index))
            if self.wins:
                self.wins.append(self.wins.pop(0))
                self.wins[0].command('focus')
        elif self.wins:
            self.stack_raise(self.wins[0])
            self.wins = []

    def on_focus(self, con, event):
        if not self.wins:
            self.stack_raise(event.container)

    def on_window(self, con, event):
        c = event.container
        xprops = c.ipc_data.get('window_properties', {})
        if c.type == 'con' and xprops.get('window_type') != 'dialog':
            con.command('[con_id=%i] border none' % c.id)
            con.command('[con_id=%i] floating enable' % c.id)
            con.command('[con_id=%i] resize set 100 ppt 100 ppt' % c.id)
            con.command('[con_id=%i] move position 0 0' % c.id)

    def on_binding(self, con, event):
        if event.binding.command == 'nop layout floating':
            g = con.get_tree().find_focused().geometry
            con.command('border pixel 2')
            con.command('resize set %i px %i px' % (g.width, g.height))
            con.command('move position center')
        elif event.binding.command == 'nop layout maximized':
            con.command('border none')
            con.command('resize set 100 ppt 100 ppt')
            con.command('move position 0 0')
        if event.binding.command == 'nop layout left':
            con.command('border pixel 2')
            con.command('resize set 50 ppt 100 ppt')
            con.command('move position 0 0')
        elif event.binding.command == 'nop layout right':
            con.command('border pixel 2')
            con.command('resize set 50 ppt 100 ppt')
            # FIXME: newer sway version supports ppt
            con.command('move position 640 0')


if __name__ == '__main__':
    mgr = Manager()
    con = Connection()
    con.on(Event.MODE, mgr.on_mode)
    con.on(Event.WINDOW_FOCUS, mgr.on_focus)
    con.on(Event.BINDING, mgr.on_binding)
    con.on(Event.WINDOW_NEW, mgr.on_window)
    con.main()
```

## Conclusion

Wayland is the future and outside of the big desktop environments, sway is the
only viable option right now. So if, like me, you want to use wayland but don't
like i3, maybe implementing a window manager is the right approach. It might
not be elegant, but it works.
