libawm/core/hooks.rs
1//! Hook for adding additional functionality around standard WindowManager actions
2//!
3//! # Overview
4//!
5//! Hooks are the primary way of injecting custom functionality into penrose when you want to go
6//! beyond simply binding actions to key presses. There are multiple points in normal
7//! [WindowManager] execution that will trigger the running of user defined hooks, during which you
8//! will have complete control over the window manager state and (importantly) block the event loop
9//! until your hook exits. For details of what hook points are available, see each of the trait
10//! methods outlined below. Note that a single [Hook] can register itself to be called at multiple
11//! hook points (all, if desired!) and that hooks are allways called in the order that they are
12//! registered with the [WindowManager] on init (i.e. the order of the `Vec` itself).
13//!
14//! # Implementing Hook
15//!
16//! As an example of how to write a hook and register it, lets implement a simple hook that logs
17//! each new client that is added to a particular workspace, noting if we've seen it before or not.
18//! Completely pointless, but it will serve as a nice starting point to show what is happening.
19//!
20//! ```no_run
21//! use penrose::{
22//! core::{
23//! hooks::Hook,
24//! xconnection::{XConn, Xid},
25//! },
26//! xcb::XcbConnection,
27//! Config, Result, WindowManager, logging_error_handler
28//! };
29//!
30//! use std::collections::{HashMap, HashSet};
31//!
32//! use tracing::info;
33//!
34//! // Start with the struct itself which will contain any internal state we need to track
35//! pub struct LogAddedClients {
36//! seen: HashMap<usize, HashSet<Xid>>,
37//! }
38//!
39//! // It is idiomatic for Hooks to provide a `new` method that returns a pre-boxed struct
40//! // so that you can add it straight into your hooks Vector in your main.rs
41//! impl LogAddedClients {
42//! pub fn new() -> Box<Self> {
43//! Box::new(Self { seen: HashMap::new() })
44//! }
45//! }
46//!
47//! // As we only care about one of the hook points, that is the only method we need to
48//! // implement: all other Hook methods for this struct will be no-ops
49//! impl<X: XConn> Hook<X> for LogAddedClients {
50//! fn client_added_to_workspace(
51//! &mut self,
52//! wm: &mut WindowManager<X>,
53//! id: Xid,
54//! wix: usize
55//! ) -> Result<()> {
56//! let clients = self.seen.entry(wix).or_insert(HashSet::new());
57//! if clients.contains(&id) {
58//! info!("'{}' has been on '{}' before!", id, wix)
59//! } else {
60//! clients.insert(id);
61//! info!("'{}' was added to '{}' for the first time", id, wix)
62//! };
63//!
64//! Ok(())
65//! }
66//! }
67//!
68//! // Now we simply pass our hook to the WindowManager when we create it
69//! fn main() -> penrose::Result<()> {
70//! let mut manager = WindowManager::new(
71//! Config::default(),
72//! XcbConnection::new()?,
73//! vec![LogAddedClients::new()],
74//! logging_error_handler()
75//! );
76//!
77//! manager.init()?;
78//!
79//! // rest of your startup logic here
80//!
81//! Ok(())
82//! }
83//! ```
84//!
85//! Now, whenever a [Client][4] is added to a [Workspace][1] (either because it has been newly
86//! created, or because it has been moved from one workspace to another) our hook will be called,
87//! and our log message will be included in the penrose log stream. More complicated hooks can be
88//! built that listen to multiple triggers, but most of the time you will likely only need to
89//! implement a single method. For an example of a more complex set up, see the [Scratchpad][2]
90//! extension which uses multiple hooks to spawn and manage a client program outside of normal
91//! `WindowManager` operation.
92//!
93//! # When hooks are called
94//!
95//! Each Hook trigger will be called as part of normal execution of `WindowManager` methods at a
96//! point that should be relatively intuitive based on the name of the method. Each method provides
97//! a more detailed explanation of exactly what conditions it will be called under. If you would
98//! like to see exactly which user level actions lead to specific triggers, try turning on `DEBUG`
99//! logging in your logging config as part of your **main.rs** and lookk for the "Running <method>
100//! hooks" message that each trigger logs out.
101//!
102//! *Please see the documentation on each of the individual methods for more details.*
103//!
104//! # WindowManager execution with user defined Hooks
105//!
106//! As mentioned above, each time a hook trigger point is reached the `WindowManager` stops normal
107//! execution (including responding to [XEvents][3]) and each of the registered hooks is called in
108//! turn. If the hook implements the method associated with the trigger that has been hit, then
109//! your logic will be run and you will have a mutable reference to the current [WindowManager]
110//! state, giving you complete control over what happens next. Note that method calls on the
111//! `WindowManager` itself will (of course) resolve immediately, but that any actions which
112//! generate [XEvents][3] will only be processed once all hooks have run and control has returned to
113//! the manager itself.
114//!
115//! [1]: crate::core::workspace::Workspace
116//! [2]: crate::contrib::extensions::scratchpad::Scratchpad
117//! [3]: crate::core::xconnection::XEvent
118//! [4]: crate::core::client::Client
119use crate::{
120 core::{
121 data_types::Region,
122 manager::WindowManager,
123 xconnection::{XConn, Xid},
124 },
125 Result,
126};
127
128/// Names of each of the individual hooks that are triggerable in Penrose.
129///
130/// This enum is used to indicate to the [WindowManager] that a particular hook should now be
131/// triggered as the result of some other action that has taken place during execution.
132#[non_exhaustive]
133#[allow(missing_docs)]
134#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
135#[derive(Debug, PartialEq, Eq, Hash)]
136pub enum HookName {
137 Startup,
138 NewClient(Xid),
139 RemoveClient(Xid),
140 ClientAddedToWorkspace(Xid, usize),
141 ClientNameUpdated(Xid, String, bool),
142 LayoutApplied(usize, usize),
143 LayoutChange(usize),
144 WorkspaceChange(usize, usize),
145 WorkspacesUpdated(Vec<String>, usize),
146 ScreenChange,
147 ScreenUpdated,
148 RanderNotify,
149 FocusChange(u32),
150 EventHandled,
151}
152
153/// Utility type for defining hooks in your penrose configuration.
154pub type Hooks<X> = Vec<Box<dyn Hook<X>>>;
155
156/// User defined functionality triggered by [WindowManager] actions.
157///
158/// impls of [Hook] can be registered to receive events during [WindowManager] operation. Each hook
159/// point is documented as individual methods detailing when and how they will be called. All
160/// registered hooks will be called for each trigger so the required methods all provide a no-op
161/// default implementation that must be overriden to provide functionality. Hooks may subscribe to
162/// multiple triggers to implement more complex behaviours and may store additional state.
163///
164/// *Care should be taken when writing [Hook] impls to ensure that infinite loops are not created by
165/// nested triggers and that, where possible, support for other hooks running from the same triggers
166/// is possible.*
167///
168///
169/// # Implementing Hook
170///
171/// For an example of how to write Hooks, please see the [module level][1] documentation.
172///
173/// Note that you only need to implement the methods for triggers you intended to respond to: all
174/// hook methods have a default empty implementation that is ignored by the `WindowManager`.
175///
176/// [1]: crate::core::hooks
177pub trait Hook<X: XConn> {
178 /// # Trigger Point
179 ///
180 /// Called once at [WindowManager] startup in [grab_keys_and_run][1] after setting up signal handlers
181 /// and grabbing key / mouse bindings but before entering the main event loop that polls for
182 /// [XEvents][2].
183 ///
184 /// # Example Uses
185 ///
186 /// When this trigger is reached, the `WindowManager` will have initialised all of its internal
187 /// state, including setting up [Workspaces][3] and [Screens][4] so any set up logic for Hooks
188 /// that requires access to this should be placed in a `startup` hook as opposed to being
189 /// attempted in the `new` method of the hook itself.
190 ///
191 /// [1]: crate::core::manager::WindowManager::grab_keys_and_run
192 /// [2]: crate::core::xconnection::XEvent
193 /// [3]: crate::core::workspace::Workspace
194 /// [4]: crate::core::screen::Screen
195 #[allow(unused_variables)]
196 fn startup(&mut self, wm: &mut WindowManager<X>) -> Result<()> {
197 Ok(())
198 }
199
200 /// # Trigger Point
201 ///
202 /// Called when a new [Client][5] has been created in response to map request and all penrose
203 /// specific state has been initialised, but before the client has been added to the active
204 /// [Workspace][1] and before any [Layouts][2] have been applied.
205 ///
206 /// The `client` argument is the newly created Client which can be modified if desired and
207 /// optionally marked as [externally_managed][3] which will prevent penrose from adding it to a
208 /// workspace. If the hook takes ownership of the client in this way then it is responsible
209 /// for ensuring that it mapped and unmapped.
210 ///
211 /// # Example Uses
212 ///
213 /// Inspecting newly created clients is the first and most obvious use of this hook but more
214 /// advanced actions can be performed if the hook takes ownership of the client. For an
215 /// example, see the [Scratchpad][4] extension which uses this hook to capture a spawned client.
216 ///
217 /// [1]: crate::core::workspace::Workspace
218 /// [2]: crate::core::layout::Layout
219 /// [3]: crate::core::client::Client::externally_managed
220 /// [4]: crate::contrib::extensions::scratchpad::Scratchpad
221 /// [5]: crate::core::client::Client
222 #[allow(unused_variables)]
223 fn new_client(&mut self, wm: &mut WindowManager<X>, id: Xid) -> Result<()> {
224 Ok(())
225 }
226
227 /// # Trigger Point
228 ///
229 /// Called *after* a [Client][3] is removed from internal [WindowManager] state, either through
230 /// a user initiated [kill_client][1] action or the underlying program exiting.
231 ///
232 /// # Example Uses
233 ///
234 /// This hook is called after the client has already been removed, so it is not possible to
235 /// interact with the client in any way. This is typically used as a companion to
236 /// [new_client][2] when managing a target client externally.
237 ///
238 /// [1]: crate::core::manager::WindowManager::kill_client
239 /// [2]: Hook::new_client
240 /// [3]: crate::core::client::Client
241 #[allow(unused_variables)]
242 fn remove_client(&mut self, wm: &mut WindowManager<X>, id: Xid) -> Result<()> {
243 Ok(())
244 }
245
246 /// # Trigger Point
247 ///
248 /// Called whenever an existing [Client][5] is added to a [Workspace][1]. This includes newly
249 /// created clients when they are first mapped and clients being moved between workspaces using
250 /// the [client_to_workspace][2] method on [WindowManager].
251 ///
252 /// # Example Uses
253 ///
254 /// The built in [status bar][3] widget [Workspaces][4] uses this to keep track of whether or
255 /// not each workspace is occupied or not.
256 ///
257 /// [1]: crate::core::workspace::Workspace
258 /// [2]: crate::core::manager::WindowManager::client_to_workspace
259 /// [3]: crate::draw::bar::StatusBar
260 /// [4]: crate::draw::widget::bar::Workspaces
261 /// [5]: crate::core::client::Client
262 #[allow(unused_variables)]
263 fn client_added_to_workspace(
264 &mut self,
265 wm: &mut WindowManager<X>,
266 id: Xid,
267 wix: usize,
268 ) -> Result<()> {
269 Ok(())
270 }
271
272 /// # Trigger Point
273 ///
274 /// Called whenever something updates the WM_NAME or _NET_WM_NAME property on a window.
275 /// `is_root == true` indicates that this is the root window that is being modified.
276 ///
277 /// # Example Uses
278 ///
279 /// This allows for simple setting / fetching of string data from individual clients or the
280 /// root X window. In particular, this allows for a [dwm][1] style API for controlling
281 /// something like a status bar by setting the root window name and then reading it inside of a
282 /// hook.
283 ///
284 /// [1]: https://dwm.suckless.org/
285 #[allow(unused_variables)]
286 fn client_name_updated(
287 &mut self,
288 wm: &mut WindowManager<X>,
289 id: Xid,
290 name: &str,
291 is_root: bool,
292 ) -> Result<()> {
293 Ok(())
294 }
295
296 /// # Trigger Point
297 ///
298 /// Called after a [Layout][1] is applied to the active Workspace.
299 ///
300 /// Arguments are indices into the WindowManager workspace and screen rings (internal data
301 /// structures that support indexing) which can be used to fetch references to the active [Workspace][2]
302 /// and [Screen][3]. Note that this is called for every application of the layout which
303 /// includes:
304 ///
305 /// - changing the active workspace
306 /// - adding or removing a client from the active workspace
307 /// - updating the main ratio or number of master clients
308 /// - user calls to the [layout_screen][4] method on [WindowManager]
309 ///
310 /// # Example Uses
311 ///
312 /// Running logic that applies after windows have been positioned for a given workspace: for
313 /// example, ensuring that a particular window is always in a certain position or that it
314 /// floats above all other windows.
315 ///
316 /// [1]: crate::core::layout::Layout
317 /// [2]: crate::core::workspace::Workspace
318 /// [3]: crate::core::screen::Screen
319 /// [4]: crate::core::manager::WindowManager::layout_screen
320 #[allow(unused_variables)]
321 fn layout_applied(
322 &mut self,
323 wm: &mut WindowManager<X>,
324 workspace_index: usize,
325 screen_index: usize,
326 ) -> Result<()> {
327 Ok(())
328 }
329
330 /// # Trigger Point
331 ///
332 /// Called after a workspace's [Layout][1] has been updated via [cycle_layout][4].
333 ///
334 /// Arguments are indices into the WindowManager workspace and screen rings (internal data
335 /// structures that support indexing) which can be used to fetch references to the active [Workspace][2]
336 /// and [Screen][3].
337 ///
338 /// # Example Uses
339 ///
340 /// Running additional setup logic for more complex layout functions that can not be done when
341 /// the layout itself is invoked.
342 ///
343 /// [1]: crate::core::layout::Layout
344 /// [2]: crate::core::workspace::Workspace
345 /// [3]: crate::core::screen::Screen
346 /// [4]: crate::core::manager::WindowManager::cycle_layout
347 #[allow(unused_variables)]
348 fn layout_change(
349 &mut self,
350 wm: &mut WindowManager<X>,
351 workspace_index: usize,
352 screen_index: usize,
353 ) -> Result<()> {
354 Ok(())
355 }
356
357 /// # Trigger Point
358 ///
359 /// Called after the active [Workspace][1] is changed on a [Screen][2].
360 ///
361 /// Arguments are indices into the WindowManager workspace ring (internal data structure that
362 /// supports indexing) for the previous and new workspace.
363 ///
364 /// # Example Uses
365 ///
366 /// Triggering logic when a particular workspace gains or loses focus.
367 ///
368 /// [1]: crate::core::workspace::Workspace
369 /// [2]: crate::core::screen::Screen
370 #[allow(unused_variables)]
371 fn workspace_change(
372 &mut self,
373 wm: &mut WindowManager<X>,
374 previous_workspace: usize,
375 new_workspace: usize,
376 ) -> Result<()> {
377 Ok(())
378 }
379
380 /// # Trigger Point
381 ///
382 /// Called whenever a [Workspace][1] is dynamically added or removed from the list of known
383 /// workspaces once penrose is running.
384 ///
385 /// # Example Uses
386 ///
387 /// Updating hooks that care about tracking workspaces when the list of available workspaces is
388 /// being dynamically updated while the [WindowManager] is running.
389 ///
390 /// [1]: crate::core::workspace::Workspace
391 #[allow(unused_variables)]
392 fn workspaces_updated(
393 &mut self,
394 wm: &mut WindowManager<X>,
395 names: &[&str],
396 active: usize,
397 ) -> Result<()> {
398 Ok(())
399 }
400
401 /// # Trigger Point
402 ///
403 /// Called after focus moves to a new [Screen][1].
404 ///
405 /// Argument is a index into the WindowManager screen ring (internal data structure that supports
406 /// indexing) for the new Screen.
407 ///
408 /// # Example Uses
409 ///
410 /// Tracking which screen is currently focused without needing to poll state in the
411 /// `WindowManager`.
412 ///
413 /// [1]: crate::core::screen::Screen
414 #[allow(unused_variables)]
415 fn screen_change(&mut self, wm: &mut WindowManager<X>, screen_index: usize) -> Result<()> {
416 Ok(())
417 }
418
419 /// # Trigger Point
420 ///
421 /// Called when the list of known [Screens][1] is updated via the [detect_screens][2] method on
422 /// the `WindowManager`.
423 ///
424 /// # Example Uses
425 ///
426 /// Tracking Screen sizes and details without needing to poll / check every time your hook is
427 /// called.
428 ///
429 /// [1]: crate::core::screen::Screen
430 /// [2]: crate::core::manager::WindowManager::detect_screens
431 #[allow(unused_variables)]
432 fn screens_updated(&mut self, wm: &mut WindowManager<X>, dimensions: &[Region]) -> Result<()> {
433 Ok(())
434 }
435
436 /// # Trigger Point
437 ///
438 /// Called when the underlying [XConn] emitted a [RandrNotify][1] event.
439 ///
440 /// This hook will run _before_ polling state for newly connected screens and running the
441 /// [screens_updated][2] hook.
442 ///
443 /// # Example Uses
444 ///
445 /// This is where any logic you want to run when external monitors are added / removed should
446 /// be placed.
447 ///
448 /// [1]: crate::core::xconnection::XEvent::RandrNotify
449 /// [2]: Hook::screens_updated
450 #[allow(unused_variables)]
451 fn randr_notify(&mut self, wm: &mut WindowManager<X>) -> Result<()> {
452 Ok(())
453 }
454
455 /// # Trigger Point
456 ///
457 /// Called after a [Client][1] gains focus.
458 ///
459 /// Argument is the focused Client ID which can be used to fetch the internal Client state if
460 /// needed.
461 ///
462 /// # Example Uses
463 ///
464 /// Updating information about the focused client, such as in the [ActiveWindowName][2] status
465 /// bar widget.
466 ///
467 /// [1]: crate::core::client::Client
468 /// [2]: crate::draw::widget::bar::ActiveWindowName
469 #[allow(unused_variables)]
470 fn focus_change(&mut self, wm: &mut WindowManager<X>, id: Xid) -> Result<()> {
471 Ok(())
472 }
473
474 /// # Trigger Point
475 ///
476 /// Called at the bottom of the main [WindowManager] event loop after each [XEvent][1] is handled.
477 ///
478 /// # Example Uses
479 ///
480 /// Useful if you want to ensure that all other event processing has taken place before you
481 /// take action in response as part of a more complex hook.
482 ///
483 /// [1]: crate::core::xconnection::XEvent
484 #[allow(unused_variables)]
485 fn event_handled(&mut self, wm: &mut WindowManager<X>) -> Result<()> {
486 Ok(())
487 }
488}