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 delete:false,
61 window,
62 property:AtomEnum::WM_CLASS,
63 type_:AtomEnum::STRING,
64 long_offset:0,
65 long_length: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 delete:false,
182 window,
183 property,
184 type_:AtomEnum::WM_SIZE_HINTS,
185 long_offset: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: i32, denominator: i32), remaining: &[u8]) = TryParse::try_parse(value)?;
225 let result: AspectRatio = 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: u8, b: u8, c: u8, d: u8] = self.numerator.serialize();
235 let [e: u8, f: u8, g: u8, h: u8] = 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 delete:false,
466 window,
467 property:AtomEnum::WM_HINTS,
468 type_:AtomEnum::WM_HINTS,
469 long_offset: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: T, remaining: &[u8]) = 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}
824