x11rb/
properties.rs

1//! Utility functions for working with X11 properties
2
3use crate::connection::RequestConnection;
4use crate::cookie::{Cookie, VoidCookie};
5use crate::errors::{ConnectionError, ParseError, ReplyError};
6use crate::protocol::xproto::{self, Atom, AtomEnum, GetPropertyReply, Window};
7use crate::x11_utils::{Serialize, TryParse};
8
9macro_rules! property_cookie {
10    {
11        $(#[$meta:meta])*
12        pub struct $cookie_name:ident: $struct_name:ident,
13        $from_reply:expr,
14    } => {
15        $(#[$meta])*
16        #[derive(Debug)]
17        pub struct $cookie_name<'a, Conn: RequestConnection + ?Sized>(Cookie<'a, Conn, GetPropertyReply>);
18
19        impl<'a, Conn> $cookie_name<'a, Conn>
20        where
21            Conn: RequestConnection + ?Sized,
22        {
23            /// Get the reply that the server sent.
24            pub fn reply(self) -> Result<Option<$struct_name>, ReplyError> {
25                #[allow(clippy::redundant_closure_call)]
26                Ok($from_reply(self.0.reply()?)?)
27            }
28
29            /// Get the reply that the server sent, but have errors handled as events.
30            pub fn reply_unchecked(self) -> Result<Option<$struct_name>, ConnectionError> {
31                self.0
32                    .reply_unchecked()?
33                    .map($from_reply)
34                    .transpose()
35                    .map(|e| e.flatten())
36                    .map_err(Into::into)
37            }
38        }
39    }
40}
41
42// WM_CLASS
43
44property_cookie! {
45    /// A cookie for getting a window's `WM_CLASS` property.
46    ///
47    /// See `WmClass`.
48    pub struct WmClassCookie: WmClass,
49    WmClass::from_reply,
50}
51
52impl<'a, Conn> WmClassCookie<'a, Conn>
53where
54    Conn: RequestConnection + ?Sized,
55{
56    /// Send a `GetProperty` request for the `WM_CLASS` property of the given window
57    pub fn new(conn: &'a Conn, window: Window) -> Result<Self, ConnectionError> {
58        Ok(Self(xproto::get_property(
59            conn,
60            false,
61            window,
62            AtomEnum::WM_CLASS,
63            AtomEnum::STRING,
64            0,
65            2048,
66        )?))
67    }
68}
69
70/// The value of a window's `WM_CLASS` property.
71///
72/// Usage example:
73/// ```
74/// use x11rb::connection::Connection;
75/// use x11rb::errors::ConnectionError;
76/// use x11rb::properties::WmClass;
77/// use x11rb::protocol::xproto::Window;
78///
79/// fn print_class_instance(
80///     conn: &impl Connection,
81///     window: Window,
82/// ) -> Result<bool, ConnectionError> {
83///     let wm_class = match WmClass::get(conn, window)?.reply_unchecked()? {
84///         Some(wm_class) => wm_class,
85///         None => return Ok(false), // Getting the property failed
86///     };
87///     // Note that the WM_CLASS property is not actually encoded in utf8.
88///     // ASCII values are most common and for these from_utf8() should be fine.
89///     let class = std::str::from_utf8(wm_class.class());
90///     let instance = std::str::from_utf8(wm_class.instance());
91///     println!(
92///         "For window {:x}, class is '{:?}' and instance is '{:?}'",
93///         window, class, instance,
94///     );
95///     Ok(true)
96/// }
97/// ```
98#[derive(Debug)]
99pub struct WmClass(GetPropertyReply, usize);
100
101impl WmClass {
102    /// Send a `GetProperty` request for the `WM_CLASS` property of the given window
103    pub fn get<C: RequestConnection>(
104        conn: &C,
105        window: Window,
106    ) -> Result<WmClassCookie<'_, C>, ConnectionError> {
107        WmClassCookie::new(conn, window)
108    }
109
110    /// Construct a new `WmClass` instance from a `GetPropertyReply`.
111    ///
112    /// The original `GetProperty` request must have been for a `WM_CLASS` property for this
113    /// function to return sensible results.
114    pub fn from_reply(reply: GetPropertyReply) -> Result<Option<Self>, ParseError> {
115        if reply.type_ == AtomEnum::NONE.into() {
116            return Ok(None);
117        }
118        if reply.type_ != AtomEnum::STRING.into() || reply.format != 8 {
119            return Err(ParseError::InvalidValue);
120        }
121        // Find the first zero byte in the value
122        let offset = reply
123            .value
124            .iter()
125            .position(|&v| v == 0)
126            .unwrap_or(reply.value.len());
127        Ok(Some(WmClass(reply, offset)))
128    }
129
130    /// Get the instance contained in this `WM_CLASS` property
131    pub fn instance(&self) -> &[u8] {
132        &self.0.value[0..self.1]
133    }
134
135    /// Get the class contained in this `WM_CLASS` property
136    pub fn class(&self) -> &[u8] {
137        let start = self.1 + 1;
138        if start >= self.0.value.len() {
139            return &[];
140        };
141        let end = if self.0.value.last() == Some(&0) {
142            self.0.value.len() - 1
143        } else {
144            self.0.value.len()
145        };
146        &self.0.value[start..end]
147    }
148}
149
150// WM_SIZE_HINTS
151
152/// Representation of whether some part of `WM_SIZE_HINTS` was user/program specified.
153#[derive(Debug, Copy, Clone)]
154pub enum WmSizeHintsSpecification {
155    /// The user specified the values.
156    UserSpecified,
157    /// The program specified the values.
158    ProgramSpecified,
159}
160
161property_cookie! {
162    /// A cookie for getting a window's `WM_SIZE_HINTS` property.
163    pub struct WmSizeHintsCookie: WmSizeHints,
164    |reply| WmSizeHints::from_reply(&reply),
165}
166
167const NUM_WM_SIZE_HINTS_ELEMENTS: u16 = 18;
168
169impl<'a, Conn> WmSizeHintsCookie<'a, Conn>
170where
171    Conn: RequestConnection + ?Sized,
172{
173    /// Send a `GetProperty` request for the given property of the given window
174    pub fn new(
175        conn: &'a Conn,
176        window: Window,
177        property: impl Into<Atom>,
178    ) -> Result<Self, ConnectionError> {
179        Ok(Self(xproto::get_property(
180            conn,
181            false,
182            window,
183            property,
184            AtomEnum::WM_SIZE_HINTS,
185            0,
186            NUM_WM_SIZE_HINTS_ELEMENTS.into(),
187        )?))
188    }
189}
190
191// Possible flags for `WM_SIZE_HINTS`.
192const U_S_POSITION: u32 = 1;
193const U_S_SIZE: u32 = 1 << 1;
194const P_S_POSITION: u32 = 1 << 2;
195const P_S_SIZE: u32 = 1 << 3;
196const P_MIN_SIZE: u32 = 1 << 4;
197const P_MAX_SIZE: u32 = 1 << 5;
198const P_RESIZE_INCREMENT: u32 = 1 << 6;
199const P_ASPECT: u32 = 1 << 7;
200const P_BASE_SIZE: u32 = 1 << 8;
201const P_WIN_GRAVITY: u32 = 1 << 9;
202
203/// An aspect ratio `numerator` / `denominator`.
204#[derive(Debug, Copy, Clone)]
205pub struct AspectRatio {
206    /// The numerator of the aspect ratio.
207    pub numerator: i32,
208    /// The denominator of the aspect ratio.
209    pub denominator: i32,
210}
211
212impl AspectRatio {
213    /// Create a new aspect ratio with the given values.
214    pub fn new(numerator: i32, denominator: i32) -> Self {
215        Self {
216            numerator,
217            denominator,
218        }
219    }
220}
221
222impl TryParse for AspectRatio {
223    fn try_parse(value: &[u8]) -> Result<(Self, &[u8]), ParseError> {
224        let ((numerator, denominator), remaining) = TryParse::try_parse(value)?;
225        let result = AspectRatio::new(numerator, denominator);
226        Ok((result, remaining))
227    }
228}
229
230#[allow(clippy::many_single_char_names)]
231impl Serialize for AspectRatio {
232    type Bytes = [u8; 8];
233    fn serialize(&self) -> Self::Bytes {
234        let [a, b, c, d] = self.numerator.serialize();
235        let [e, f, g, h] = self.denominator.serialize();
236        [a, b, c, d, e, f, g, h]
237    }
238    fn serialize_into(&self, bytes: &mut Vec<u8>) {
239        (self.numerator, self.denominator).serialize_into(bytes);
240    }
241}
242
243/// A structure representing a `WM_SIZE_HINTS` property.
244#[derive(Debug, Default, Copy, Clone)]
245pub struct WmSizeHints {
246    /// The position that the window should be assigned.
247    ///
248    /// Note that current versions of ICCCM only make use of the `WmSizeHintsSpecification` field.
249    /// The later two fields exist only for backwards compatibility.
250    pub position: Option<(WmSizeHintsSpecification, i32, i32)>,
251    /// The size that the window should be assigned.
252    ///
253    /// Note that current versions of ICCCM only make use of the `WmSizeHintsSpecification` field.
254    /// The later two fields exist only for backwards compatibility.
255    pub size: Option<(WmSizeHintsSpecification, i32, i32)>,
256    /// The minimum size that the window may be assigned.
257    pub min_size: Option<(i32, i32)>,
258    /// The maximum size that the window may be assigned.
259    pub max_size: Option<(i32, i32)>,
260    /// The increment to be used for sizing the window together with `base_size`.
261    pub size_increment: Option<(i32, i32)>,
262    /// The minimum and maximum aspect ratio.
263    pub aspect: Option<(AspectRatio, AspectRatio)>,
264    /// The base size of the window.
265    ///
266    /// This is used together with `size_increment`.
267    pub base_size: Option<(i32, i32)>,
268    /// The gravity that is used to make room for window decorations.
269    pub win_gravity: Option<xproto::Gravity>,
270}
271
272impl WmSizeHints {
273    /// Get a new, empty `WmSizeHints` structure.
274    pub fn new() -> Self {
275        Default::default()
276    }
277
278    /// Send a `GetProperty` request for the given property of the given window
279    pub fn get<C: RequestConnection>(
280        conn: &C,
281        window: Window,
282        property: impl Into<Atom>,
283    ) -> Result<WmSizeHintsCookie<'_, C>, ConnectionError> {
284        WmSizeHintsCookie::new(conn, window, property)
285    }
286
287    /// Send a `GetProperty` request for the `WM_NORMAL_HINTS` property of the given window
288    pub fn get_normal_hints<C: RequestConnection>(
289        conn: &C,
290        window: Window,
291    ) -> Result<WmSizeHintsCookie<'_, C>, ConnectionError> {
292        Self::get(conn, window, AtomEnum::WM_NORMAL_HINTS)
293    }
294
295    /// Construct a new `WmSizeHints` instance from a `GetPropertyReply`.
296    ///
297    /// The original `WmSizeHints` request must have been for a `WM_SIZE_HINTS` property for this
298    /// function to return sensible results.
299    pub fn from_reply(reply: &GetPropertyReply) -> Result<Option<Self>, ParseError> {
300        if reply.type_ == AtomEnum::NONE.into() {
301            return Ok(None);
302        }
303        if reply.type_ != AtomEnum::WM_SIZE_HINTS.into() || reply.format != 32 {
304            return Err(ParseError::InvalidValue);
305        }
306        Ok(Some(Self::try_parse(&reply.value)?.0))
307    }
308
309    /// Set these `WM_SIZE_HINTS` on some window as the `WM_NORMAL_HINTS` property.
310    pub fn set_normal_hints<'a, C: RequestConnection + ?Sized>(
311        &self,
312        conn: &'a C,
313        window: Window,
314    ) -> Result<VoidCookie<'a, C>, ConnectionError> {
315        self.set(conn, window, AtomEnum::WM_NORMAL_HINTS)
316    }
317
318    /// Set these `WM_SIZE_HINTS` on some window as the given property.
319    pub fn set<'a, C: RequestConnection + ?Sized>(
320        &self,
321        conn: &'a C,
322        window: Window,
323        property: impl Into<Atom>,
324    ) -> Result<VoidCookie<'a, C>, ConnectionError> {
325        let data = self.serialize();
326        xproto::change_property(
327            conn,
328            xproto::PropMode::REPLACE,
329            window,
330            property.into(),
331            AtomEnum::WM_SIZE_HINTS,
332            32,
333            NUM_WM_SIZE_HINTS_ELEMENTS.into(),
334            &data,
335        )
336    }
337}
338
339impl TryParse for WmSizeHints {
340    fn try_parse(remaining: &[u8]) -> Result<(Self, &[u8]), ParseError> {
341        // Implemented based on what xcb_icccm does. At least a bit. This stuff makes no sense...
342
343        let (flags, remaining) = u32::try_parse(remaining)?;
344        let (x, remaining) = i32::try_parse(remaining)?;
345        let (y, remaining) = i32::try_parse(remaining)?;
346        let (width, remaining) = i32::try_parse(remaining)?;
347        let (height, remaining) = i32::try_parse(remaining)?;
348        let (min_size, remaining) = parse_with_flag::<(i32, i32)>(remaining, flags, P_MIN_SIZE)?;
349        let (max_size, remaining) = parse_with_flag::<(i32, i32)>(remaining, flags, P_MAX_SIZE)?;
350        let (size_increment, remaining) =
351            parse_with_flag::<(i32, i32)>(remaining, flags, P_RESIZE_INCREMENT)?;
352        let (aspect, remaining) =
353            parse_with_flag::<(AspectRatio, AspectRatio)>(remaining, flags, P_ASPECT)?;
354        // Apparently, some older version of ICCCM didn't have these...?
355        let (base_size, win_gravity, remaining) = if remaining.is_empty() {
356            (min_size, Some(xproto::Gravity::NORTH_WEST), remaining)
357        } else {
358            let (base_size, remaining) =
359                parse_with_flag::<(i32, i32)>(remaining, flags, P_BASE_SIZE)?;
360            let (win_gravity, remaining) = parse_with_flag::<u32>(remaining, flags, P_WIN_GRAVITY)?;
361            (base_size, win_gravity.map(Into::into), remaining)
362        };
363
364        let position = if flags & U_S_POSITION != 0 {
365            Some((WmSizeHintsSpecification::UserSpecified, x, y))
366        } else if flags & P_S_POSITION != 0 {
367            Some((WmSizeHintsSpecification::ProgramSpecified, x, y))
368        } else {
369            None
370        };
371        let size = if flags & U_S_SIZE != 0 {
372            Some((WmSizeHintsSpecification::UserSpecified, width, height))
373        } else if flags & P_S_SIZE != 0 {
374            Some((WmSizeHintsSpecification::ProgramSpecified, width, height))
375        } else {
376            None
377        };
378
379        Ok((
380            WmSizeHints {
381                position,
382                size,
383                min_size,
384                max_size,
385                size_increment,
386                aspect,
387                base_size,
388                win_gravity,
389            },
390            remaining,
391        ))
392    }
393}
394
395impl Serialize for WmSizeHints {
396    type Bytes = Vec<u8>;
397    fn serialize(&self) -> Self::Bytes {
398        let mut result = Vec::with_capacity((NUM_WM_SIZE_HINTS_ELEMENTS * 4).into());
399        self.serialize_into(&mut result);
400        result
401    }
402    fn serialize_into(&self, bytes: &mut Vec<u8>) {
403        let mut flags = 0;
404        match self.position {
405            Some((WmSizeHintsSpecification::UserSpecified, _, _)) => flags |= U_S_POSITION,
406            Some((WmSizeHintsSpecification::ProgramSpecified, _, _)) => flags |= P_S_POSITION,
407            None => {}
408        }
409        match self.size {
410            Some((WmSizeHintsSpecification::UserSpecified, _, _)) => flags |= U_S_SIZE,
411            Some((WmSizeHintsSpecification::ProgramSpecified, _, _)) => flags |= P_S_SIZE,
412            None => {}
413        }
414        flags |= self.min_size.map_or(0, |_| P_MIN_SIZE);
415        flags |= self.max_size.map_or(0, |_| P_MAX_SIZE);
416        flags |= self.size_increment.map_or(0, |_| P_RESIZE_INCREMENT);
417        flags |= self.aspect.map_or(0, |_| P_ASPECT);
418        flags |= self.base_size.map_or(0, |_| P_BASE_SIZE);
419        flags |= self.win_gravity.map_or(0, |_| P_WIN_GRAVITY);
420        flags.serialize_into(bytes);
421
422        match self.position {
423            Some((_, x, y)) => (x, y),
424            None => (0, 0),
425        }
426        .serialize_into(bytes);
427
428        match self.size {
429            Some((_, width, height)) => (width, height),
430            None => (0, 0),
431        }
432        .serialize_into(bytes);
433
434        self.min_size.unwrap_or((0, 0)).serialize_into(bytes);
435        self.max_size.unwrap_or((0, 0)).serialize_into(bytes);
436        self.size_increment.unwrap_or((0, 0)).serialize_into(bytes);
437        self.aspect
438            .unwrap_or((AspectRatio::new(0, 0), AspectRatio::new(0, 0)))
439            .serialize_into(bytes);
440        self.base_size.unwrap_or((0, 0)).serialize_into(bytes);
441        self.win_gravity.map_or(0, u32::from).serialize_into(bytes);
442    }
443}
444
445// WM_HINTS
446//
447property_cookie! {
448    /// A cookie for getting a window's `WM_HINTS` property.
449    ///
450    /// See `WmHints`.
451    pub struct WmHintsCookie: WmHints,
452    |reply| WmHints::from_reply(&reply),
453}
454
455const NUM_WM_HINTS_ELEMENTS: u32 = 9;
456
457impl<'a, Conn> WmHintsCookie<'a, Conn>
458where
459    Conn: RequestConnection + ?Sized,
460{
461    /// Send a `GetProperty` request for the `WM_CLASS` property of the given window
462    pub fn new(conn: &'a Conn, window: Window) -> Result<Self, ConnectionError> {
463        Ok(Self(xproto::get_property(
464            conn,
465            false,
466            window,
467            AtomEnum::WM_HINTS,
468            AtomEnum::WM_HINTS,
469            0,
470            NUM_WM_HINTS_ELEMENTS,
471        )?))
472    }
473}
474
475/// The possible values for a `WM_STATE`'s state field.
476#[derive(Debug, Copy, Clone)]
477pub enum WmHintsState {
478    /// The window should be in Normal state.
479    Normal,
480    /// The window should be in Iconic state.
481    Iconic,
482}
483
484// Possible flags for `WM_HINTS`.
485const HINT_INPUT: u32 = 1;
486const HINT_STATE: u32 = 1 << 1;
487const HINT_ICON_PIXMAP: u32 = 1 << 2;
488const HINT_ICON_WINDOW: u32 = 1 << 3;
489const HINT_ICON_POSITION: u32 = 1 << 4;
490const HINT_ICON_MASK: u32 = 1 << 5;
491const HINT_WINDOW_GROUP: u32 = 1 << 6;
492// This bit is obsolete, according to ICCCM
493//const HINT_MESSAGE: u32 = 1 << 7;
494const HINT_URGENCY: u32 = 1 << 8;
495
496/// A structure representing a `WM_HINTS` property.
497#[derive(Debug, Default, Copy, Clone)]
498pub struct WmHints {
499    /// Whether the window manager may set the input focus to this window.
500    ///
501    /// See ICCCM §4.1.7 for details.
502    pub input: Option<bool>,
503
504    /// The state that the window should be when it leaves the Withdrawn state.
505    pub initial_state: Option<WmHintsState>,
506
507    /// A pixmap that represents the icon of this window.
508    pub icon_pixmap: Option<xproto::Pixmap>,
509
510    /// A window that should be used as icon.
511    pub icon_window: Option<Window>,
512
513    /// The position where the icon should be shown.
514    pub icon_position: Option<(i32, i32)>,
515
516    /// A mask for `icon_pixmap`.
517    ///
518    /// This allows nonrectangular icons.
519    pub icon_mask: Option<xproto::Pixmap>,
520
521    /// A window that represents a group of windows.
522    ///
523    /// The specified window is called the "group leader". All windows with the same group leader
524    /// are part of the same group.
525    pub window_group: Option<Window>,
526
527    /// Indication that the window contents are urgent.
528    ///
529    /// Urgency means that a timely response of the user is required. The window manager must make
530    /// some effort to draw the user's attention to this window while this flag is set.
531    pub urgent: bool,
532}
533
534impl WmHints {
535    /// Get a new, empty `WmSizeHints` structure.
536    pub fn new() -> Self {
537        Default::default()
538    }
539
540    /// Send a `GetProperty` request for the `WM_HINTS` property of the given window
541    pub fn get<C: RequestConnection>(
542        conn: &C,
543        window: Window,
544    ) -> Result<WmHintsCookie<'_, C>, ConnectionError> {
545        WmHintsCookie::new(conn, window)
546    }
547
548    /// Construct a new `WmHints` instance from a `GetPropertyReply`.
549    ///
550    /// The original `WmHints` request must have been for a `WM_HINTS` property for this
551    /// function to return sensible results.
552    pub fn from_reply(reply: &GetPropertyReply) -> Result<Option<Self>, ParseError> {
553        if reply.type_ == AtomEnum::NONE.into() {
554            return Ok(None);
555        }
556        if reply.type_ != AtomEnum::WM_HINTS.into() || reply.format != 32 {
557            return Err(ParseError::InvalidValue);
558        }
559        Ok(Some(Self::try_parse(&reply.value)?.0))
560    }
561
562    /// Set these `WM_HINTS` on some window.
563    pub fn set<'a, C: RequestConnection + ?Sized>(
564        &self,
565        conn: &'a C,
566        window: Window,
567    ) -> Result<VoidCookie<'a, C>, ConnectionError> {
568        let data = self.serialize();
569        xproto::change_property(
570            conn,
571            xproto::PropMode::REPLACE,
572            window,
573            AtomEnum::WM_HINTS,
574            AtomEnum::WM_HINTS,
575            32,
576            NUM_WM_HINTS_ELEMENTS,
577            &data,
578        )
579    }
580}
581
582impl TryParse for WmHints {
583    fn try_parse(remaining: &[u8]) -> Result<(Self, &[u8]), ParseError> {
584        let (flags, remaining) = u32::try_parse(remaining)?;
585        let (input, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_INPUT)?;
586        let (initial_state, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_STATE)?;
587        let (icon_pixmap, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_PIXMAP)?;
588        let (icon_window, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_WINDOW)?;
589        let (icon_position, remaining) =
590            parse_with_flag::<(i32, i32)>(remaining, flags, HINT_ICON_POSITION)?;
591        let (icon_mask, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_MASK)?;
592        // Apparently, some older version of ICCCM didn't have this...?
593        let (window_group, remaining) = if remaining.is_empty() {
594            (None, remaining)
595        } else {
596            let (window_group, remaining) =
597                parse_with_flag::<u32>(remaining, flags, HINT_WINDOW_GROUP)?;
598            (window_group, remaining)
599        };
600
601        let input = input.map(|input| input != 0);
602
603        let initial_state = match initial_state {
604            None => None,
605            Some(1) => Some(WmHintsState::Normal),
606            Some(3) => Some(WmHintsState::Iconic),
607            _ => return Err(ParseError::InvalidValue),
608        };
609
610        let urgent = flags & HINT_URGENCY != 0;
611
612        Ok((
613            WmHints {
614                input,
615                initial_state,
616                icon_pixmap,
617                icon_window,
618                icon_position,
619                icon_mask,
620                window_group,
621                urgent,
622            },
623            remaining,
624        ))
625    }
626}
627
628impl Serialize for WmHints {
629    type Bytes = Vec<u8>;
630    fn serialize(&self) -> Self::Bytes {
631        // 9*4 surely fits into an usize, so this unwrap() cannot trigger
632        let mut result = Vec::with_capacity((NUM_WM_HINTS_ELEMENTS * 4).try_into().unwrap());
633        self.serialize_into(&mut result);
634        result
635    }
636    fn serialize_into(&self, bytes: &mut Vec<u8>) {
637        let mut flags = 0;
638        flags |= self.input.map_or(0, |_| HINT_INPUT);
639        flags |= self.initial_state.map_or(0, |_| HINT_STATE);
640        flags |= self.icon_pixmap.map_or(0, |_| HINT_ICON_PIXMAP);
641        flags |= self.icon_window.map_or(0, |_| HINT_ICON_WINDOW);
642        flags |= self.icon_position.map_or(0, |_| HINT_ICON_POSITION);
643        flags |= self.icon_mask.map_or(0, |_| HINT_ICON_MASK);
644        flags |= self.window_group.map_or(0, |_| HINT_WINDOW_GROUP);
645        if self.urgent {
646            flags |= HINT_URGENCY;
647        }
648
649        flags.serialize_into(bytes);
650        u32::from(self.input.unwrap_or(false)).serialize_into(bytes);
651        match self.initial_state {
652            Some(WmHintsState::Normal) => 1,
653            Some(WmHintsState::Iconic) => 3,
654            None => 0,
655        }
656        .serialize_into(bytes);
657        self.icon_pixmap.unwrap_or(0).serialize_into(bytes);
658        self.icon_window.unwrap_or(0).serialize_into(bytes);
659        self.icon_position.unwrap_or((0, 0)).serialize_into(bytes);
660        self.icon_mask.unwrap_or(0).serialize_into(bytes);
661        self.window_group.unwrap_or(0).serialize_into(bytes);
662    }
663}
664
665/// Parse an element of type `T` and turn it into an `Option` by checking if the given `bit` is set
666/// in `flags`.
667fn parse_with_flag<T: TryParse>(
668    remaining: &[u8],
669    flags: u32,
670    bit: u32,
671) -> Result<(Option<T>, &[u8]), ParseError> {
672    let (value, remaining) = T::try_parse(remaining)?;
673    if flags & bit != 0 {
674        Ok((Some(value), remaining))
675    } else {
676        Ok((None, remaining))
677    }
678}
679
680#[cfg(test)]
681mod test {
682    use super::{WmClass, WmHints, WmHintsState, WmSizeHints};
683    use crate::protocol::xproto::{Atom, AtomEnum, GetPropertyReply, Gravity};
684    use crate::x11_utils::Serialize;
685
686    fn get_property_reply(value: &[u8], format: u8, type_: impl Into<Atom>) -> GetPropertyReply {
687        GetPropertyReply {
688            format,
689            sequence: 0,
690            length: 0,
691            type_: type_.into(),
692            bytes_after: 0,
693            value_len: value.len().try_into().unwrap(),
694            value: value.to_vec(),
695        }
696    }
697
698    #[test]
699    fn test_wm_class() {
700        for (input, instance, class) in &[
701            (&b""[..], &b""[..], &b""[..]),
702            (b"\0", b"", b""),
703            (b"\0\0", b"", b""),
704            (b"\0\0\0", b"", b"\0"),
705            (b"Hello World", b"Hello World", b""),
706            (b"Hello World\0", b"Hello World", b""),
707            (b"Hello\0World\0", b"Hello", b"World"),
708            (b"Hello\0World", b"Hello", b"World"),
709            (b"Hello\0World\0Good\0Day", b"Hello", b"World\0Good\0Day"),
710        ] {
711            let wm_class = WmClass::from_reply(get_property_reply(input, 8, AtomEnum::STRING))
712                .unwrap()
713                .unwrap();
714            assert_eq!((wm_class.instance(), wm_class.class()), (*instance, *class));
715        }
716    }
717
718    #[test]
719    fn test_wm_class_missing() {
720        let wm_class = WmClass::from_reply(get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
721        assert!(wm_class.is_none());
722    }
723
724    #[test]
725    fn test_wm_normal_hints() {
726        // This is the value of some random xterm window.
727        // It was acquired via 'xtrace xprop WM_NORMAL_HINTS'.
728        let input = [
729            0x0000_0350,
730            0x0000_0000,
731            0x0000_0000,
732            0x0000_0000,
733            0x0000_0000,
734            0x0000_0015,
735            0x0000_0017,
736            0x0000_0000,
737            0x0000_0000,
738            0x0000_000a,
739            0x0000_0013,
740            0x0000_0000,
741            0x0000_0000,
742            0x0000_0000,
743            0x0000_0000,
744            0x0000_000b,
745            0x0000_0004,
746            0x0000_0001,
747        ];
748        let input = input
749            .iter()
750            .flat_map(|v| u32::serialize(v).to_vec())
751            .collect::<Vec<u8>>();
752        let wm_size_hints =
753            WmSizeHints::from_reply(&get_property_reply(&input, 32, AtomEnum::WM_SIZE_HINTS))
754                .unwrap()
755                .unwrap();
756
757        assert!(
758            wm_size_hints.position.is_none(),
759            "{:?}",
760            wm_size_hints.position,
761        );
762        assert!(wm_size_hints.size.is_none(), "{:?}", wm_size_hints.size);
763        assert_eq!(wm_size_hints.min_size, Some((21, 23)));
764        assert_eq!(wm_size_hints.max_size, None);
765        assert_eq!(wm_size_hints.size_increment, Some((10, 19)));
766        assert!(wm_size_hints.aspect.is_none(), "{:?}", wm_size_hints.aspect);
767        assert_eq!(wm_size_hints.base_size, Some((11, 4)));
768        assert_eq!(wm_size_hints.win_gravity, Some(Gravity::NORTH_WEST));
769
770        assert_eq!(input, wm_size_hints.serialize());
771    }
772
773    #[test]
774    fn test_wm_normal_hints_missing() {
775        let wm_size_hints =
776            WmSizeHints::from_reply(&get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
777        assert!(wm_size_hints.is_none());
778    }
779
780    #[test]
781    fn test_wm_hints() {
782        // This is the value of some random xterm window.
783        // It was acquired via 'xtrace xprop WM_HINTS'.
784        let input = [
785            0x0000_0043,
786            0x0000_0001,
787            0x0000_0001,
788            0x0000_0000,
789            0x0000_0000,
790            0x0000_0000,
791            0x0000_0000,
792            0x0000_0000,
793            0x0060_0009,
794        ];
795        let input = input
796            .iter()
797            .flat_map(|v| u32::serialize(v).to_vec())
798            .collect::<Vec<u8>>();
799        let wm_hints = WmHints::from_reply(&get_property_reply(&input, 32, AtomEnum::WM_HINTS))
800            .unwrap()
801            .unwrap();
802
803        assert_eq!(wm_hints.input, Some(true));
804        match wm_hints.initial_state {
805            Some(WmHintsState::Normal) => {}
806            value => panic!("Expected Some(Normal), but got {:?}", value),
807        }
808        assert_eq!(wm_hints.icon_pixmap, None);
809        assert_eq!(wm_hints.icon_window, None);
810        assert_eq!(wm_hints.icon_position, None);
811        assert_eq!(wm_hints.icon_mask, None);
812        assert_eq!(wm_hints.window_group, Some(0x0060_0009));
813        assert!(!wm_hints.urgent);
814
815        assert_eq!(input, wm_hints.serialize());
816    }
817
818    #[test]
819    fn test_wm_hints_missing() {
820        let wm_hints = WmHints::from_reply(&get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
821        assert!(wm_hints.is_none());
822    }
823}