Pure Code vs X Code

Writing a window manager presents a rather large problem for implementing and maintaining a codebase, namely that you need an X server running in order to run code that talks to an X server... The approach penrose takes to solving this issue (primarily for being able to write tests) is shamelessly stolen from Xmonad: split the code base in two.

On one side we have "pure" rust code that manipulates internal data structures representing the logical state of the window manager (known clients, which screen they are on, where on that screen they are positioned etc) and on the other side we have code that submits requests to (and receives events from) the X server. The two sides meet inside of the modify_and_refresh method of the XConnExt trait.

Working with diffs

The primary way that most X actions are generated by penrose is through this modify_and_refresh method. It computes a diff of the "pure" window manager state before and after some mutating operation in order to determine what changes need to be reflected in the X server state. For example:

  • Focus has moved to a new window
  • A new window has been created
  • The active workspace has switched to a new layout algorithm
  • A new screen has been detected
  • ...

You get the idea.

Doing things this way lets penrose decouple the logical operations you want to carry out (and test!) from the X side effects needed to actually do something on screen. This isn't a full, capital M monad in the style of Xmonad but it's pretty good for our purposes without getting snarled up in a lot of type theory. Another way to think of this is as a render of internal state changes out to the X server much like you would see in a GUI library or front end web framework.

What you'll see in practice, is that most operations that you want to perform in penrose are handled by calling a method on one of the pure data structures inside of a call to modify_and_refresh. The methods themselves are nice and easy to test and have confidence in, and the complexity of managing the X state is confined to one part of the code base that is then a lot easier to audit and maintain.

Triggering a refresh directly

There are of course, times when you need to do something a little more involved that requires you to make some requests to the X server yourself as part of some otherwise pure logic. You might need to check (or set) a property on a client window for example. In those cases, you can call the refresh method after carrying out whatever combination of pure and X related actions you need to perform. Internally, penrose tracks a snapshot of the previous pure state each time a refresh takes place so you always have something to compute the diff against.

Just be careful to remember that none of the pure state changes will be reflected in the "real" X server state until a refresh takes place! Depending on how complicated your logic is you may need multiple refreshes to get what you are after (but if you find yourself doing this it's probably an indicator that you should raise an issue in GitHub to see if there is a simpler way to achieve what you are after).

Given that it's the easier to reason about side of things, lets kick things off with a look at the data structures that make up the pure side of the penrose APIs.