Introduction

Welcome to Penrose: a modular tiling window manager library for X11 written in Rust.

Unlike most other tiling window managers, Penrose is not a binary that you install on your system. Instead, you use it like a normal dependency in your own crate for writing your own window manager. Don't worry, the top level API is well documented and a lot of things will work out of the box, and if you fancy digging deeper you'll find lots of opportunities to customise things to your liking.

If you are new to Rust it is worthwhile taking a look at the learning materials provided by the Rust project to get up to speed on how the language works. (The rest of this book assumes you are somewhat familiar with the language).

The rest of this book covers the concepts and implementation of Penrose at a level of detail that should allow you to implement your own extensions and custom functionality on top of the base library. If you just want to skip ahead to a working, minimal window manager then take a look at the Quickstart section of this book or the examples directory of the GitHub repo. (My personal config is also available to take a look at if you want to see what something a bit more involved looks like!)

As with all crates on crates.io, the crate level documentation is also available on docs.rs.

Happy window managing!

Getting started

So you'd like to manage your windows, maybe even tile them?

Well aren't you in luck!

The following is a quick guide for how to get your system set up for building a penrose based window manager and getting it running. By the end of this guide you will have a very minimal window manager that you can use as a starting point.

If you've ever tried out xmonad before then the overall design and feel of how penrose works should feel (somewhat) familiar. The key thing is this: penrose is a library for writing a window manager. It's not a pre-built window manager that you then configure via a config file. In practical terms what that means is that it's time to get our hands dirty with writing some code!

Step 0: Getting set up with Rust

If you have Rust on your system already, congrats! You can skip this section.

For everyone else, head on over to rust-lang.org and click on the big "Get Started button" which will advise you to curl a setup script straight into sh. If you'd prefer to see what you are about to run, the following should do the trick:

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh

# Open and peruse in your editor of choice
$ $EDITOR rustup.sh

# Then, to carry out the actual install
$ chmod +x rustup.sh
$ ./rustup.sh

Now simply sit back and wait for while Rust is installed on your system.

Initialising your window manager crate

"Crates" are Rust's term for what you might be more used to calling a package or project. Either way, for your window manager you are going to want to make a new binary crate like so:

$ cargo new --bin my_penrose_config
$ cd my_penrose_config
$ exa -T  # or just plain old 'ls' if you prefer
.
├── Cargo.toml
└── src
   └── main.rs

If you open up main.rs you should see a simple hello world program:

fn main() {
    println!("Hello, world!");
}

We can run this using cargo run to check everything is good to go:

