use std::cell::RefCell;
use std::collections::VecDeque;
use x11rb::connection::Connection;
use x11rb::errors::{ConnectError, ConnectionError, ReplyError};
use x11rb::protocol::Event;
use x11rb::protocol::xproto::{
Atom, AtomEnum, ConnectionExt as _, EventMask, PropMode, Timestamp, Window,
};
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
#[derive(Debug, thiserror::Error)]
pub enum X11Error {
#[error("could not connect to the X server: {0}")]
Connect(#[from] ConnectError),
#[error("X11 connection error: {0}")]
Connection(#[from] ConnectionError),
#[error("X11 request failed: {0}")]
Reply(#[from] ReplyError),
#[error("could not allocate an X11 resource id: {0}")]
IdAllocation(#[from] x11rb::errors::ReplyOrIdError),
#[error("timed out waiting for {0}")]
Timeout(&'static str),
}
pub fn ignore_errors<C: x11rb::connection::RequestConnection>(
result: Result<x11rb::cookie::VoidCookie<'_, C>, ConnectionError>,
) {
if let Ok(cookie) = result {
let _ = cookie.check();
}
}
#[derive(Debug, Clone)]
pub struct Atoms {
pub xdnd_aware: Atom,
pub xdnd_proxy: Atom,
pub xdnd_selection: Atom,
pub xdnd_enter: Atom,
pub xdnd_position: Atom,
pub xdnd_status: Atom,
pub xdnd_leave: Atom,
pub xdnd_drop: Atom,
pub xdnd_finished: Atom,
pub xdnd_type_list: Atom,
pub xdnd_action_copy: Atom,
pub xdnd_action_move: Atom,
pub xdnd_action_link: Atom,
pub xdnd_action_private: Atom,
pub xdnd_action_list: Atom,
pub incr: Atom,
pub targets: Atom,
pub timestamp: Atom,
pub teksilo_transfer: Atom,
pub teksilo_timestamp: Atom,
pub text_uri_list: Atom,
pub text_plain_utf8: Atom,
pub text_plain: Atom,
pub utf8_string: Atom,
pub string: Atom,
pub net_supported: Atom,
pub net_supporting_wm_check: Atom,
pub net_wm_moveresize: Atom,
pub motif_wm_hints: Atom,
}
impl Atoms {
fn intern(conn: &RustConnection) -> Result<Self, X11Error> {
const NAMES: &[&[u8]] = &[
b"XdndAware",
b"XdndProxy",
b"XdndSelection",
b"XdndEnter",
b"XdndPosition",
b"XdndStatus",
b"XdndLeave",
b"XdndDrop",
b"XdndFinished",
b"XdndTypeList",
b"XdndActionCopy",
b"XdndActionMove",
b"XdndActionLink",
b"XdndActionPrivate",
b"XdndActionList",
b"INCR",
b"TARGETS",
b"TIMESTAMP",
b"_TEKSILO_DND_TRANSFER",
b"_TEKSILO_DND_TIMESTAMP",
b"text/uri-list",
b"text/plain;charset=utf-8",
b"text/plain",
b"UTF8_STRING",
b"STRING",
b"_NET_SUPPORTED",
b"_NET_SUPPORTING_WM_CHECK",
b"_NET_WM_MOVERESIZE",
b"_MOTIF_WM_HINTS",
];
let cookies = NAMES
.iter()
.map(|name| conn.intern_atom(false, name))
.collect::<Result<Vec<_>, _>>()?;
let mut atoms = Vec::with_capacity(cookies.len());
for cookie in cookies {
atoms.push(cookie.reply()?.atom);
}
let mut next = atoms.into_iter();
let mut take = || next.next().expect("one atom per interned name");
Ok(Self {
xdnd_aware: take(),
xdnd_proxy: take(),
xdnd_selection: take(),
xdnd_enter: take(),
xdnd_position: take(),
xdnd_status: take(),
xdnd_leave: take(),
xdnd_drop: take(),
xdnd_finished: take(),
xdnd_type_list: take(),
xdnd_action_copy: take(),
xdnd_action_move: take(),
xdnd_action_link: take(),
xdnd_action_private: take(),
xdnd_action_list: take(),
incr: take(),
targets: take(),
timestamp: take(),
teksilo_transfer: take(),
teksilo_timestamp: take(),
text_uri_list: take(),
text_plain_utf8: take(),
text_plain: take(),
utf8_string: take(),
string: take(),
net_supported: take(),
net_supporting_wm_check: take(),
net_wm_moveresize: take(),
motif_wm_hints: take(),
})
}
pub fn preferred_targets(&self) -> [Atom; 5] {
[
self.text_uri_list,
self.text_plain_utf8,
self.utf8_string,
self.text_plain,
self.string,
]
}
pub fn atom_for_mime(&self, mime: &str) -> Option<Atom> {
match mime {
"text/uri-list" => Some(self.text_uri_list),
"text/plain;charset=utf-8" => Some(self.text_plain_utf8),
"text/plain" => Some(self.text_plain),
"UTF8_STRING" => Some(self.utf8_string),
"STRING" => Some(self.string),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct PropertyValue {
pub type_: Atom,
pub format: u8,
pub bytes: Vec<u8>,
}
impl PropertyValue {
pub fn as_u32s(&self) -> Vec<u32> {
if self.format != 32 {
return Vec::new();
}
self.bytes
.as_chunks::<4>()
.0
.iter()
.map(|chunk| u32::from_ne_bytes(*chunk))
.collect()
}
pub fn as_u32(&self) -> Option<u32> {
self.as_u32s().first().copied()
}
}
pub struct X11Connection {
conn: RustConnection,
root: Window,
atoms: Atoms,
pending: RefCell<VecDeque<Event>>,
}
impl X11Connection {
pub fn open() -> Result<Self, X11Error> {
let (conn, screen_num) = x11rb::connect(None)?;
let root = conn.setup().roots[screen_num].root;
let atoms = Atoms::intern(&conn)?;
Ok(Self {
conn,
root,
atoms,
pending: RefCell::new(VecDeque::new()),
})
}
pub fn conn(&self) -> &RustConnection {
&self.conn
}
pub fn root(&self) -> Window {
self.root
}
pub fn atoms(&self) -> &Atoms {
&self.atoms
}
pub fn flush(&self) -> Result<(), X11Error> {
self.conn.flush()?;
Ok(())
}
pub fn get_property_full(
&self,
window: Window,
property: Atom,
type_: Atom,
) -> Result<Option<PropertyValue>, X11Error> {
const CHUNK_UNITS: u32 = 1024;
let mut offset = 0u32;
let mut out: Option<PropertyValue> = None;
loop {
let reply = self
.conn
.get_property(false, window, property, type_, offset, CHUNK_UNITS)?
.reply()?;
if reply.type_ == x11rb::NONE {
return Ok(out);
}
let more = reply.bytes_after > 0;
let format = reply.format;
let reply_type = reply.type_;
let len = reply.value.len();
match &mut out {
Some(acc) => acc.bytes.extend_from_slice(&reply.value),
None => {
out = Some(PropertyValue {
type_: reply_type,
format,
bytes: reply.value,
})
}
}
if !more || len == 0 {
return Ok(out);
}
offset += (len as u32).div_ceil(4);
}
}
pub fn get_property_and_delete(
&self,
window: Window,
property: Atom,
) -> Result<Option<PropertyValue>, X11Error> {
let reply = self
.conn
.get_property(true, window, property, AtomEnum::ANY, 0, u32::MAX / 4)?
.reply()?;
if reply.type_ == x11rb::NONE {
return Ok(None);
}
Ok(Some(PropertyValue {
type_: reply.type_,
format: reply.format,
bytes: reply.value,
}))
}
pub fn set_property32(
&self,
window: Window,
property: Atom,
type_: Atom,
data: &[u32],
) -> Result<(), X11Error> {
self.conn
.change_property32(PropMode::REPLACE, window, property, type_, data)?
.check()?;
Ok(())
}
pub fn set_property8(
&self,
window: Window,
property: Atom,
type_: Atom,
data: &[u8],
) -> Result<(), X11Error> {
self.conn
.change_property8(PropMode::REPLACE, window, property, type_, data)?
.check()?;
Ok(())
}
pub fn fetch_timestamp(&self, window: Window) -> Result<Timestamp, X11Error> {
self.conn
.change_property8(
PropMode::APPEND,
window,
self.atoms.teksilo_timestamp,
AtomEnum::STRING,
&[],
)?
.check()?;
self.conn.flush()?;
for _ in 0..64 {
let event = self.conn.wait_for_event()?;
if let Event::PropertyNotify(ref notify) = event
&& notify.window == window
&& notify.atom == self.atoms.teksilo_timestamp
{
return Ok(notify.time);
}
self.pending.borrow_mut().push_back(event);
}
Err(X11Error::Timeout(
"a PropertyNotify carrying a server timestamp",
))
}
pub fn next_event(&self) -> Result<Event, X11Error> {
if let Some(event) = self.pending.borrow_mut().pop_front() {
return Ok(event);
}
Ok(self.conn.wait_for_event()?)
}
pub fn poll_event(&self) -> Result<Option<Event>, X11Error> {
if let Some(event) = self.pending.borrow_mut().pop_front() {
return Ok(Some(event));
}
Ok(self.conn.poll_for_event()?)
}
pub fn send_client_message(
&self,
destination: Window,
window_field: Window,
type_: Atom,
data: [u32; 5],
mask: EventMask,
) -> Result<(), X11Error> {
use x11rb::protocol::xproto::ClientMessageEvent;
let event = ClientMessageEvent::new(32, window_field, type_, data);
self.conn
.send_event(false, destination, mask, event)?
.check()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_value_reads_32_bit_words() {
let value = PropertyValue {
type_: 1,
format: 32,
bytes: 5u32
.to_ne_bytes()
.into_iter()
.chain(7u32.to_ne_bytes())
.collect(),
};
assert_eq!(value.as_u32s(), vec![5, 7]);
assert_eq!(value.as_u32(), Some(5));
}
#[test]
fn property_value_rejects_a_mismatched_format() {
let value = PropertyValue {
type_: 1,
format: 8,
bytes: vec![1, 2, 3, 4],
};
assert!(value.as_u32s().is_empty());
assert_eq!(value.as_u32(), None);
}
#[test]
fn property_value_ignores_a_trailing_partial_word() {
let value = PropertyValue {
type_: 1,
format: 32,
bytes: vec![1, 2, 3, 4, 5],
};
assert_eq!(value.as_u32s().len(), 1);
}
}