libawm/core/xconnection/
mod.rs

1//! An abstraciton layer for talking to an underlying X server.
2//!
3//! An implementation of the [XConn] trait is required for running a [WindowManager][1]. The choice
4//! of back end (e.g. xlib, xcb...) is an implementation detail that does not surface in the
5//! `WindowManager` itself. All low level details of working with the X server should be captured in
6//! this trait, though accessing backend specific functionality is possible by writing an impl
7//! block for `WindowManager<YourXConn>` if desired.
8//!
9//! [1]: crate::core::manager::WindowManager
10use crate::{
11    core::{
12        bindings::{KeyBindings, KeyPress, MouseBindings},
13        client::Client,
14        data_types::{Point, Region},
15        screen::Screen,
16    },
17    draw::Color,
18};
19
20use penrose_proc::stubbed_companion_trait;
21
22pub mod atom;
23pub mod event;
24pub mod property;
25
26pub use atom::{
27    Atom, AtomIter, AUTO_FLOAT_WINDOW_TYPES, EWMH_SUPPORTED_ATOMS, UNMANAGED_WINDOW_TYPES,
28};
29pub use event::{
30    ClientEventMask, ClientMessage, ClientMessageData, ClientMessageKind, ConfigureEvent,
31    ExposeEvent, PointerChange, PropertyEvent, XEvent,
32};
33pub use property::{
34    MapState, Prop, WindowAttributes, WindowClass, WindowState, WmHints, WmNormalHints,
35    WmNormalHintsFlags,
36};
37
38/// An X resource ID
39pub type Xid = u32;
40
41const WM_NAME: &str = "penrose";
42
43/// Enum to store the various ways that operations can fail in X traits
44#[derive(thiserror::Error, Debug)]
45pub enum XError {
46    /// The underlying connection to the X server is closed
47    #[error("The underlying connection to the X server is closed")]
48    ConnectionClosed,
49
50    /// Client data was malformed
51    #[error("Invalid client message format: {0} (expected 8, 16 or 32)")]
52    InvalidClientMessageData(u8),
53
54    /// The requested property is not set for the given client
55    #[error("The {0} property is not set for client {1}")]
56    MissingProperty(String, Xid),
57
58    /// A generic error type for use in user code when needing to construct
59    /// a simple [XError].
60    #[error("Unhandled error: {0}")]
61    Raw(String),
62
63    /// Parsing an [Atom][crate::core::xconnection::Atom] from a str failed.
64    ///
65    /// This happens when the atom name being requested is not a known atom.
66    #[error(transparent)]
67    Strum(#[from] strum::ParseError),
68
69    /// An attempt was made to reference an atom that is not known to penrose
70    #[error("{0} is not a known atom")]
71    UnknownAtom(Xid),
72
73    /// An attempt was made to reference a client that is not known to penrose
74    #[error("{0} is not a known client")]
75    UnknownClient(Xid),
76
77    /*
78     * Conversions from other penrose error types
79     */
80    /// Something went wrong using the [xcb][crate::xcb] module.
81    ///
82    /// See [XcbError][crate::xcb::XcbError] for variants.
83    #[cfg(feature = "xcb")]
84    #[error(transparent)]
85    Xcb(#[from] crate::xcb::XcbError),
86
87    /// Something went wrong using the [x11rb][crate::x11rb] module.
88    ///
89    /// See [X11rbError][crate::x11rb::X11rbError] for variants.
90    #[cfg(feature = "x11rb")]
91    #[error(transparent)]
92    X11rb(#[from] crate::x11rb::X11rbError),
93}
94
95/// Result type for errors raised by X traits
96pub type Result<T> = std::result::Result<T, XError>;
97
98/// On screen configuration options for X clients (not all are curently implemented)
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub enum ClientConfig {
102    /// The border width in pixels
103    BorderPx(u32),
104    /// Absolute size and position on the screen as a [Region]
105    Position(Region),
106    /// Mark this window as stacking on top of its peers
107    StackAbove,
108}
109
110/// Attributes for an X11 client window (not all are curently implemented)
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
113pub enum ClientAttr {
114    /// Border color as an argb hex value
115    BorderColor(u32),
116    /// Set the pre-defined client event mask
117    ClientEventMask,
118    /// Set the pre-defined root event mask
119    RootEventMask,
120}
121
122/// An [XEvent] parsed into a [KeyPress] if possible, otherwise the original `XEvent`
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum KeyPressParseAttempt {
125    /// The event was parasble as a [KeyPress]
126    KeyPress(KeyPress),
127    /// The event was not a [KeyPress]
128    XEvent(XEvent),
129}
130
131/// Convert between string representations of X atoms and their IDs
132#[stubbed_companion_trait(doc_hidden = "true")]
133pub trait XAtomQuerier {
134    /// Convert an X atom id to its human friendly name
135    #[stub(Err(XError::Raw("mocked".into())))]
136    fn atom_name(&self, atom: Xid) -> Result<String>;
137
138    /// Fetch or intern an atom by name
139    #[stub(Err(XError::Raw("mocked".into())))]
140    fn atom_id(&self, name: &str) -> Result<Xid>;
141}
142
143/// State queries against the running X server
144#[stubbed_companion_trait(doc_hidden = "true")]
145pub trait XState: XAtomQuerier {
146    /// The root window ID
147    #[stub(42)]
148    fn root(&self) -> Xid;
149
150    /// Determine the currently connected [screens][Screen] and return their details
151    #[stub(Ok(vec![]))]
152    fn current_screens(&self) -> Result<Vec<Screen>>;
153
154    /// Determine the current (x,y) position of the cursor relative to the root window.
155    #[stub(Ok(Point::default()))]
156    fn cursor_position(&self) -> Result<Point>;
157
158    /// Warp the cursor to be within the specified window. If id == None then behaviour is
159    /// definined by the implementor (e.g. warp cursor to active window, warp to center of screen)
160    #[stub(Ok(()))]
161    fn warp_cursor(&self, win_id: Option<Xid>, screen: &Screen) -> Result<()>;
162
163    /// Return the current (x, y, w, h) dimensions of the requested window
164    #[stub(Ok(Region::default()))]
165    fn client_geometry(&self, id: Xid) -> Result<Region>;
166
167    /// Run on startup/restart to determine already running windows that we need to track
168    #[stub(Ok(vec![]))]
169    fn active_clients(&self) -> Result<Vec<Xid>>;
170
171    /// Return the client ID of the [crate::core::client::Client] that currently holds X focus
172    #[stub(Ok(0))]
173    fn focused_client(&self) -> Result<Xid>;
174}
175
176/// Sending and receiving X events
177#[stubbed_companion_trait(doc_hidden = "true")]
178pub trait XEventHandler {
179    /// Flush pending actions to the X event loop
180    #[stub(true)]
181    fn flush(&self) -> bool;
182
183    /// Wait for the next event from the X server and return it as an [XEvent]
184    #[stub(Err(XError::Raw("mocked".into())))]
185    fn wait_for_event(&self) -> Result<XEvent>;
186
187    /// Send an X event to the target client
188    ///
189    /// The `msg` being sent can be composed by hand or, for known common message types, generated
190    /// using the [build_client_event][1] method.
191    ///
192    /// [1]: XEventHandler::build_client_event
193    #[stub(Err(XError::Raw("mocked".into())))]
194    fn send_client_event(&self, msg: ClientMessage) -> Result<()>;
195
196    /// Build the required event data for sending a known client event.
197    #[stub(Err(XError::Raw("mocked".into())))]
198    fn build_client_event(&self, kind: ClientMessageKind) -> Result<ClientMessage>;
199}
200
201/// Management of the visibility and lifecycle of X clients
202#[stubbed_companion_trait(doc_hidden = "true")]
203pub trait XClientHandler {
204    /// Map a client to the display.
205    #[stub(Ok(()))]
206    fn map_client(&self, id: Xid) -> Result<()>;
207
208    /// Unmap a client from the display.
209    #[stub(Ok(()))]
210    fn unmap_client(&self, id: Xid) -> Result<()>;
211
212    /// Destroy an existing client.
213    #[stub(Ok(()))]
214    fn destroy_client(&self, id: Xid) -> Result<()>;
215
216    /// Forcably kill an existing client.
217    #[stub(Ok(()))]
218    fn kill_client(&self, id: Xid) -> Result<()>;
219
220    /// Mark the given client as having focus
221    #[stub(Ok(()))]
222    fn focus_client(&self, id: Xid) -> Result<()>;
223
224    /// Map a known penrose [Client] if it is not currently visible
225    fn map_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
226        if let Some(c) = win {
227            if !c.mapped {
228                c.mapped = true;
229                self.map_client(c.id())?;
230            }
231        }
232        Ok(())
233    }
234
235    /// Unmap a known penrose [Client] if it is currently visible
236    fn unmap_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
237        if let Some(c) = win {
238            if c.mapped {
239                c.mapped = false;
240                self.unmap_client(c.id())?;
241            }
242        }
243        Ok(())
244    }
245}
246
247/// Querying and updating properties on X clients
248#[stubbed_companion_trait(doc_hidden = "true")]
249pub trait XClientProperties {
250    /// Return the list of all properties set on the given client window
251    ///
252    /// Properties should be returned as their string name as would be used to intern the
253    /// respective atom.
254    #[stub(Ok(vec![]))]
255    fn list_props(&self, id: Xid) -> Result<Vec<String>>;
256
257    /// Query a property for a client by ID and name.
258    ///
259    /// Can fail if the property name is invalid or we get a malformed response from xcb.
260    #[stub(Err(XError::Raw("mocked".into())))]
261    fn get_prop(&self, id: Xid, name: &str) -> Result<Prop>;
262
263    /// Delete an existing property from a client
264    #[stub(Ok(()))]
265    fn delete_prop(&self, id: Xid, name: &str) -> Result<()>;
266
267    /// Change an existing property for a client
268    #[stub(Ok(()))]
269    fn change_prop(&self, id: Xid, name: &str, val: Prop) -> Result<()>;
270
271    /// Update a client's `WM_STATE` property to the given value.
272    ///
273    /// See the [ICCCM docs][1] for more information on what each value means for the client.
274    ///
275    /// [1]: https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.3.1
276    #[stub(Ok(()))]
277    fn set_client_state(&self, id: Xid, wm_state: WindowState) -> Result<()>;
278
279    /*
280     *  The following default implementations should used if possible.
281     *
282     *  Any custom implementations should take care to ensure that the state changes being made are
283     *  equivaled to those implemented here.
284     */
285
286    /// Check to see if a given client window supports a particular protocol or not
287    fn client_supports_protocol(&self, id: Xid, proto: &str) -> Result<bool> {
288        match self.get_prop(id, Atom::WmProtocols.as_ref()) {
289            Ok(Prop::Atom(protocols)) => Ok(protocols.iter().any(|p| p == proto)),
290            Ok(p) => Err(XError::Raw(format!("Expected atoms, got {:?}", p))),
291            Err(XError::MissingProperty(_, _)) => Ok(false),
292            Err(e) => Err(e),
293        }
294    }
295
296    /// Check to see if a given client accepts input focus
297    fn client_accepts_focus(&self, id: Xid) -> bool {
298        match self.get_prop(id, Atom::WmHints.as_ref()) {
299            Ok(Prop::WmHints(WmHints { accepts_input, .. })) => accepts_input,
300            _ => true,
301        }
302    }
303
304    /// Toggle the fullscreen state of the given client ID with the X server
305    fn toggle_client_fullscreen(&self, id: Xid, client_is_fullscreen: bool) -> Result<()> {
306        let data = if client_is_fullscreen {
307            vec![]
308        } else {
309            vec![Atom::NetWmStateFullscreen.as_ref().to_string()]
310        };
311
312        self.change_prop(id, Atom::NetWmState.as_ref(), Prop::Atom(data))
313    }
314
315    /// Fetch a [client's][1] name proprty following ICCCM / EWMH standards
316    ///
317    /// [1]: crate::core::client::Client
318    fn client_name(&self, id: Xid) -> Result<String> {
319        match self.get_prop(id, Atom::NetWmName.as_ref()) {
320            Ok(Prop::UTF8String(strs)) if !strs.is_empty() && !strs[0].is_empty() => {
321                Ok(strs[0].clone())
322            }
323
324            _ => match self.get_prop(id, Atom::WmName.as_ref()) {
325                Ok(Prop::UTF8String(strs)) if !strs.is_empty() => Ok(strs[0].clone()),
326                Err(e) => Err(e),
327                _ => Ok(String::new()),
328            },
329        }
330    }
331
332    /// Determine whether the target client should be tiled or allowed to float
333    fn client_should_float(&self, id: Xid, floating_classes: &[&str]) -> bool {
334        if let Ok(prop) = self.get_prop(id, Atom::WmTransientFor.as_ref()) {
335            trace!(?prop, "window is transient: setting to floating state");
336            return true;
337        }
338
339        if let Ok(Prop::UTF8String(strs)) = self.get_prop(id, Atom::WmClass.as_ref()) {
340            if strs.iter().any(|c| floating_classes.contains(&c.as_ref())) {
341                return true;
342            }
343        }
344
345        let float_types: Vec<&str> = AUTO_FLOAT_WINDOW_TYPES.iter().map(|a| a.as_ref()).collect();
346        if let Ok(Prop::Atom(atoms)) = self.get_prop(id, Atom::NetWmWindowType.as_ref()) {
347            atoms.iter().any(|a| float_types.contains(&a.as_ref()))
348        } else {
349            false
350        }
351    }
352}
353
354/// Modifying X client config and attributes
355#[stubbed_companion_trait(doc_hidden = "true")]
356pub trait XClientConfig {
357    /// Configure the on screen appearance of a client window
358    #[stub(Ok(()))]
359    fn configure_client(&self, id: Xid, data: &[ClientConfig]) -> Result<()>;
360
361    /// Set client attributes such as event masks, border color etc
362    #[stub(Ok(()))]
363    fn set_client_attributes(&self, id: Xid, data: &[ClientAttr]) -> Result<()>;
364
365    /// Get the [WindowAttributes] for this client
366    #[stub(Err(XError::Raw("mocked".into())))]
367    fn get_window_attributes(&self, id: Xid) -> Result<WindowAttributes>;
368
369    /*
370     *  The following default implementations should used if possible.
371     *
372     *  Any custom implementations should take care to ensure that the state changes being made are
373     *  equivaled to those implemented here.
374     */
375
376    /// Reposition the window identified by 'id' to the specifed region
377    fn position_client(&self, id: Xid, r: Region, border: u32, stack_above: bool) -> Result<()> {
378        let mut data = vec![ClientConfig::Position(r), ClientConfig::BorderPx(border)];
379        if stack_above {
380            data.push(ClientConfig::StackAbove);
381        }
382        self.configure_client(id, &data)
383    }
384
385    /// Raise the window to the top of the stack so it renders above peers
386    fn raise_client(&self, id: Xid) -> Result<()> {
387        self.configure_client(id, &[ClientConfig::StackAbove])
388    }
389
390    /// Change the border color for the given client
391    fn set_client_border_color(&self, id: Xid, color: Color) -> Result<()> {
392        self.set_client_attributes(id, &[ClientAttr::BorderColor(color.rgb_u32())])
393    }
394}
395
396/// Keyboard input for created clients
397#[stubbed_companion_trait(doc_hidden = "true")]
398pub trait XKeyboardHandler {
399    /// Attempt to grab control of all keyboard input
400    #[stub(Ok(()))]
401    fn grab_keyboard(&self) -> Result<()>;
402
403    /// Attempt to release control of all keyboard inputs
404    #[stub(Ok(()))]
405    fn ungrab_keyboard(&self) -> Result<()>;
406
407    /// Attempt to parse the next [XEvent] from an underlying connection as a [KeyPress] if there
408    /// is one.
409    ///
410    /// Should return Ok(None) if no events are currently available.
411    #[stub(Ok(None))]
412    fn next_keypress(&self) -> Result<Option<KeyPressParseAttempt>>;
413
414    /// Wait for the next [XEvent] from an underlying connection as a [KeyPress] and attempt to
415    /// parse it as a [KeyPress].
416    #[stub(Err(XError::Raw("mocked".into())))]
417    fn next_keypress_blocking(&self) -> Result<KeyPressParseAttempt>;
418}
419
420/// A handle on a running X11 connection that we can use for issuing X requests.
421///
422/// XConn is intended as an abstraction layer to allow for communication with the underlying
423/// display system (assumed to be X) using whatever mechanism the implementer wishes. In theory, it
424/// should be possible to write an implementation that allows penrose to run on systems not using X
425/// as the windowing system but X idioms and high level event types / client interations are
426/// assumed.
427#[stubbed_companion_trait(doc_hidden = "true")]
428pub trait XConn:
429    XState + XEventHandler + XClientHandler + XClientProperties + XClientConfig + Sized
430{
431    /// Hydrate this XConn to restore internal state following serde deserialization
432    #[cfg(feature = "serde")]
433    #[stub(Ok(()))]
434    fn hydrate(&mut self) -> Result<()>;
435
436    /// Initialise any state required before this connection can be used by the WindowManager.
437    ///
438    /// This must include checking to see if another window manager is running and return an error
439    /// if there is, but other than that there are no other requirements.
440    ///
441    /// This method is called once during [WindowManager::init][1]
442    ///
443    /// [1]: crate::core::manager::WindowManager::init
444    #[stub(Ok(()))]
445    fn init(&self) -> Result<()>;
446
447    /// An X id for a check window that will be used for holding EWMH window manager properties
448    ///
449    /// The creation of any resources required for this should be handled in `init` and the
450    /// destruction of those resources should be handled in `cleanup`.
451    #[stub(0)]
452    fn check_window(&self) -> Xid;
453
454    /// Perform any state cleanup required prior to shutting down the window manager
455    #[stub(Ok(()))]
456    fn cleanup(&self) -> Result<()>;
457
458    /// Notify the X server that we are intercepting the user specified key bindings and prevent
459    /// them being passed through to the underlying applications.
460    ///
461    /// This is what determines which key press events end up being sent through in the main event
462    /// loop for the WindowManager.
463    #[stub(Ok(()))]
464    fn grab_keys(
465        &self,
466        key_bindings: &KeyBindings<Self>,
467        mouse_bindings: &MouseBindings<Self>,
468    ) -> Result<()>;
469
470    /*
471     *  The following default implementations should used if possible.
472     *
473     *  Any custom implementations should take care to ensure that the state changes being made are
474     *  equivaled to those implemented here.
475     */
476
477    /// Mark the given client as newly created
478    fn mark_new_client(&self, id: Xid) -> Result<()> {
479        self.set_client_attributes(id, &[ClientAttr::ClientEventMask])
480    }
481
482    /// Set required EWMH properties to ensure compatability with external programs
483    fn set_wm_properties(&self, workspaces: &[String]) -> Result<()> {
484        let root = self.root();
485        let check_win = self.check_window();
486        for &win in &[check_win, root] {
487            self.change_prop(
488                win,
489                Atom::NetSupportingWmCheck.as_ref(),
490                Prop::Window(vec![check_win]),
491            )?;
492
493            self.change_prop(
494                win,
495                Atom::WmName.as_ref(),
496                Prop::UTF8String(vec![WM_NAME.into()]),
497            )?;
498        }
499
500        // EWMH support
501        self.change_prop(
502            root,
503            Atom::NetSupported.as_ref(),
504            Prop::Atom(
505                EWMH_SUPPORTED_ATOMS
506                    .iter()
507                    .map(|a| a.as_ref().to_string())
508                    .collect(),
509            ),
510        )?;
511        self.update_desktops(workspaces)?;
512        self.delete_prop(root, Atom::NetClientList.as_ref())?;
513        self.delete_prop(root, Atom::NetClientListStacking.as_ref())
514    }
515
516    /// Update the root window properties with the current desktop details
517    fn update_desktops(&self, workspaces: &[String]) -> Result<()> {
518        let root = self.root();
519        self.change_prop(
520            root,
521            Atom::NetNumberOfDesktops.as_ref(),
522            Prop::Cardinal(workspaces.len() as u32),
523        )?;
524        self.change_prop(
525            root,
526            Atom::NetDesktopNames.as_ref(),
527            Prop::UTF8String(workspaces.to_vec()),
528        )
529    }
530
531    /// Update the root window properties with the current client details
532    fn update_known_clients(&self, clients: &[Xid]) -> Result<()> {
533        let root = self.root();
534        self.change_prop(
535            root,
536            Atom::NetClientList.as_ref(),
537            Prop::Window(clients.to_vec()),
538        )?;
539        self.change_prop(
540            root,
541            Atom::NetClientListStacking.as_ref(),
542            Prop::Window(clients.to_vec()),
543        )
544    }
545
546    /// Update which desktop is currently focused
547    fn set_current_workspace(&self, wix: usize) -> Result<()> {
548        self.change_prop(
549            self.root(),
550            Atom::NetCurrentDesktop.as_ref(),
551            Prop::Cardinal(wix as u32),
552        )
553    }
554
555    /// Set the WM_NAME prop of the root window
556    fn set_root_window_name(&self, name: &str) -> Result<()> {
557        self.change_prop(
558            self.root(),
559            Atom::WmName.as_ref(),
560            Prop::UTF8String(vec![name.to_string()]),
561        )
562    }
563
564    /// Update which desktop a client is currently on
565    fn set_client_workspace(&self, id: Xid, wix: usize) -> Result<()> {
566        self.change_prop(id, Atom::NetWmDesktop.as_ref(), Prop::Cardinal(wix as u32))
567    }
568
569    /// Check to see if this client is one that we should be handling or not
570    #[tracing::instrument(level = "trace", skip(self))]
571    fn is_managed_client(&self, c: &Client) -> bool {
572        let unmanaged_types: Vec<String> = UNMANAGED_WINDOW_TYPES
573            .iter()
574            .map(|t| t.as_ref().to_string())
575            .collect();
576        trace!(ty = ?c.wm_type, "checking window type to see we should manage");
577        return c.wm_type.iter().all(|ty| !unmanaged_types.contains(ty));
578    }
579
580    /// The subset of active clients that are considered managed by penrose
581    fn active_managed_clients(&self, floating_classes: &[&str]) -> Result<Vec<Client>> {
582        Ok(self
583            .active_clients()?
584            .into_iter()
585            .filter_map(|id| {
586                let attrs_ok = self.get_window_attributes(id).map_or(true, |a| {
587                    !a.override_redirect
588                        && a.window_class == WindowClass::InputOutput
589                        && a.map_state == MapState::Viewable
590                });
591                if attrs_ok {
592                    trace!(id, "parsing existing client");
593                    let wix = match self.get_prop(id, Atom::NetWmDesktop.as_ref()) {
594                        Ok(Prop::Cardinal(wix)) => wix,
595                        _ => 0, // Drop unknown clients onto ws 0 as we know that is always there
596                    };
597
598                    let c = Client::new(self, id, wix as usize, floating_classes);
599                    if self.is_managed_client(&c) {
600                        return Some(c);
601                    }
602                }
603                None
604            })
605            .collect())
606    }
607}
608
609#[cfg(test)]
610pub use mock_conn::MockXConn;
611
612#[cfg(test)]
613mod mock_conn {
614    use super::*;
615    use std::{cell::Cell, fmt};
616
617    #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
618    pub struct MockXConn {
619        screens: Vec<Screen>,
620        #[cfg_attr(feature = "serde", serde(skip))]
621        events: Cell<Vec<XEvent>>,
622        focused: Cell<Xid>,
623        unmanaged_ids: Vec<Xid>,
624    }
625
626    impl fmt::Debug for MockXConn {
627        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628            f.debug_struct("MockXConn")
629                .field("screens", &self.screens)
630                .field("remaining_events", &self.remaining_events())
631                .field("focused", &self.focused.get())
632                .field("unmanaged_ids", &self.unmanaged_ids)
633                .finish()
634        }
635    }
636
637    impl MockXConn {
638        /// Set up a new [MockXConn] with pre-defined [Screen]s and an event stream to pull from
639        pub fn new(screens: Vec<Screen>, events: Vec<XEvent>, unmanaged_ids: Vec<Xid>) -> Self {
640            MockXConn {
641                screens,
642                events: Cell::new(events),
643                focused: Cell::new(0),
644                unmanaged_ids,
645            }
646        }
647
648        fn remaining_events(&self) -> Vec<XEvent> {
649            let remaining = self.events.replace(vec![]);
650            self.events.set(remaining.clone());
651            remaining
652        }
653    }
654
655    __impl_stub_xcon! {
656        for MockXConn;
657
658        atom_queries: {
659            fn mock_atom_id(&self, name: &str) -> Result<Xid> {
660                Ok(name.len() as u32)
661            }
662        }
663        client_properties: {
664            fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
665                if name == Atom::WmName.as_ref() || name == Atom::NetWmName.as_ref() {
666                    Ok(Prop::UTF8String(vec!["mock name".into()]))
667                } else {
668                    Err(XError::MissingProperty(name.into(), id))
669                }
670            }
671        }
672        client_handler: {
673            fn mock_focus_client(&self, id: Xid) -> Result<()> {
674                self.focused.replace(id);
675                Ok(())
676            }
677        }
678        client_config: {}
679        event_handler: {
680            fn mock_wait_for_event(&self) -> Result<XEvent> {
681                let mut remaining = self.events.replace(vec![]);
682                if remaining.is_empty() {
683                    return Err(XError::ConnectionClosed)
684                }
685                let next = remaining.remove(0);
686                self.events.set(remaining);
687                Ok(next)
688            }
689
690            fn mock_send_client_event(&self, _: ClientMessage) -> Result<()> {
691                Ok(())
692            }
693        }
694        state: {
695            fn mock_current_screens(&self) -> Result<Vec<Screen>> {
696                Ok(self.screens.clone())
697            }
698
699            fn mock_focused_client(&self) -> Result<Xid> {
700                Ok(self.focused.get())
701            }
702        }
703        conn: {
704            fn mock_is_managed_client(&self, c: &Client) -> bool {
705                !self.unmanaged_ids.contains(&c.id())
706            }
707        }
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714
715    use std::str::FromStr;
716
717    struct WmNameXConn {
718        wm_name: bool,
719        net_wm_name: bool,
720        empty_net_wm_name: bool,
721    }
722
723    impl StubXClientProperties for WmNameXConn {
724        fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
725            match Atom::from_str(name)? {
726                Atom::WmName if self.wm_name => Ok(Prop::UTF8String(vec!["wm_name".into()])),
727                Atom::WmName if self.net_wm_name && self.empty_net_wm_name => {
728                    Ok(Prop::UTF8String(vec!["".into()]))
729                }
730                Atom::NetWmName if self.net_wm_name => {
731                    Ok(Prop::UTF8String(vec!["net_wm_name".into()]))
732                }
733                Atom::NetWmName if self.empty_net_wm_name => Ok(Prop::UTF8String(vec!["".into()])),
734                _ => Err(XError::MissingProperty(name.into(), id)),
735            }
736        }
737    }
738
739    test_cases! {
740        window_name;
741        args: (wm_name: bool, net_wm_name: bool, empty_net_wm_name: bool, expected: &str);
742
743        case: wm_name_only => (true, false, false, "wm_name");
744        case: net_wm_name_only => (false, true, false, "net_wm_name");
745        case: both_prefers_net => (true, true, false, "net_wm_name");
746        case: net_wm_name_empty => (true, false, true, "wm_name");
747
748        body: {
749            let conn = WmNameXConn {
750                wm_name,
751                net_wm_name,
752                empty_net_wm_name,
753            };
754            assert_eq!(&conn.client_name(42).unwrap(), expected);
755        }
756    }
757}