use crate::connection::RequestConnection;
use crate::cookie::{Cookie, VoidCookie};
use crate::errors::{ConnectionError, ParseError, ReplyError};
use crate::protocol::xproto::{self, Atom, AtomEnum, GetPropertyReply, Window};
use crate::x11_utils::{Serialize, TryParse};
macro_rules! property_cookie {
{
$(#[$meta:meta])*
pub struct $cookie_name:ident: $struct_name:ident,
$from_reply:expr,
} => {
$(#[$meta])*
#[derive(Debug)]
pub struct $cookie_name<'a, Conn: RequestConnection + ?Sized>(Cookie<'a, Conn, GetPropertyReply>);
impl<'a, Conn> $cookie_name<'a, Conn>
where
Conn: RequestConnection + ?Sized,
{
pub fn reply(self) -> Result<Option<$struct_name>, ReplyError> {
#[allow(clippy::redundant_closure_call)]
Ok($from_reply(self.0.reply()?)?)
}
pub fn reply_unchecked(self) -> Result<Option<$struct_name>, ConnectionError> {
self.0
.reply_unchecked()?
.map($from_reply)
.transpose()
.map(|e| e.flatten())
.map_err(Into::into)
}
}
}
}
property_cookie! {
pub struct WmClassCookie: WmClass,
WmClass::from_reply,
}
impl<'a, Conn> WmClassCookie<'a, Conn>
where
Conn: RequestConnection + ?Sized,
{
pub fn new(conn: &'a Conn, window: Window) -> Result<Self, ConnectionError> {
Ok(Self(xproto::get_property(
conn,
false,
window,
AtomEnum::WM_CLASS,
AtomEnum::STRING,
0,
2048,
)?))
}
}
#[derive(Debug)]
pub struct WmClass(GetPropertyReply, usize);
impl WmClass {
pub fn get<C: RequestConnection>(
conn: &C,
window: Window,
) -> Result<WmClassCookie<'_, C>, ConnectionError> {
WmClassCookie::new(conn, window)
}
pub fn from_reply(reply: GetPropertyReply) -> Result<Option<Self>, ParseError> {
if reply.type_ == AtomEnum::NONE.into() {
return Ok(None);
}
if reply.type_ != AtomEnum::STRING.into() || reply.format != 8 {
return Err(ParseError::InvalidValue);
}
let offset = reply
.value
.iter()
.position(|&v| v == 0)
.unwrap_or(reply.value.len());
Ok(Some(WmClass(reply, offset)))
}
pub fn instance(&self) -> &[u8] {
&self.0.value[0..self.1]
}
pub fn class(&self) -> &[u8] {
let start = self.1 + 1;
if start >= self.0.value.len() {
return &[];
};
let end = if self.0.value.last() == Some(&0) {
self.0.value.len() - 1
} else {
self.0.value.len()
};
&self.0.value[start..end]
}
}
#[derive(Debug, Copy, Clone)]
pub enum WmSizeHintsSpecification {
UserSpecified,
ProgramSpecified,
}
property_cookie! {
pub struct WmSizeHintsCookie: WmSizeHints,
|reply| WmSizeHints::from_reply(&reply),
}
const NUM_WM_SIZE_HINTS_ELEMENTS: u16 = 18;
impl<'a, Conn> WmSizeHintsCookie<'a, Conn>
where
Conn: RequestConnection + ?Sized,
{
pub fn new(
conn: &'a Conn,
window: Window,
property: impl Into<Atom>,
) -> Result<Self, ConnectionError> {
Ok(Self(xproto::get_property(
conn,
false,
window,
property,
AtomEnum::WM_SIZE_HINTS,
0,
NUM_WM_SIZE_HINTS_ELEMENTS.into(),
)?))
}
}
const U_S_POSITION: u32 = 1;
const U_S_SIZE: u32 = 1 << 1;
const P_S_POSITION: u32 = 1 << 2;
const P_S_SIZE: u32 = 1 << 3;
const P_MIN_SIZE: u32 = 1 << 4;
const P_MAX_SIZE: u32 = 1 << 5;
const P_RESIZE_INCREMENT: u32 = 1 << 6;
const P_ASPECT: u32 = 1 << 7;
const P_BASE_SIZE: u32 = 1 << 8;
const P_WIN_GRAVITY: u32 = 1 << 9;
#[derive(Debug, Copy, Clone)]
pub struct AspectRatio {
pub numerator: i32,
pub denominator: i32,
}
impl AspectRatio {
pub fn new(numerator: i32, denominator: i32) -> Self {
Self {
numerator,
denominator,
}
}
}
impl TryParse for AspectRatio {
fn try_parse(value: &[u8]) -> Result<(Self, &[u8]), ParseError> {
let ((numerator, denominator), remaining) = TryParse::try_parse(value)?;
let result = AspectRatio::new(numerator, denominator);
Ok((result, remaining))
}
}
#[allow(clippy::many_single_char_names)]
impl Serialize for AspectRatio {
type Bytes = [u8; 8];
fn serialize(&self) -> Self::Bytes {
let [a, b, c, d] = self.numerator.serialize();
let [e, f, g, h] = self.denominator.serialize();
[a, b, c, d, e, f, g, h]
}
fn serialize_into(&self, bytes: &mut Vec<u8>) {
(self.numerator, self.denominator).serialize_into(bytes);
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct WmSizeHints {
pub position: Option<(WmSizeHintsSpecification, i32, i32)>,
pub size: Option<(WmSizeHintsSpecification, i32, i32)>,
pub min_size: Option<(i32, i32)>,
pub max_size: Option<(i32, i32)>,
pub size_increment: Option<(i32, i32)>,
pub aspect: Option<(AspectRatio, AspectRatio)>,
pub base_size: Option<(i32, i32)>,
pub win_gravity: Option<xproto::Gravity>,
}
impl WmSizeHints {
pub fn new() -> Self {
Default::default()
}
pub fn get<C: RequestConnection>(
conn: &C,
window: Window,
property: impl Into<Atom>,
) -> Result<WmSizeHintsCookie<'_, C>, ConnectionError> {
WmSizeHintsCookie::new(conn, window, property)
}
pub fn get_normal_hints<C: RequestConnection>(
conn: &C,
window: Window,
) -> Result<WmSizeHintsCookie<'_, C>, ConnectionError> {
Self::get(conn, window, AtomEnum::WM_NORMAL_HINTS)
}
pub fn from_reply(reply: &GetPropertyReply) -> Result<Option<Self>, ParseError> {
if reply.type_ == AtomEnum::NONE.into() {
return Ok(None);
}
if reply.type_ != AtomEnum::WM_SIZE_HINTS.into() || reply.format != 32 {
return Err(ParseError::InvalidValue);
}
Ok(Some(Self::try_parse(&reply.value)?.0))
}
pub fn set_normal_hints<'a, C: RequestConnection + ?Sized>(
&self,
conn: &'a C,
window: Window,
) -> Result<VoidCookie<'a, C>, ConnectionError> {
self.set(conn, window, AtomEnum::WM_NORMAL_HINTS)
}
pub fn set<'a, C: RequestConnection + ?Sized>(
&self,
conn: &'a C,
window: Window,
property: impl Into<Atom>,
) -> Result<VoidCookie<'a, C>, ConnectionError> {
let data = self.serialize();
xproto::change_property(
conn,
xproto::PropMode::REPLACE,
window,
property.into(),
AtomEnum::WM_SIZE_HINTS,
32,
NUM_WM_SIZE_HINTS_ELEMENTS.into(),
&data,
)
}
}
impl TryParse for WmSizeHints {
fn try_parse(remaining: &[u8]) -> Result<(Self, &[u8]), ParseError> {
let (flags, remaining) = u32::try_parse(remaining)?;
let (x, remaining) = i32::try_parse(remaining)?;
let (y, remaining) = i32::try_parse(remaining)?;
let (width, remaining) = i32::try_parse(remaining)?;
let (height, remaining) = i32::try_parse(remaining)?;
let (min_size, remaining) = parse_with_flag::<(i32, i32)>(remaining, flags, P_MIN_SIZE)?;
let (max_size, remaining) = parse_with_flag::<(i32, i32)>(remaining, flags, P_MAX_SIZE)?;
let (size_increment, remaining) =
parse_with_flag::<(i32, i32)>(remaining, flags, P_RESIZE_INCREMENT)?;
let (aspect, remaining) =
parse_with_flag::<(AspectRatio, AspectRatio)>(remaining, flags, P_ASPECT)?;
let (base_size, win_gravity, remaining) = if remaining.is_empty() {
(min_size, Some(xproto::Gravity::NORTH_WEST), remaining)
} else {
let (base_size, remaining) =
parse_with_flag::<(i32, i32)>(remaining, flags, P_BASE_SIZE)?;
let (win_gravity, remaining) = parse_with_flag::<u32>(remaining, flags, P_WIN_GRAVITY)?;
(base_size, win_gravity.map(Into::into), remaining)
};
let position = if flags & U_S_POSITION != 0 {
Some((WmSizeHintsSpecification::UserSpecified, x, y))
} else if flags & P_S_POSITION != 0 {
Some((WmSizeHintsSpecification::ProgramSpecified, x, y))
} else {
None
};
let size = if flags & U_S_SIZE != 0 {
Some((WmSizeHintsSpecification::UserSpecified, width, height))
} else if flags & P_S_SIZE != 0 {
Some((WmSizeHintsSpecification::ProgramSpecified, width, height))
} else {
None
};
Ok((
WmSizeHints {
position,
size,
min_size,
max_size,
size_increment,
aspect,
base_size,
win_gravity,
},
remaining,
))
}
}
impl Serialize for WmSizeHints {
type Bytes = Vec<u8>;
fn serialize(&self) -> Self::Bytes {
let mut result = Vec::with_capacity((NUM_WM_SIZE_HINTS_ELEMENTS * 4).into());
self.serialize_into(&mut result);
result
}
fn serialize_into(&self, bytes: &mut Vec<u8>) {
let mut flags = 0;
match self.position {
Some((WmSizeHintsSpecification::UserSpecified, _, _)) => flags |= U_S_POSITION,
Some((WmSizeHintsSpecification::ProgramSpecified, _, _)) => flags |= P_S_POSITION,
None => {}
}
match self.size {
Some((WmSizeHintsSpecification::UserSpecified, _, _)) => flags |= U_S_SIZE,
Some((WmSizeHintsSpecification::ProgramSpecified, _, _)) => flags |= P_S_SIZE,
None => {}
}
flags |= self.min_size.map_or(0, |_| P_MIN_SIZE);
flags |= self.max_size.map_or(0, |_| P_MAX_SIZE);
flags |= self.size_increment.map_or(0, |_| P_RESIZE_INCREMENT);
flags |= self.aspect.map_or(0, |_| P_ASPECT);
flags |= self.base_size.map_or(0, |_| P_BASE_SIZE);
flags |= self.win_gravity.map_or(0, |_| P_WIN_GRAVITY);
flags.serialize_into(bytes);
match self.position {
Some((_, x, y)) => (x, y),
None => (0, 0),
}
.serialize_into(bytes);
match self.size {
Some((_, width, height)) => (width, height),
None => (0, 0),
}
.serialize_into(bytes);
self.min_size.unwrap_or((0, 0)).serialize_into(bytes);
self.max_size.unwrap_or((0, 0)).serialize_into(bytes);
self.size_increment.unwrap_or((0, 0)).serialize_into(bytes);
self.aspect
.unwrap_or((AspectRatio::new(0, 0), AspectRatio::new(0, 0)))
.serialize_into(bytes);
self.base_size.unwrap_or((0, 0)).serialize_into(bytes);
self.win_gravity.map_or(0, u32::from).serialize_into(bytes);
}
}
property_cookie! {
pub struct WmHintsCookie: WmHints,
|reply| WmHints::from_reply(&reply),
}
const NUM_WM_HINTS_ELEMENTS: u32 = 9;
impl<'a, Conn> WmHintsCookie<'a, Conn>
where
Conn: RequestConnection + ?Sized,
{
pub fn new(conn: &'a Conn, window: Window) -> Result<Self, ConnectionError> {
Ok(Self(xproto::get_property(
conn,
false,
window,
AtomEnum::WM_HINTS,
AtomEnum::WM_HINTS,
0,
NUM_WM_HINTS_ELEMENTS,
)?))
}
}
#[derive(Debug, Copy, Clone)]
pub enum WmHintsState {
Normal,
Iconic,
}
const HINT_INPUT: u32 = 1;
const HINT_STATE: u32 = 1 << 1;
const HINT_ICON_PIXMAP: u32 = 1 << 2;
const HINT_ICON_WINDOW: u32 = 1 << 3;
const HINT_ICON_POSITION: u32 = 1 << 4;
const HINT_ICON_MASK: u32 = 1 << 5;
const HINT_WINDOW_GROUP: u32 = 1 << 6;
const HINT_URGENCY: u32 = 1 << 8;
#[derive(Debug, Default, Copy, Clone)]
pub struct WmHints {
pub input: Option<bool>,
pub initial_state: Option<WmHintsState>,
pub icon_pixmap: Option<xproto::Pixmap>,
pub icon_window: Option<Window>,
pub icon_position: Option<(i32, i32)>,
pub icon_mask: Option<xproto::Pixmap>,
pub window_group: Option<Window>,
pub urgent: bool,
}
impl WmHints {
pub fn new() -> Self {
Default::default()
}
pub fn get<C: RequestConnection>(
conn: &C,
window: Window,
) -> Result<WmHintsCookie<'_, C>, ConnectionError> {
WmHintsCookie::new(conn, window)
}
pub fn from_reply(reply: &GetPropertyReply) -> Result<Option<Self>, ParseError> {
if reply.type_ == AtomEnum::NONE.into() {
return Ok(None);
}
if reply.type_ != AtomEnum::WM_HINTS.into() || reply.format != 32 {
return Err(ParseError::InvalidValue);
}
Ok(Some(Self::try_parse(&reply.value)?.0))
}
pub fn set<'a, C: RequestConnection + ?Sized>(
&self,
conn: &'a C,
window: Window,
) -> Result<VoidCookie<'a, C>, ConnectionError> {
let data = self.serialize();
xproto::change_property(
conn,
xproto::PropMode::REPLACE,
window,
AtomEnum::WM_HINTS,
AtomEnum::WM_HINTS,
32,
NUM_WM_HINTS_ELEMENTS,
&data,
)
}
}
impl TryParse for WmHints {
fn try_parse(remaining: &[u8]) -> Result<(Self, &[u8]), ParseError> {
let (flags, remaining) = u32::try_parse(remaining)?;
let (input, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_INPUT)?;
let (initial_state, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_STATE)?;
let (icon_pixmap, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_PIXMAP)?;
let (icon_window, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_WINDOW)?;
let (icon_position, remaining) =
parse_with_flag::<(i32, i32)>(remaining, flags, HINT_ICON_POSITION)?;
let (icon_mask, remaining) = parse_with_flag::<u32>(remaining, flags, HINT_ICON_MASK)?;
let (window_group, remaining) = if remaining.is_empty() {
(None, remaining)
} else {
let (window_group, remaining) =
parse_with_flag::<u32>(remaining, flags, HINT_WINDOW_GROUP)?;
(window_group, remaining)
};
let input = input.map(|input| input != 0);
let initial_state = match initial_state {
None => None,
Some(1) => Some(WmHintsState::Normal),
Some(3) => Some(WmHintsState::Iconic),
_ => return Err(ParseError::InvalidValue),
};
let urgent = flags & HINT_URGENCY != 0;
Ok((
WmHints {
input,
initial_state,
icon_pixmap,
icon_window,
icon_position,
icon_mask,
window_group,
urgent,
},
remaining,
))
}
}
impl Serialize for WmHints {
type Bytes = Vec<u8>;
fn serialize(&self) -> Self::Bytes {
let mut result = Vec::with_capacity((NUM_WM_HINTS_ELEMENTS * 4).try_into().unwrap());
self.serialize_into(&mut result);
result
}
fn serialize_into(&self, bytes: &mut Vec<u8>) {
let mut flags = 0;
flags |= self.input.map_or(0, |_| HINT_INPUT);
flags |= self.initial_state.map_or(0, |_| HINT_STATE);
flags |= self.icon_pixmap.map_or(0, |_| HINT_ICON_PIXMAP);
flags |= self.icon_window.map_or(0, |_| HINT_ICON_WINDOW);
flags |= self.icon_position.map_or(0, |_| HINT_ICON_POSITION);
flags |= self.icon_mask.map_or(0, |_| HINT_ICON_MASK);
flags |= self.window_group.map_or(0, |_| HINT_WINDOW_GROUP);
if self.urgent {
flags |= HINT_URGENCY;
}
flags.serialize_into(bytes);
u32::from(self.input.unwrap_or(false)).serialize_into(bytes);
match self.initial_state {
Some(WmHintsState::Normal) => 1,
Some(WmHintsState::Iconic) => 3,
None => 0,
}
.serialize_into(bytes);
self.icon_pixmap.unwrap_or(0).serialize_into(bytes);
self.icon_window.unwrap_or(0).serialize_into(bytes);
self.icon_position.unwrap_or((0, 0)).serialize_into(bytes);
self.icon_mask.unwrap_or(0).serialize_into(bytes);
self.window_group.unwrap_or(0).serialize_into(bytes);
}
}
fn parse_with_flag<T: TryParse>(
remaining: &[u8],
flags: u32,
bit: u32,
) -> Result<(Option<T>, &[u8]), ParseError> {
let (value, remaining) = T::try_parse(remaining)?;
if flags & bit != 0 {
Ok((Some(value), remaining))
} else {
Ok((None, remaining))
}
}
#[cfg(test)]
mod test {
use super::{WmClass, WmHints, WmHintsState, WmSizeHints};
use crate::protocol::xproto::{Atom, AtomEnum, GetPropertyReply, Gravity};
use crate::x11_utils::Serialize;
fn get_property_reply(value: &[u8], format: u8, type_: impl Into<Atom>) -> GetPropertyReply {
GetPropertyReply {
format,
sequence: 0,
length: 0,
type_: type_.into(),
bytes_after: 0,
value_len: value.len().try_into().unwrap(),
value: value.to_vec(),
}
}
#[test]
fn test_wm_class() {
for (input, instance, class) in &[
(&b""[..], &b""[..], &b""[..]),
(b"\0", b"", b""),
(b"\0\0", b"", b""),
(b"\0\0\0", b"", b"\0"),
(b"Hello World", b"Hello World", b""),
(b"Hello World\0", b"Hello World", b""),
(b"Hello\0World\0", b"Hello", b"World"),
(b"Hello\0World", b"Hello", b"World"),
(b"Hello\0World\0Good\0Day", b"Hello", b"World\0Good\0Day"),
] {
let wm_class = WmClass::from_reply(get_property_reply(input, 8, AtomEnum::STRING))
.unwrap()
.unwrap();
assert_eq!((wm_class.instance(), wm_class.class()), (*instance, *class));
}
}
#[test]
fn test_wm_class_missing() {
let wm_class = WmClass::from_reply(get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
assert!(wm_class.is_none());
}
#[test]
fn test_wm_normal_hints() {
let input = [
0x0000_0350,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_0015,
0x0000_0017,
0x0000_0000,
0x0000_0000,
0x0000_000a,
0x0000_0013,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_000b,
0x0000_0004,
0x0000_0001,
];
let input = input
.iter()
.flat_map(|v| u32::serialize(v).to_vec())
.collect::<Vec<u8>>();
let wm_size_hints =
WmSizeHints::from_reply(&get_property_reply(&input, 32, AtomEnum::WM_SIZE_HINTS))
.unwrap()
.unwrap();
assert!(
wm_size_hints.position.is_none(),
"{:?}",
wm_size_hints.position,
);
assert!(wm_size_hints.size.is_none(), "{:?}", wm_size_hints.size);
assert_eq!(wm_size_hints.min_size, Some((21, 23)));
assert_eq!(wm_size_hints.max_size, None);
assert_eq!(wm_size_hints.size_increment, Some((10, 19)));
assert!(wm_size_hints.aspect.is_none(), "{:?}", wm_size_hints.aspect);
assert_eq!(wm_size_hints.base_size, Some((11, 4)));
assert_eq!(wm_size_hints.win_gravity, Some(Gravity::NORTH_WEST));
assert_eq!(input, wm_size_hints.serialize());
}
#[test]
fn test_wm_normal_hints_missing() {
let wm_size_hints =
WmSizeHints::from_reply(&get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
assert!(wm_size_hints.is_none());
}
#[test]
fn test_wm_hints() {
let input = [
0x0000_0043,
0x0000_0001,
0x0000_0001,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0000_0000,
0x0060_0009,
];
let input = input
.iter()
.flat_map(|v| u32::serialize(v).to_vec())
.collect::<Vec<u8>>();
let wm_hints = WmHints::from_reply(&get_property_reply(&input, 32, AtomEnum::WM_HINTS))
.unwrap()
.unwrap();
assert_eq!(wm_hints.input, Some(true));
match wm_hints.initial_state {
Some(WmHintsState::Normal) => {}
value => panic!("Expected Some(Normal), but got {:?}", value),
}
assert_eq!(wm_hints.icon_pixmap, None);
assert_eq!(wm_hints.icon_window, None);
assert_eq!(wm_hints.icon_position, None);
assert_eq!(wm_hints.icon_mask, None);
assert_eq!(wm_hints.window_group, Some(0x0060_0009));
assert!(!wm_hints.urgent);
assert_eq!(input, wm_hints.serialize());
}
#[test]
fn test_wm_hints_missing() {
let wm_hints = WmHints::from_reply(&get_property_reply(&[], 0, AtomEnum::NONE)).unwrap();
assert!(wm_hints.is_none());
}
}