Ξ

Writing a wayland window manager in 2020

Published on 2020-12-28 code linux

If a voice in your had 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. What went wrong?

The modular X11 desktop

X11 was about painting pixels to the screen. But it also provided APIs like EWMH 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 between implementations in the 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.

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) 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 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:

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.