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}