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!