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 theXConn
used by your window manager and runs whatever custom code you care to write.modify_with
: for callingpure
state methods this helper handles the diff and refresh cycle for you. Simply update theStackSet
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 aMessage
and sends it to the active layout. (Useful for updating your layout behaviour on the fly).broadcast_layout_message
: does the same thing assend_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 asdmenu
orrofi
. 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:
- A Stack is never empty (there is always at least the focused element)
- Operations that manipulate which element is focused do not alter the order of the elements themselves.
- 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 onMonocle
but...come on). Why not take a look atMainAndStack
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.