1 | //! Utility functions for working with X11 properties |
2 | |
3 | use crate::connection::RequestConnection; |
4 | use crate::cookie::{Cookie, VoidCookie}; |
5 | use crate::errors::{ConnectionError, ParseError, ReplyError}; |
6 | use crate::protocol::xproto::{self, Atom, AtomEnum, GetPropertyReply, Window}; |
7 | use crate::x11_utils::{Serialize, TryParse}; |
8 | |
9 | macro_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 | |
44 | property_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 | |
52 | impl<'a, Conn> WmClassCookie<'a, Conn> |
53 | where |
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)] |
99 | pub struct WmClass(GetPropertyReply, usize); |
100 | |
101 | impl 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)] |
154 | pub enum WmSizeHintsSpecification { |
155 | /// The user specified the values. |
156 | UserSpecified, |
157 | /// The program specified the values. |
158 | ProgramSpecified, |
159 | } |
160 | |
161 | property_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 | |
167 | const NUM_WM_SIZE_HINTS_ELEMENTS: u16 = 18; |
168 | |
169 | impl<'a, Conn> WmSizeHintsCookie<'a, Conn> |
170 | where |
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`. |
192 | const U_S_POSITION: u32 = 1; |
193 | const U_S_SIZE: u32 = 1 << 1; |
194 | const P_S_POSITION: u32 = 1 << 2; |
195 | const P_S_SIZE: u32 = 1 << 3; |
196 | const P_MIN_SIZE: u32 = 1 << 4; |
197 | const P_MAX_SIZE: u32 = 1 << 5; |
198 | const P_RESIZE_INCREMENT: u32 = 1 << 6; |
199 | const P_ASPECT: u32 = 1 << 7; |
200 | const P_BASE_SIZE: u32 = 1 << 8; |
201 | const P_WIN_GRAVITY: u32 = 1 << 9; |
202 | |
203 | /// An aspect ratio `numerator` / `denominator`. |
204 | #[derive (Debug, Copy, Clone)] |
205 | pub 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 | |
212 | impl 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 | |
222 | impl 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)] |
231 | impl 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)] |
245 | pub 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 | |
272 | impl 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 | |
339 | impl 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 | |
395 | impl 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 | // |
447 | property_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 | |
455 | const NUM_WM_HINTS_ELEMENTS: u32 = 9; |
456 | |
457 | impl<'a, Conn> WmHintsCookie<'a, Conn> |
458 | where |
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)] |
477 | pub 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`. |
485 | const HINT_INPUT: u32 = 1; |
486 | const HINT_STATE: u32 = 1 << 1; |
487 | const HINT_ICON_PIXMAP: u32 = 1 << 2; |
488 | const HINT_ICON_WINDOW: u32 = 1 << 3; |
489 | const HINT_ICON_POSITION: u32 = 1 << 4; |
490 | const HINT_ICON_MASK: u32 = 1 << 5; |
491 | const HINT_WINDOW_GROUP: u32 = 1 << 6; |
492 | // This bit is obsolete, according to ICCCM |
493 | //const HINT_MESSAGE: u32 = 1 << 7; |
494 | const HINT_URGENCY: u32 = 1 << 8; |
495 | |
496 | /// A structure representing a `WM_HINTS` property. |
497 | #[derive (Debug, Default, Copy, Clone)] |
498 | pub 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 | |
534 | impl 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 | |
582 | impl 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 | |
628 | impl 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`. |
667 | fn 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)] |
681 | mod 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 | |