| 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 | |