$ cargo run
   Compiling example v0.1.0 (/home/roger/my_penrose_config)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/example`
Hello, world!

Nice! Time to write a window manager.

Writing your main.rs

You should now have a new git repo with the content shown above. At the moment, your main.rs is just a simple Hello World program (we'll be fixing that soon) but first we need to add some dependencies. Most importantly, we need to add penrose itself but we're also going to add another crate as well to make our lives a little easier: tracing-subscriber can be used to collect and process the logs that penrose generates as it runs. It's by no means required to collect the logs but it definitely helps track down issues with your window manager if you can see what's going on inside as it runs!

Thankfully, adding new dependencies is also something cargo can handle for us! It even handles enabling optional features for us (which is handy, because we need to do just that with tracing-subscriber):

$ cargo add tracing-subscriber --features env-filter
$ cargo add penrose

With that done, we're going to copy the minimal example from the penrose repository in GitHub as our window manager. Either copy and paste the contents of the example into your main.rs or (my prefered choice) use wget to pull it directly from GitHub:

$ cd src
$ rm main.rs
$ wget https://raw.githubusercontent.com/sminez/penrose/develop/examples/minimal/main.rs

For reference, your main.rs should now look like this:

//! penrose :: minimal configuration
//!
//! This file will give you a functional if incredibly minimal window manager that
//! has multiple workspaces and simple client / workspace movement.
use penrose::{
    builtin::{
        actions::{
            exit,
            floating::{sink_focused, MouseDragHandler, MouseResizeHandler},
            modify_with, send_layout_message, spawn,
        },
        layout::messages::{ExpandMain, IncMain, ShrinkMain},
    },
    core::{
        bindings::{
            click_handler, parse_keybindings_with_xmodmap, KeyEventHandler, MouseEventHandler,
            MouseState,
        },
        Config, WindowManager,
    },
    map,
    x11rb::RustConn,
    Result,
};
use std::collections::HashMap;
use tracing_subscriber::{self, prelude::*};

fn raw_key_bindings() -> HashMap<String, Box<dyn KeyEventHandler<RustConn>>> {
    let mut raw_bindings = map! {
        map_keys: |k: &str| k.to_string();

        "M-j" => modify_with(|cs| cs.focus_down()),
        "M-k" => modify_with(|cs| cs.focus_up()),
        "M-S-j" => modify_with(|cs| cs.swap_down()),
        "M-S-k" => modify_with(|cs| cs.swap_up()),
        "M-S-q" => modify_with(|cs| cs.kill_focused()),
        "M-Tab" => modify_with(|cs| cs.toggle_tag()),
        "M-bracketright" => modify_with(|cs| cs.next_screen()),
        "M-bracketleft" => modify_with(|cs| cs.previous_screen()),
        "M-grave" => modify_with(|cs| cs.next_layout()),
        "M-S-grave" => modify_with(|cs| cs.previous_layout()),
        "M-S-Up" => send_layout_message(|| IncMain(1)),
        "M-S-Down" => send_layout_message(|| IncMain(-1)),
        "M-S-Right" => send_layout_message(|| ExpandMain),
        "M-S-Left" => send_layout_message(|| ShrinkMain),
        "M-semicolon" => spawn("dmenu_run"),
        "M-Return" => spawn("st"),
        "M-A-Escape" => exit(),
    };

    for tag in &["1", "2", "3", "4", "5", "6", "7", "8", "9"] {
        raw_bindings.extend([
            (
                format!("M-{tag}"),
                modify_with(move |client_set| client_set.focus_tag(tag)),
            ),
            (
                format!("M-S-{tag}"),
                modify_with(move |client_set| client_set.move_focused_to_tag(tag)),
            ),
        ]);
    }

    raw_bindings
}

fn mouse_bindings() -> HashMap<MouseState, Box<dyn MouseEventHandler<RustConn>>> {
    use penrose::core::bindings::{
        ModifierKey::{Meta, Shift},
        MouseButton::{Left, Middle, Right},
    };

    map! {
        map_keys: |(button, modifiers)| MouseState { button, modifiers };

        (Left, vec![Shift, Meta]) => MouseDragHandler::boxed_default(),
        (Right, vec![Shift, Meta]) => MouseResizeHandler::boxed_default(),
        (Middle, vec![Shift, Meta]) => click_handler(sink_focused()),
    }
}

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .finish()
        .init();

    let conn = RustConn::new()?;
    let key_bindings = parse_keybindings_with_xmodmap(raw_key_bindings())?;
    let wm = WindowManager::new(Config::default(), key_bindings, mouse_bindings(), conn)?;

    wm.run()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bindings_parse_correctly_with_xmodmap() {
        let res = parse_keybindings_with_xmodmap(raw_key_bindings());

        if let Err(e) = res {
            panic!("{e}");
        }
    }
}

Checking we're good to go

Hopefully you've spotted that the end of the example includes a test. Now, it's entirely up to you whether or not you keep (and run) the test but it's highly recommended that you do. It's actually recommended that you write more tests for your window manager as you extend the features you want and write your own custom code!

Penrose itself has a pretty comprehensive test suite of the main logic and provides a variety of ways for you to check and confirm that things are behaving in the way that you expect. To run our test (and any others that you have added yourself) we simply need to run cargo test:

NOTE: This test (and the example itself) require you to have the xmodmap utility installed on your system in order to parse our keybindings.

Make sure you have it installed before going further!

$ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.03s
     Running unittests src/main.rs (target/debug/deps/example-1562870d47d380ed)

running 1 test
test tests::bindings_parse_correctly_with_xmodmap ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

You'll see a lot more output the first time you run the test as things are compiled, but so long as you see the test passing we should be good to take things for a spin!

Making use of our new window manager

So far we've been building things in debug mode, which is exactly what we want when we're testing things out and running any test suites we have. For actually making use of our new window manager though, we want to switch to release mode:

$ cargo build --release

You'll see a lot of output the first time round as the dependencies of Penrose itself are compiled, but after that your re-compile should be pretty quick following any changes that you make. Once the binary is compiled you should see it as a new executable in the target/release directory.

The simplest way of running your new window manager is to login via a TTY and place the following in your ~/.xinitrc:

exec /home/roger/my_penrose_config/target/release/my_penrose_config &> ~/.penrose.log

Then, you can simply type startx after logging in and your window manager will start up, with the log output available in my your home directory as .penrose.log!

If logging in and starting your graphical session from a TTY is "too hipster" for you (which, lets be honest, it is), you might want to look at installing and running a display manager and using something like xinit-session to make this a little nicer. Alternatively, dropping something like the following into /usr/share/xsessions should also do the trick. The arch wiki is a fantastic place to read up on how to do these sorts of things whether you use Arch Linux or not.

[Desktop Entry]
Version=1.0
Name=Penrose Session
Comment=Use this session to run penrose as your desktop environment
Exec=sh -c "/home/roger/my_penrose_config/target/release/my_penrose_config &> ~/.penrose.log"
Icon=
Type=Application
DesktopNames=Penrose

Profit

And that's it!

You are now the proud owner of a shiny new window manager. From this point on you can start tinkering to your heart's content and setting things up exactly how you want. Speaking from personal experience, I would advise that you commit your changes to your window manager regularly and that you make sure you know how to revert to your last good state in case you manage to introduce any particularly nasty bugs into your setup. If that happens, simply rebuild your previous good state and get to work on fixing your bug.

The xephyr.sh script in the GitHub repository can be used to run a given example in an embedded X session if you want a little bit of safety while you sanity check changes that you are making. Details of how it works are in a comment at the top of the script and examples/local_test is git ignored for you to be able to have a place to try things out.

The rest of this book goes on to cover some of the inner workings of the main library, how to write and work with extensions and generally have fun tinkering with your window manager.

Happy window managing!

Built In Functionality

Out of the box, penrose offers a minimal amount of functionality that can be used to write your own window manager. The core of the library is the state management and X server interaction logic, but there are a number of types and traits available for building out custom behaviour as you see fit.

Partly to exist as a reference (and also to bootstrap a minimal window manager) the builtin module is provided with a small number of examples for each of the pieces of functionality on offer.

Layouts

The built in layout functionality is primarily focused around giving a default experience that is useful out of the box. With that in mind, things are restricted to a couple of simple layouts that showcase the message handling capabilities of the Layout trait, the associated Messages and a couple of Transformers that combine nicely to give your windows a little bit of breathing room.

Layouts

Monocle

+-----------------------+
|                       |
|                       |
|                       |
|                       |
|                       |
+-----------------------+

The monocle layout is lifted directly from dwm as what is possibly the simplest possible layout: the currently focused window gets the full available screen space and everything else is hidden.

NOTE: This is not the same thing as making a window fullscreen. With the monocle layout you will still see the effect of any LayoutTransformers that have been applied which may reduce the space available for the window.

Main and Stack

+--------------+--------+
|              |        |
|              |        |
|              +--------+
|              |        |
|              |        |
+--------------+--------+

The default and primary layout for penrose is the MainAndStack which is a slight generalisation of the default tiled layout from xmonad. There are several ways to set it up but the common theme is the idea of a "main" area and stack (or secondary) area that contains the windows that are not the current focus of what you are doing. The number of windows allowed in the main area can be changed using messages as can the proportions of the screen assigned to each area.

As you might expect you have the choice of whether the main area is on the left, right, top or bottom of the screen. There are also a couple of Messages that can be sent to switch between the different behaviours if you want to modify a single layout rather than register several different ones.

Centered Main

+-----------+-----------+
|           |           |
|           |           |
+-----------+-----------+
|                       |
|                       |
+-----------+-----------+
|           |           |
|           |           |
+-----------+-----------+

There is also a modified version of the MainAndStack layout called CenteredMain which provides two secondary areas, one either side of the main area. As with its counterpart, you can rotate between having the secondary areas to the side or above and below the main area by sending a Rotate Message

Grid

+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+

The Grid layout will tile windows in the smallest nxn grid that can hold the number of windows present on the workspace.

Please be aware that if there are not a square number of windows to be tiled, this layout will leave gaps:

+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
|       |       |
|       |       |
+-------+-------+

Messages

As mentioned above, there are a handful of built in messages that work with the MainAndStack layout which are also generally applicable to other layouts with a similar sort of set up. The IncMain, ExpandMain and ShrinkMain messages should be relevant for any layout that emphasises some clients over others. The Rotate and Mirror messages can be used if a single layout supports rotational and reflective symmetry (or if pairs of layouts can be mapped to one another).

The UnwrapTransformer message is tied to the LayoutTransformer trait as a way of removing a layout transformer from the underlying layout. Nothing needs to be done to support this message as it is handled by the LayoutTransformer trait itself.

Transformers

To showcase a couple of simple things that are possible with LayoutTransformers, there is are the ReflectHorizontal and ReflectVertical transformers which do pretty much what you would expect. To support the built in status bar there is also a ReserveTop transformer that can be used to prevent layouts from positioning windows over a status bar, and finally there is the Gaps transformer because (lets face it) most of us like at least a little bit of space between our windows.

Actions

When it comes to extending the behaviour of you window manager, the first and most obvious thing to look at is running some custom code in response to a key binding being pressed. In penrose, this is refered to as an action.

Actions can be anything from focusing a new window, to changing the layout algorithm being used, to opening a terminal or running fully custom logic to find and display amusing pictures of squirrels.

The choice is yours.

To help with some of the boilerplate and common cases, there are a couple of helper functions that will generate a KeyEventHandler for you in a relatively simple way. There are also a couple of built in actions for working with floating windows and exiting penrose to get you started.

Writing actions using helpers

There are five helper functions for writing common actions:

  • key_handler: this one is the most general. It wraps a function that takes a mutable reference to the current window manager state and a reference to the XConn used by your window manager and runs whatever custom code you care to write.
  • modify_with: for calling pure state methods this helper handles the diff and refresh cycle for you. Simply update the StackSet with whatever changes you want to make and a refresh will be triggered for you to reflect you changes to the X server.
  • send_layout_message: this does pretty much what you'd expect. It calls the given function to construct a Message and sends it to the active layout. (Useful for updating your layout behaviour on the fly).
  • broadcast_layout_message: does the same thing as send_layout_message only in this case the message is copied and sent to all layouts available to the current workspace rather than just the active one.
  • spawn: as the name implies, this spawns a given program as a subprocess. You probably want at least one key binding for spawning either a terminal or a program launcher such as dmenu or rofi. For the programs you use the most, this lets you get to them with a single key press!

UI

Currently, penrose offers a single piece of built in UI via the penrose_ui crate: a status bar. The bar is inspired by the dwm status bar and provides a simple API for writing your own text based widgets for rendering to the screen.

In addition to the widgets described below there are a couple of debugging based widgets which are useful when trying to diagnose issues with the window manager state but probably not something you want on your screen all the time. If you are interested in taking a look at them they can be found here

The Text widget

For building up simple widgets there is the Text widget which can be used to provide most of the layout and re-render logic with an easy to use API. Any time the contents of the widget are modified it will be re-rendered to the bar. On its own this isn't particularly useful but you can add hooks to set the content in response to changes in the window manager state (which we'll take a look at in the next section).

Text widgets are left justified by default but this can be switched to right justified if desired. There is also the ability to specify that the widget is greedy which will cause it to take up any available left over space once all other widgets have finished laying out their contents. Personally I use this with the ActiveWindowName widget to take up the middle of the status bar and act as a sort of active screen indicator .

The RefreshText widget

If you want to render something that doesn't depend on the internal state of the window manager (such as the current time, volume, connected wifi network etc) then you can set up a very minimal widget quickly using RefreshText. All you need is a function that returns the string to be rendered when it is called and the styling you'd like to use when rendering. From that you get a widget that will check if it needs to re-render every time the internal window manager state is refreshed and re-render any time the output of your function changes.

The sys module has a number of simple widgets of this nature that you can use as a reference to get you started. For example, this is all you need to display the current date and time:

#![allow(unused)]
fn main() {
use penrose::util::spawn_for_output_with_args;
use penrose_ui::bar::widgets::{RefreshText, TextStyle};

pub fn current_date_and_time(style: &TextStyle) -> RefreshText {
    RefreshText::new(style, || {
        spawn_for_output_with_args("date", &["+%F %R"])
            .unwrap_or_default()
            .trim()
            .to_string()
    })
}
}

Built in widgets

Workspaces

The Workspaces widget is the most complicated built in widget on offer. It checks the currently available workspaces and several properties about each one:

  • The tag assigned to the workspace
  • Whether or not the workspace is focused (and on what screen)
  • If there are any windows visible on the workspace

From that it will generate a workspace listing with highlighting to indicate the current state of your window manager. Workspaces with windows present are assigned a different foreground color and focused workspaces are assigned a different background color. The active workspace is indicated with its own highlight for visibility as well.

RootWindowName

The RootWindowName widget is an idea lifted directly from dwm: any time the root window name is updated it will re-render with its content set to the new name. The xsetroot tool can be used to set the root window name to whatever string you like and typically this is used by spawning a shell script that updates the root window name with system stats on an interval:

# Set the root window name to the current date and time
$ xsetroot -name "$(date '+%F %R')"

ActiveWindowName

In a similar way, ActiveWindowName will display the title of the currently focused window. Given that there is less control over what the contents of this string will be, this widget allows you to set a maximum character count after which the title is truncated to ....

This widget will also only render on the active screen so it works well as a visual indicator of which screen currently has focus.

CurrentLayout

The CurrentLayout widget simply calls the layout_name method on the active workspace each time the internal state is refreshed. Each Layout is free to specify whatever name it choses so if you want to customise the text displayed by this widget you will need to write a LayoutTransformer that intercepts the inner name and maps it to your preferred string instead (or write a new widget that bakes that behaviour into the widget itself).

Extensions

An extension is some piece of user written code that works with the penrose APIs to provide some additional functionality. For specific, common use cases there are a number of traits and helper functions that are available to do most of the heavy lifting (see the builtin section of this book for examples of what is on offer). For things that are a little more custom, you'll want to make use of the lower level Hook and State Extension APIs.

This section of the book gives an overview of the different APIs that are on offer and goes into a little bit of detail around a couple of the commonly requested pieces of functionality that penrose implements using them.

Hooks

Penrose offers several Hooks for you to add custom logic into the existing window manager logic in order to customise its behaviour. Each type of hook is covered in the following pages and further details of how new hooks can be written are discussed in the Reference Guide section later in the book.

Startup Hooks

Startup hooks are run a single time after you call the run method on the WindowManager struct. This takes before entering the main event loop but after all other setup has taken place. Any startup actions you need to take that require the interaction with the X server or manipulating the window manager state need to placed in here as a StateHook (completely custom code independent of the window manager or X server can be run in your main.rs instead if you prefer).

The compose_or_set_startup_hook method on Config can be used to compose together multiple startup hooks if you are making use of other extensions that also need to set one.

NOTE: it is always best to use this method for setting additional hooks after you have created you initial Config struct in order to avoid accidentally replacing an existing hook!

Event Hooks

EventHooks run before each event from the X server is processed, allowing you to provide your own custom handling of events. You are free to run whatever code you want in response to events and you are also able to decide whether or not the built-in event handling should run after you are done: if you return Ok(true) from your hook then the processing will continue, if you return Ok(false) then it will stop.

If you do decide to skip default handling you should check carefully what it is that you are skipping. The main event handling logic can be found here in the core module.

As with the other hooks, there is a compose_or_set method on Config to help you combine multiple event hooks together without accidentally overwriting anything along the way.

NOTE: EventHooks are run in order and the first hook to say that no further processing should take place will short circuit any remaining composed event hooks and the default handling!

Manage Hooks

ManageHooks allow you to modify how a window is initially added to the window manager state when it first appears. For example you might move the client to a specific workspace or position in the stack, or you might mark it as floating in a certain position on the screen. Your hook will be called after the window has been added into the internal state so the full set of APIs will be available for you to make use of.

Again, as with the other hooks there is a [compose_or_set][2] method on Config to help you combine multiple manage hooks together without accidentally overwriting anything along the way.

Refresh Hooks

Refresh hooks (like startup hooks) are added to your window manager as an implementation of the StateHook trait. They are run at the end of the modify_and_refresh method of the XConnExt trait each time the internal state of the window manager is refreshed and rendered to the X server. This is one of the more general purpose hooks available for you to make use of and can be used to run code any time something changes in the internal state of your window manager.

NOTE: Xmonad refers to this as a "Log Hook" which I find a little confusing. The name comes from the fact that one of the main use cases is to log the internal state of the window manager in order to update a status bar, which makes sense but I prefer naming the hooks for where they are called in the event handling flow.

As with the other hooks, there is a compose_or_set method on Config for adding Refresh Hooks into you existing Config struct.

EWMH

Support for EWMH in penrose is provided (surprisingly enough) via the ewmh module in extensions. This provides minimal support for floating windows and setting the appropriate properties for interaction with things like external status bars (polybar for example).

The add_ewmh_hooks function can be applied to an existing Config in order to set up the required hooks for adding this support.

Overview of Concepts

Penrose is a dynamic tiling window manager for Xorg in the spirit of Xmonad. Most of the concepts and APIs you'll find for penrose are nothing new, but if you plan on digging into writing your own window manager then it's worthwhile taking a bit of time to learn what all the moving parts are.

At its core, the main operation of penrose is an event loop that reacts to events received from the X server. In simplified rust code, it looks something like this:

#![allow(unused)]
fn main() {
loop {
    let event = get_next_xevent();
    match event {
        // for each event type run the appropriate handler
    }
}
}

There's obviously more to it than that, but this is a pretty good starting point for how to think about your window manager. Penrose provides a number of different ways to modify how the default handling of events behaves and for running custom code in response to key presses. The pages in this section of the book each cover (at a relatively high level) what the moving parts that make this work all look like.

First up: pure code vs X code.

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.

Data Structures

As mentioned in Pure Code vs X Code, there are a number of pure data structures that penrose makes use of in order to manage the internal state of the window manager. We wont get too much into the details of all of the various methods associated with each data structure: for that it's best to read the docs on docs.rs. Instead, we'll take a quick look at what each data structure does and how you can make use of it when writing your own penrose based window manager.

Most of the data structures outlines below are some form of zipper (or some meta-data wrapped around a zipper). If the Wikipedia page all looks a bit "computer science-y" to you then you can get by pretty well by thinking of a zipper as collection type (like a list or a tree) that has an added concept of "focus" (that is, "the element of the collection we are currently looking at"). There is a really nice article about the use of zippers in Xmonad which is worth a read if you have the time. It covers the starting point for the use of zippers in penrose and also shows where all of the names come from(!) Penrose takes the idea a little further than what is seen in Xmonad in order to provide what I think is a nicer API to work with (but I'll let you be the judge of that).

First up, the arguably incorrectly named "Stack".

Stacks

So called because it (primarily) represents the X "window stack" for the current screen you are looking at. Getting technical for a minute, a Stack is a zipper over a doubly-linked list that has a couple of nice properties that help to simplify how a lot of the rest of the code in penrose is written:

  1. A Stack is never empty (there is always at least the focused element)
  2. Operations that manipulate which element is focused do not alter the order of the elements themselves.
  3. Operations that work with the focused element are O(1)

You can think of a Stack as simply being a normal linked list with a flag on one of the elements to indicate where the focus point currently sits. (The actual implementation is a little different in order to make things nicer to work with but the idea itself is fine).

Penrose makes use of Stacks for anything that we want to track focus for. Specifically, we use them for tracking:

  • windows assigned to each workspace
  • the layouts in use on each workspace
  • workspaces assigned to a each screen

The operations available on Stacks are pretty much what you'd expect: you can treat them like collections (map, filter, re-order the elements, iterate, etc) and you can move the focus point around.

Workspaces

Up next after Stacks is Workspaces. You can think of a workspace as a wrapper around a given window stack that helps penrose know how to locate the given stack of clients and how (and when) to position them on the screen. Rust type wise, a workspace look like this (the fields on a real Workspace aren't public but we can ignore that for now):

#![allow(unused)]
fn main() {
pub struct Workspace {
    id: usize,
    tag: String,
    layouts: Stack<Layout>,
    stack: Option<Stack<Xid>>,
}
}

The id and tag fields are used to identify workspaces within the larger pure state: useful, but not particularly interesting. The client Stack itself is wrapped in an Option because (like we mentioned above) there is no such thing as an empty Stack, so a Workspace with no windows has None. Running operations on the stack contained in a given workspace is possible from the top level of the pure state (which we'll cover in a bit).

The layouts field contains all of the possible Layout algorithms available for positioning windows on this workspace. There must be at least one layout available (so no Option<Stack> here) and the currently focused layout in the stack is the one that will be used to position windows when this workspace is placed on a given screen.

Speaking of which...

Screens

If you thought a Workspace was pretty much "a window Stack with a fancy hat", then a Screen is "a Workspace in a box".

A 2D box to be precise.

For the purposes of our pure state, all we care about when it comes to the physical screens we have to play with are:

  • which screen we're talking about
  • the dimensions of the screen
  • the workspace that is currently active

Each screen pairs a Workspace with an ID (0..n in the order that they are returned to us by the X server) and a Rect to denote the size and relative position of each screen in pixels. Workspaces can be moved between screens, clients can be moved between workspaces.

Lovely.

Rect(angles)

Both screens and the windows that sit within them are described using rectangles. Each Rect is simply the (x, y) coordinates of its top left corner along with its width and height. Not massively exciting on its own but it's worth taking a look at the docs on the Rect struct to see what methods are available for slicing, dicing, positioning and comparing Rects while you write your custom Layout algorithms and extensions.

The StackSet

And last but by no means least, we have the StackSet. It's a little "set-y" when you break it down so that's what we're going for name wise until someone gives me something better (it's definitely a lot more like a set than the original from Xmonad in my opinion but we'll get to that in a second).

Ignoring several book-keeping fields which we maintain for quality of life purposes, the Rust type looks something like this:

#![allow(unused)]
fn main() {
struct StackSet {
    screens: Stack<Screen>,
    hidden: LinkedList<Workspace>,
    // and some book-keeping...
}
}

I'm not quite sure how best to describe what's going on here in terms of Zippers as it's a little bit of an abuse of the concept but, if you squint hard enough, what you're looking at is pretty much a "Stack of Stacks". Albeit with a healthy sprinkling of meta-data throughout and the fact that for the unfocused elements we don't care about their order (hence the set based name).

If you think back to what we said a Zipper was, we said we had some collection of elements along with the idea of there being a "focus point" that picks out an element from that collection. For the StackSet, the collection is a set of Workpsaces, and the "focus" is actually a Stack of Screens and their associated Workspaces.

...still with me?

If you think about what we care about when managing windows, we can break things down into the following:

  • The windows we are managing (Stacks)
  • The workspaces those windows are assigned to (Workspaces)
  • The screens those workspaces are shown on (Screens)
  • The workspaces that are currently hidden from view (more Workspaces)

For the workspaces that are visible, we move them in and out of the available screens as needed and we maintain the currently focused screen which is where the X input focus currently lies. For the hidden workspaces we don't really care about what order they are in (we can't see them) so we use a LinkedList to store anything not currently allocated to a screen.

We could use a HashSet but then we'd need Workspaces to be hashable and it doesn't actually buy us much in terms of the API we end up with.

Having the focused "element" be another level of wrapping around multiple element from the collection really pushes the definiton of a Zipper I suspect but it works pretty nicely all things considered. We can then fully manage the on screen position and stack position of each window and manipulate groups of windows based on the workspace they are part of.

Nice.

And that's it!

Admittedly, "it" is a rather large set of methods on a StackSet but it gives you a rich, zipper based API for manipulating your windows which handles all of the focus book-keeping for you. To really understand everything that is possible with the API it is best to dive into the docs.rs docs and try things out for yourself. The real structs are generic rather than having to contain Xids as shown in the pseudo-code above so feel free to pull in penrose as a dependency and start having a play with them to see what is possible!

The tests suites are another good place to take a look at how things work without getting too tied up in the specific use cases penrose has for things.

Speaking of specifics, lets take a look at how to actually do useful things with your window manager: up next we're covering layouts.

Building on top of penrose

Out of the box, the examples provided in the penrose GitHub repository show you how to put together a fairly minimal window manager. By design, penrose does not attempt to implement every piece of functionality you might like from your favourite window manager, instead it provides a set of rich, composable APIs for extending the behaviour and adding your own custom logic.

The simplest place to start is with running custom code in response to key bindings, whether that's to modify how your windows are arranged on the screen, to launch a new program or run completely custom logic. From there you can dig into things like custom layout algorithms and extending the core window manager behaviour with hooks.

If you've ever experimented with Xmonad or Qtile before then the set up should feel somewhat familiar to you.

Actions

To start with we're going to assume that when we talk about running an Action we're talking about executing some custom code in response to a key binding bein pressed. With that in mind, lets take a look at the KeyEventHandler trait found in penrose::core::bindings:

#![allow(unused)]
fn main() {
pub trait KeyEventHandler<X: XConn> {
    fn call(&mut self, state: &mut State<X>, x: &X) -> Result<()>;
}
}

There's not much to it: you are given mutable access to the window manager State and a reference to the X connection. From there you can do pretty much whatever you like other than return data (we'll take a look at how you can persist and manage your own state in a bit!)

To make things easier to work with (and to avoid having to implement this trait for every piece of custom logic you want to run) there are several helper functions provided for wrapping free functions of the right signature.

NOTE: In any case where you do not need to manage any additional state, it is strongly recommended that you make use of these helpers to write your actions as simple functions rather than structs that implement the KeyEventHandler trait.

Built-in helpers

In the penrose::builtin::actions module you will find a number of helper functions for writing actions. The most general of these being key_handler which simply handles plumbing through the required type information for Rust to generate the KeyEventHandler trait implementation for you.

An example

As a real example of how this can be used, here is the power menu helper I have in my own set up which makes use of the dmenu based helpers in penrose::extensions::util::dmenu to prompt the user for a selection before executing the selected action:

#![allow(unused)]
fn main() {
use penrose::{
    builtin::actions::key_handler,
    core::bindings::KeyEventHandler,
    custom_error,
    extensions::util::dmenu::{DMenu, DMenuConfig, MenuMatch},
    util::spawn,
};
use std::process::exit;

pub fn power_menu<X: XConn>() -> KeyEventHandler<X> {
    key_handler(|state, _| {
        let options = vec!["lock", "logout", "restart-wm", "shutdown", "reboot"];
        let menu = DMenu::new(">>> ", options, DMenuConfig::default());
        let screen_index = state.client_set.current_screen().index();

        if let Ok(MenuMatch::Line(_, choice)) = menu.run(screen_index) {
            match choice.as_ref() {
                "lock" => spawn("xflock4"),
                "logout" => spawn("pkill -fi penrose"),
                "shutdown" => spawn("sudo shutdown -h now"),
                "reboot" => spawn("sudo reboot"),
                "restart-wm" => exit(0), // Wrapper script then handles restarting us
                _ => unimplemented!(),
            }
        } else {
            Ok(())
        }
    })
}
}

The window manager state is used to determine the current screen (where we want to open dmenu) but other than that we're running completely arbitrary code in response to a keypress. The main thing to keep in mind is that penrose is single threaded so anything you do in an action must complete in order for the event loop to continue running.

StackSet manipulation

The most common set of actions you'll want to perform are modifications to the StackSet in to reposition and select windows on the screen. There are a large number of methods available for modifying the current state of your windows and the modify_with helper gives you an easy way to call them directly. If you think back to the minimal example window manager we covered in the "getting started" section, we saw this in use for most of the key bindings. Paraphrasing a little, it looks like this:

#![allow(unused)]
fn main() {
use penrose::builtin::actions::modify_with;

// Select the next available layout algorithm
modify_with(|cs| cs.next_layout());

// Close the currently focused window
modify_with(|cs| cs.kill_focused());
}

Layouts

Layouts are (lets face it) a large part of why people use a dynamic tiling window manager in the first place. You want to automatically manage your windows in a way that either lets you get on with what you're doing, or looks fun and interesting!

For penrose, layouts are implemented using a trait that lets you specify how the layout should be applied and manage any additional state you might need. They also support custom messages being sent to modify their behaviour and update that state: another shamelessly re-used idea from Xmonad. You may be starting to spot a pattern here...

Taking a look at the Layout trait

Other than a few pieces of housekeeping (providing a string name to be used to identify the layout and some plumbing to help with dynamic typing) the Layout trait is primarily several methods that give you (the implementer) some flexability in how you want to approach positioning your windows and how what level of customisation you want to give the user while the window manager is running:

#![allow(unused)]
fn main() {
pub trait Layout {
    fn name(&self) -> String;
    fn boxed_clone(&self) -> Box<dyn Layout>;

    fn layout_workspace(
        &mut self,
        tag: &str,
        stack: &Option<Stack<Xid>>,
        r: Rect
    ) -> (Option<Box<dyn Layout>>, Vec<(Xid, Rect)>);

    fn layout(
        &mut self,
        s: &Stack<Xid>,
        r: Rect
    ) -> (Option<Box<dyn Layout>>, Vec<(Xid, Rect)>);
    
    fn layout_empty(
        &mut self,
        r: Rect
    ) -> (Option<Box<dyn Layout>>, Vec<(Xid, Rect)>);

    fn handle_message(&mut self, m: &Message) -> Option<Box<dyn Layout>>;
}
}

On the "laying out windows" front (you know, the main one) you have three choices:

  • Specify how to layout a possibly empty workspace based on the specific tag being laid out
  • Specify how to layout a given (non-empty) stack of clients for any workspace
  • Specify what to do when there are no clients present on the given workspace

Both layout_workspace and layout_empty have default implementations that should work in 99% of cases, leaving you the job of writing layout: how a given screen Rect should be split up between a given Stack of client windows. That said, if you do want to specify how to layout particular workspaces or give some custom logic that should run when a workspace is empty, both default implementations are of course overridable.

If you haven't read it already, it's worthwhile taking a look at the data structures section of this book to familiarise yourself with the types being discussed here!

Writing a layout function

At it's core, a layout function is pretty simple: for a given region of screen real estate, assign sub-regions to any number of the clients present on the workspace. There are no requirements to position every client and there are no requirements that clients do not overlap. There's just one key piece of information to bear in mind:

The order that you return your positions in is the order that the windows will be stacked from top to bottom.

If none of the Rects you return overlap then this doesn't matter all that much, but if you do care about stacking order, make sure to return your positions in order of top to bottom. Positions themselves are simply a tuple of (Xid, Rect). Any client window present in the provided Stack that you do not assign a position will be unmapped from the screen.

As a simple example, here is the definition (in full) of the Monocle layout from the builtin module:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy)]
pub struct Monocle;

impl Layout for Monocle {
    fn name(&self) -> String {
        "Mono".to_owned()
    }

    fn boxed_clone(&self) -> Box<dyn Layout> {
        Box::new(Monocle)
    }

    fn layout(&mut self, s: &Stack<Xid>, r: Rect) -> (Option<Box<dyn Layout>>, Vec<(Xid, Rect)>) {
        (None, vec![(s.focus, r)])
    }

    fn handle_message(&mut self, _: &Message) -> Option<Box<dyn Layout>> {
        None
    }
}
}

Pretty simple right? Admittedly, this is about as simple as you can make it (the focused window gets the full screen and everything else gets unmapped) but the overall boilerplate is kept to a minimum, which is nice.

NOTE: The builtin module has some good examples of what a "real" layout looks like (not to dunk on Monocle but...come on). Why not take a look at MainAndStack as a starting point for how to write something a little more interesting?

But, I hear you cry (silently, through the internet) those layout_* methods don't just return a Vec<(Xid, Rect)> do they? They also return an Option<Box<dyn Layout>>. What's up with that?

I'm so glad you asked.

Swapping things out for a new layout

Depending on how fancy you want to get with your layout behaviour, you might find yourself wanting to switch things out to a new Layout implementation after you've positioned a stack of client windows for a particular screen. Maybe you want to swap things out for a different layout depending on the number of clients, or the screen size, or whether the width of the screen is a multiple of 7, or maybe you want the layout to change each time it gets applied. Who knows! The point is, if you do find yourself needing to swap things out this is a way for you to do it.

In most cases you'll simply want to return None as the first value in the tuple being returned from layout methods, but if you instead return Some(new layout), penrose will swap out your current layout for the new one.

If instead you just want to update some internal state in response to an explicit trigger, that's where Messages come in.

Handling messages

Messages are a way of sending dynamically typed data to your layouts in order to update their state. A message can be literally anything so long as it implements the IntoMessage trait, which is as simple as:

#![allow(unused)]
fn main() {
impl IntoMessage for MyMessage {}
}

What any given message actually does is entirely at the discression of the Layout that handles it. So far, so vague...lets take a look at an example:

#![allow(unused)]
fn main() {
use penrose::core::layout::{IntoMessage, Layout, Message};

// First we define our message and implement the marker trait
struct SetFrobs(pub usize);
impl IntoMessage for SetFrobs {}

// Next we write our layout
struct MyLayout {
    frobs: usize,
}

impl Layout for MyLayout {
    // TODO: actually write the layout(!)

    fn handle_message(&mut self, m: &Message) -> Option<Box<dyn Layout>> {
        // If the Message is a 'SetFrobs' we'll do what it says on the tin...
        if let Some(&SetFrobs(frobs)) = m.downcast_ref() {
            self.frobs = frobs;
        }

        // ...and anything else we can just ignore

        None
    }
}
}

The downcast_ref method is the thing to pay attention to here: this is how we go from a Message (really just a wrapper around the standard library Any trait) to a concrete type. Anything that implements IntoMessage can be sent to our Layout so we do our own type checking to see if the message is something we care about. Messages that we don't handle can safely be dropped on the floor (so don't worry about needing to exhaustively check all possible message types).

The Option<Box dyn Layout> return type is the same idea as with the layout_* methods covered above: in response to a message you can swap out to a new layout. Say hypothetically, there was a frob threshold above which things got really awesome...

#![allow(unused)]
fn main() {
// A more AWESOME layout
struct MyAwesomeLayout {
    frobs: usize,
}

// Which has its own Layout implementation
impl Layout for MyAwesomeLayout {
    // ...
}

const AWESOMENESS_THRESHOLD: usize = 42;

// Now, we modify our impl for MyLayout to "level up" once we hit the threshold
impl Layout for MyLayout {
    // TODO: still need to write the layout at some point...

    fn handle_message(&mut self, m: &Message) -> Option<Box<dyn Layout>> {
        if let Some(&SetFrobs(frobs)) = m.downcast_ref() {
            if frobs > AWESOMENESS_THRESHOLD {
                // Things are getting awesome!
                return Some(Box::new(MyAwesomeLayout { frobs }));
            }

            // Still pretty cool, but not awesome yet...
            self.frobs = frobs;
        }

        None
    }
}
}

Nice!

That's all well and good if we have a bunch of our own layouts that we can write and swap between, but what if we just want to tweak an existing layout a bit? Well that's where we move over to the wonderful world of LayoutTransformers.

Layout transformers

This one is a bit of a rabbit hole...for now we'll cover the basics of what you can do with a transformer and leave the details to the module docs themselves as there's quite a bit to cover!

LayoutTransformer is (surprise, surprise) another trait you can implement. It represents a wrapper around an inner Layout which you (the author of the transformer) get to lie to help reach its full potential. The two main things that a transformer can do are:

  • Modify the dimensions of the initial Rect being passed to the inner layout
  • Modify the positions returned by the inner layout before they are handed off for processing

So what does that let you do? Well for one thing, this is how gaps are implemented for any layout in penrose. The Gaps transformer from the builtin module shrinks the size of the initial screen seen by the inner layout (to give you an outer gap) and then shrinks the size of each window once the layout has run (to give you an inner gap).

For simple cases where you just want to modify the positions returned by an inner layout, there's a handy builtin macro to generate a LayoutTransformer from a function:

#![allow(unused)]
fn main() {
use penrose::{pure::geometry::Rect, simple_transformer, Xid};

fn my_transformer(r: Rect, positions: Vec<(Xid, Rect)>) -> Vec<(Xid, Rect)> {
    // Write your transformation implementation here
}

simple_transformer!("MyTransform", MyTransformer, my_transformer);
}

Penrose FAQs

How do I install Penrose?

You don't: Penrose is a library that you use to write your own window manager. Take a look at the getting started guide for details of how to use Penrose as a library.

Where can I view the Penrose source code?

Penrose is developed openly on GitHub and published to crates.io periodically as new features are added. The develop branch always has the latest code and is what I use for running Penrose on my personal laptop. It is not advised that you pin your use of Penrose to the GitHub develop branch as a typically end user however: breaking changes (and weird and wonderful bugs) are highly likely.

You have been warned!

How does Penrose differ from other tiling window managers?

Penrose is a tiling window manager library rather than a tiling window manager. It provides core logic and traits (interfaces) for writing your own tiling window manager, along with default implementations that can be used out of the box. That said, you can't install Penrose: you need to write your own Rust crate that brings in Penrose as a dependency.

The Penrose repository has several up to date examples of what a typical main.rs ends up looking like and there is a guide on how to go from installing rust to running Penrose as your window manager located here

Does Penrose support Wayland as a back end?

Short answer: no.

Long answer:

Wayland merges the concept of the window manager with the that of the compositor, which results in significantly more work (which I'm not planning on doing given that I'm perfectly happy with X11 as a back end). The internal APIs of Penrose only expect to be managing window positioning and workspaces (as far as X is concerned) so while it may be possibly to add Wayland support, it's not a simple task. It is definitely something that would be interesting to look into in the future but it's not a high priority for me personally as I am perfectly happy running X11 for now.

Where's the eye candy?

Short answer: there isn't any.

Long answer:

Penrose is, first and foremost, designed with simplicity, speed and stability in mind. This means that the default, out of the box offering is pretty minimal. I'm a big fan of the unix philosophy and with that in mind, Penrose largely restricts its core functionality to managing your windows. Decorations and animation are not first class citizens but can be added through extensions and user code if desired.

Are you accepting contributions for open issues?

Short answer: please discuss on the issue in question

Long answer:

Typically issues in the GitHub issue tracker are already being worked on or are blocked for some particular reason that should be clear from the issue. If you would like to work on an open issue that looks to be stalled please add a comment to the issue in question registering your interest.

If you would like to raise a bug report or make a feature request then please open a new issue and get confirmation that the change / approach to the fix is somethat that is likely to be accepted before starting work.

Can I raise a Pull Request adding a shiny new feature?

Short answer: please raise an issue first to discuss what it is you want to add.

Long answer:

No really, please make sure to raise an issue in GitHub before raising a pull request in the repo. I'm very happy to accept contributions for both bug fixes and new functionality but (like most open source maintainers) I do not have time to review pull requests that have had no prior discussion before being raised. If there are any issues with the approach being taken (or breaking changes / conflicts with ongoing work) it can end up with a reasonable amount of back and forth as changes are requested and made.

Put simply, it's a far better experience for me as a maintainer and you as a contributor to get a thumbs up on an approach before spending time on the implementation!

Can you add 'feature X' from this other window manager?

Short answer: probably not as a core feature, but feel free to raise an issue to discuss it.

Long answer:

I started penrose because 1) I like hacking on stuff and it seemed like a fun idea and 2) I was dissatisfied with the feature sets offered by other window managers. Some had everything I wanted, but came with things that I really didn't like, while others felt like they were missing features. penrose has been written to be a base layer that you can build from to write a window manager that works how you want it to: this means that there is a small set of opinionated, core functionality and then a variety of ways to extend this. There are likely a few pieces of functionality that I have missed that can be added to core without disrupting what is already there, but most "missing" features from other window managers are missing on purpose. If you would like to add them as an extension, please see the contribution guidelines above.

One important category of functionality that will not be added to the core of the penrose crate itself is any sort of helper program or additional scripts that aim to wrap penrose and make it look like a stand alone binary.

penrose is (as clearly stated in the README) a library, not a binary.

If writing your own crate and compiling and installing the resulting binary is not something you want to manage and maintain, then penrose is not for you.