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()); }