use core::ffi::{CStr, c_char, c_int, c_void};
use core::ptr;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use super::ffi::{self, Connection, Lib};
use super::read_wire;
mod fake {
pub const KEY_PRESS: u8 = 2;
pub const KEY_RELEASE: u8 = 3;
pub const BUTTON_PRESS: u8 = 4;
pub const BUTTON_RELEASE: u8 = 5;
pub const MOTION: u8 = 6;
}
struct TestLib {
set_input_focus: unsafe extern "C" fn(*mut Connection, u8, u32, u32) -> ffi::Cookie,
fake_input:
unsafe extern "C" fn(*mut Connection, u8, u8, u32, u32, i16, i16, u8) -> ffi::Cookie,
}
unsafe extern "C" {
fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
const RTLD_NOW: c_int = 2;
fn load_test_lib() -> Option<TestLib> {
let (xcb, xtest) = unsafe {
(
dlopen(c"libxcb.so.1".as_ptr(), RTLD_NOW),
dlopen(c"libxcb-xtest.so.0".as_ptr(), RTLD_NOW),
)
};
if xcb.is_null() || xtest.is_null() {
return None;
}
unsafe {
let focus = dlsym(xcb, c"xcb_set_input_focus".as_ptr());
let input = dlsym(xtest, c"xcb_test_fake_input".as_ptr());
if focus.is_null() || input.is_null() {
return None;
}
Some(TestLib {
set_input_focus: core::mem::transmute::<
*mut c_void,
unsafe extern "C" fn(*mut Connection, u8, u32, u32) -> ffi::Cookie,
>(focus),
fake_input: core::mem::transmute::<
*mut c_void,
unsafe extern "C" fn(
*mut Connection,
u8,
u8,
u32,
u32,
i16,
i16,
u8,
) -> ffi::Cookie,
>(input),
})
}
}
pub const MAX_EVENTS_PER_PUMP: usize = super::input::MAX_EVENTS_PER_PUMP;
pub const MAX_PENDING_CLIPBOARD_WRITES: usize = super::selection::MAX_PENDING_WRITES;
struct Offer {
target: u32,
bytes: Vec<u8>,
}
struct Outgoing {
requestor: u32,
property: u32,
target: u32,
bytes: Vec<u8>,
offset: usize,
chunk: usize,
terminated: bool,
}
struct Pull {
property: u32,
bytes: Vec<u8>,
done: bool,
}
#[derive(Debug, Default)]
struct Incoming {
active: bool,
incremental: bool,
bytes: Vec<u8>,
done: Option<Option<Vec<u8>>>,
}
struct DragSource {
target: u32,
version: u8,
status: Option<(bool, u32)>,
statuses: u32,
finished: Option<(bool, u32)>,
dropped_at: Option<u32>,
converted_at: Option<u32>,
}
pub struct Peer {
lib: &'static Lib,
test: TestLib,
connection: *mut Connection,
root: u32,
window: u32,
atoms: HashMap<String, u32>,
offers: Vec<Offer>,
xdnd_offers: Vec<Offer>,
drag: Option<DragSource>,
xdnd_conversions: u32,
outgoing: Vec<Outgoing>,
incoming: Incoming,
pulls: Vec<Pull>,
property_notifies: u64,
last_time: u32,
chunk: Option<usize>,
}
impl core::fmt::Debug for Peer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Peer")
.field("window", &self.window)
.field("offers", &self.offers.len())
.field("outgoing", &self.outgoing.len())
.finish()
}
}
impl Peer {
#[must_use]
pub fn new() -> Option<Self> {
let lib = ffi::load().ok()?;
let test = load_test_lib()?;
let connection = unsafe { (lib.connect)(ptr::null(), ptr::null_mut()) };
if connection.is_null() {
return None;
}
if unsafe { (lib.connection_has_error)(connection) } != 0 {
unsafe { (lib.disconnect)(connection) };
return None;
}
let root = unsafe {
let setup = (lib.get_setup)(connection);
let iterator = (lib.setup_roots_iterator)(setup);
if iterator.data.is_null() {
(lib.disconnect)(connection);
return None;
}
(*iterator.data).root
};
let window = {
let id = unsafe { (lib.generate_id)(connection) };
let values = [ffi::event_mask::PROPERTY_CHANGE];
unsafe {
(lib.create_window)(
connection,
0,
id,
root,
0,
0,
1,
1,
0,
ffi::value::INPUT_OUTPUT,
0,
ffi::cw::EVENT_MASK,
values.as_ptr().cast::<c_void>(),
);
(lib.flush)(connection);
}
id
};
Some(Self {
lib,
test,
connection,
root,
window,
atoms: HashMap::new(),
offers: Vec::new(),
xdnd_offers: Vec::new(),
drag: None,
xdnd_conversions: 0,
outgoing: Vec::new(),
incoming: Incoming::default(),
pulls: Vec::new(),
property_notifies: 0,
last_time: 0,
chunk: None,
})
}
pub fn atom(&mut self, name: &str) -> u32 {
if let Some(atom) = self.atoms.get(name) {
return *atom;
}
let atom = unsafe {
let cookie = (self.lib.intern_atom)(
self.connection,
0,
u16::try_from(name.len()).unwrap_or(u16::MAX),
name.as_ptr().cast::<c_char>(),
);
let reply = (self.lib.intern_atom_reply)(self.connection, cookie, ptr::null_mut());
if reply.is_null() {
0
} else {
let value = (*reply).atom;
ffi::free_reply(reply);
value
}
};
self.atoms.insert(name.to_string(), atom);
atom
}
fn flush(&self) {
unsafe { (self.lib.flush)(self.connection) };
}
pub fn focus(&self, xid: u32) {
const REVERT_TO_PARENT: u8 = 2;
unsafe {
(self.test.set_input_focus)(
self.connection,
REVERT_TO_PARENT,
xid,
ffi::value::CURRENT_TIME,
);
}
self.flush();
}
fn server_time(&mut self) -> u32 {
const DEADLINE: Duration = Duration::from_secs(2);
let stamp = self.atom("CRCBL_E2E_TIME");
let answered = self.property_notifies;
unsafe {
(self.lib.change_property)(
self.connection,
ffi::value::PROP_MODE_APPEND,
self.window,
stamp,
ffi::value::ATOM_STRING,
8,
0,
ptr::null(),
);
}
self.flush();
let started = Instant::now();
while self.property_notifies == answered {
assert!(
started.elapsed() < DEADLINE,
"the X server did not answer a property append with a \
PropertyNotify within {DEADLINE:?}; without one every \
timestamp this harness sends would be CurrentTime, which is \
older than everything"
);
self.service();
}
self.last_time
}
pub fn activate(&mut self, xid: u32) {
let active = self.atom("_NET_ACTIVE_WINDOW");
let time = self.server_time();
const SUBSTRUCTURE: u32 = (1 << 19) | (1 << 20);
const SOURCE_PAGER: u32 = 2;
let message = ffi::ClientMessageEvent {
response_type: ffi::event::CLIENT_MESSAGE,
format: 32,
sequence: 0,
window: xid,
type_: active,
data: [SOURCE_PAGER, time, 0, 0, 0],
};
unsafe {
(self.lib.send_event)(
self.connection,
0,
self.root,
SUBSTRUCTURE,
ptr::from_ref(&message).cast::<c_char>(),
);
}
self.flush();
}
pub fn request_close(&mut self, xid: u32) {
let protocols = self.atom("WM_PROTOCOLS");
let delete = self.atom("WM_DELETE_WINDOW");
let message = ffi::ClientMessageEvent {
response_type: ffi::event::CLIENT_MESSAGE,
format: 32,
sequence: 0,
window: xid,
type_: protocols,
data: [delete, ffi::value::CURRENT_TIME, 0, 0, 0],
};
unsafe {
(self.lib.send_event)(
self.connection,
0,
xid,
0,
ptr::from_ref(&message).cast::<c_char>(),
);
}
self.flush();
}
pub fn burst(&mut self, xid: u32, count: usize) {
let flood = self.atom("CRCBL_E2E_BURST");
let message = ffi::ClientMessageEvent {
response_type: ffi::event::CLIENT_MESSAGE,
format: 32,
sequence: 0,
window: xid,
type_: flood,
data: [0; 5],
};
for _ in 0..count {
unsafe {
(self.lib.send_event)(
self.connection,
0,
xid,
0,
ptr::from_ref(&message).cast::<c_char>(),
);
}
}
self.flush();
let _ = self.window_origin(xid);
}
#[must_use]
pub fn window_origin(&self, xid: u32) -> Option<(i16, i16)> {
let reply = unsafe {
let cookie = (self.lib.translate_coordinates)(self.connection, xid, self.root, 0, 0);
(self.lib.translate_coordinates_reply)(self.connection, cookie, ptr::null_mut())
};
if reply.is_null() {
return None;
}
let origin = unsafe { ((*reply).dst_x, (*reply).dst_y) };
unsafe { ffi::free_reply(reply) };
Some(origin)
}
#[must_use]
pub fn connection_error(&self) -> Option<i32> {
let code = unsafe { (self.lib.connection_has_error)(self.connection) };
(code != 0).then_some(code)
}
pub fn window_parent(&self, xid: u32) -> Option<u32> {
let reply = unsafe {
let cookie = (self.lib.query_tree)(self.connection, xid);
(self.lib.query_tree_reply)(self.connection, cookie, ptr::null_mut())
};
if reply.is_null() {
return None;
}
let parent = unsafe { (*reply).parent };
unsafe { ffi::free_reply(reply) };
Some(parent)
}
pub fn resize(&self, xid: u32, width: u32, height: u32) {
let values = [width, height];
unsafe {
(self.lib.configure_window)(
self.connection,
xid,
ffi::config_window::WIDTH | ffi::config_window::HEIGHT,
values.as_ptr().cast::<c_void>(),
);
}
self.flush();
}
pub fn key(&self, keycode: u8, pressed: bool) {
self.fake(
if pressed {
fake::KEY_PRESS
} else {
fake::KEY_RELEASE
},
keycode,
0,
0,
);
}
pub fn button(&self, button: u8, pressed: bool) {
self.fake(
if pressed {
fake::BUTTON_PRESS
} else {
fake::BUTTON_RELEASE
},
button,
0,
0,
);
}
pub fn motion(&self, x: i16, y: i16) {
self.fake(fake::MOTION, 0, x, y);
}
fn fake(&self, kind: u8, detail: u8, x: i16, y: i16) {
unsafe {
(self.test.fake_input)(
self.connection,
kind,
detail,
ffi::value::CURRENT_TIME,
self.root,
x,
y,
0,
);
}
self.flush();
}
pub fn own_clipboard(&mut self, payloads: &[(&str, &[u8])]) {
self.chunk = None;
self.claim(payloads);
}
pub fn own_clipboard_incrementally(&mut self, payloads: &[(&str, &[u8])], chunk: usize) {
self.chunk = Some(chunk.max(1));
self.claim(payloads);
}
fn claim(&mut self, payloads: &[(&str, &[u8])]) {
self.offers = self.intern_offers(payloads);
self.set_owner("CLIPBOARD", self.window);
}
fn intern_offers(&mut self, payloads: &[(&str, &[u8])]) -> Vec<Offer> {
payloads
.iter()
.map(|(name, bytes)| Offer {
target: self.atom(name),
bytes: (*bytes).to_vec(),
})
.collect()
}
pub fn release_clipboard(&mut self) {
self.offers.clear();
self.outgoing.clear();
self.set_owner("CLIPBOARD", ffi::value::NONE);
}
#[must_use]
pub fn xdnd_version(&mut self, xid: u32) -> Option<u8> {
let aware = self.atom("XdndAware");
let (_, bytes) = self.property_on(xid, aware)?;
let word = bytes.get(..4)?;
Some(u32::from_ne_bytes([word[0], word[1], word[2], word[3]]) as u8)
}
pub fn xdnd_enter(
&mut self,
xid: u32,
version: u8,
types: &[&str],
payloads: &[(&str, &[u8])],
) {
self.chunk = None;
self.begin_drag(xid, version, types, payloads);
}
pub fn xdnd_enter_incrementally(
&mut self,
xid: u32,
version: u8,
types: &[&str],
payloads: &[(&str, &[u8])],
chunk: usize,
) {
self.chunk = Some(chunk.max(1));
self.begin_drag(xid, version, types, payloads);
}
#[must_use]
pub const fn xdnd_conversions(&self) -> u32 {
self.xdnd_conversions
}
fn begin_drag(&mut self, xid: u32, version: u8, types: &[&str], payloads: &[(&str, &[u8])]) {
self.xdnd_offers = self.intern_offers(payloads);
self.set_owner("XdndSelection", self.window);
let atoms: Vec<u32> = types.iter().map(|name| self.atom(name)).collect();
let long = atoms.len() > 3;
if long {
let type_list = self.atom("XdndTypeList");
let bytes: Vec<u8> = atoms.iter().flat_map(|atom| atom.to_ne_bytes()).collect();
self.set_property(self.window, type_list, ffi::value::ATOM_ATOM, 32, &bytes);
}
self.drag = Some(DragSource {
target: xid,
version,
status: None,
statuses: 0,
finished: None,
dropped_at: None,
converted_at: None,
});
let flags = (u32::from(version) << 24) | u32::from(long);
let enter = self.atom("XdndEnter");
self.send_xdnd(
xid,
enter,
[
self.window,
flags,
atoms.first().copied().unwrap_or(0),
atoms.get(1).copied().unwrap_or(0),
atoms.get(2).copied().unwrap_or(0),
],
);
}
pub fn xdnd_position(&mut self, x: i16, y: i16) {
let Some(target) = self.drag.as_ref().map(|drag| drag.target) else {
return;
};
let action = self.atom("XdndActionCopy");
let position = self.atom("XdndPosition");
let time = self.last_time;
self.send_xdnd(
target,
position,
[
self.window,
0,
(u32::from(x as u16) << 16) | u32::from(y as u16),
time,
action,
],
);
}
pub fn xdnd_leave(&mut self) {
let Some(target) = self.drag.as_ref().map(|drag| drag.target) else {
return;
};
let leave = self.atom("XdndLeave");
self.send_xdnd(target, leave, [self.window, 0, 0, 0, 0]);
}
pub fn xdnd_drop(&mut self) {
let Some(target) = self.drag.as_ref().map(|drag| drag.target) else {
return;
};
let time = self.server_time();
let drop = self.atom("XdndDrop");
self.send_xdnd(target, drop, [self.window, 0, time, 0, 0]);
if let Some(drag) = self.drag.as_mut() {
drag.dropped_at = Some(time);
}
}
#[must_use]
pub fn xdnd_status(&self) -> Option<(bool, u32)> {
self.drag.as_ref().and_then(|drag| drag.status)
}
#[must_use]
pub fn xdnd_statuses(&self) -> u32 {
self.drag.as_ref().map_or(0, |drag| drag.statuses)
}
#[must_use]
pub fn xdnd_finished(&self) -> Option<(bool, u32)> {
self.drag.as_ref().and_then(|drag| drag.finished)
}
#[must_use]
pub fn xdnd_drop_times(&self) -> (Option<u32>, Option<u32>) {
self.drag
.as_ref()
.map_or((None, None), |drag| (drag.dropped_at, drag.converted_at))
}
#[must_use]
pub fn xdnd_announced_version(&self) -> Option<u8> {
self.drag.as_ref().map(|drag| drag.version)
}
fn send_xdnd(&self, xid: u32, type_: u32, data: [u32; 5]) {
let message = ffi::ClientMessageEvent {
response_type: ffi::event::CLIENT_MESSAGE,
format: 32,
sequence: 0,
window: xid,
type_,
data,
};
unsafe {
(self.lib.send_event)(
self.connection,
0,
xid,
0,
ptr::from_ref(&message).cast::<c_char>(),
);
}
self.flush();
}
fn set_owner(&mut self, selection: &str, owner: u32) {
let clipboard = self.atom(selection);
unsafe {
(self.lib.set_selection_owner)(
self.connection,
owner,
clipboard,
ffi::value::CURRENT_TIME,
);
}
self.flush();
let reply = unsafe {
let cookie = (self.lib.get_selection_owner)(self.connection, clipboard);
(self.lib.get_selection_owner_reply)(self.connection, cookie, ptr::null_mut())
};
assert!(
!reply.is_null(),
"the X server did not answer GetSelectionOwner"
);
let settled = unsafe { (*reply).owner };
unsafe { ffi::free_reply(reply) };
assert_eq!(
settled, owner,
"the server did not accept the peer's SetSelectionOwner"
);
}
pub fn read_clipboard(&mut self, target: &str) {
let clipboard = self.atom("CLIPBOARD");
let target = self.atom(target);
let property = self.atom("PEER_SELECTION");
self.incoming = Incoming {
active: true,
..Incoming::default()
};
unsafe {
(self.lib.delete_property)(self.connection, self.window, property);
(self.lib.convert_selection)(
self.connection,
self.window,
clipboard,
target,
property,
ffi::value::CURRENT_TIME,
);
}
self.flush();
}
pub fn read_clipboard_multiple(&mut self, pairs: &[(&str, &str)]) {
let clipboard = self.atom("CLIPBOARD");
let multiple = self.atom("MULTIPLE");
let atom_pair = self.atom("ATOM_PAIR");
let list = self.atom("PEER_MULTIPLE");
let words: Vec<u32> = pairs
.iter()
.flat_map(|(target, property)| [self.atom(target), self.atom(property)])
.collect();
let bytes: Vec<u8> = words.iter().flat_map(|word| word.to_ne_bytes()).collect();
for (_, property) in pairs {
let property = self.atom(property);
self.delete_property(property);
}
self.set_property(self.window, list, atom_pair, 32, &bytes);
self.incoming = Incoming {
active: true,
..Incoming::default()
};
unsafe {
(self.lib.convert_selection)(
self.connection,
self.window,
clipboard,
multiple,
list,
ffi::value::CURRENT_TIME,
);
}
self.flush();
}
pub fn pull_incremental(&mut self, name: &str) {
let property = self.atom(name);
self.pulls.retain(|pull| pull.property != property);
self.pulls.push(Pull {
property,
bytes: Vec::new(),
done: false,
});
self.delete_property(property);
self.flush();
}
#[must_use]
pub fn take_pull(&mut self, name: &str) -> Option<Vec<u8>> {
let property = self.atom(name);
let index = self
.pulls
.iter()
.position(|pull| pull.property == property && pull.done)?;
Some(self.pulls.remove(index).bytes)
}
#[must_use]
pub fn own_property(&mut self, name: &str) -> Option<Vec<u8>> {
let property = self.atom(name);
self.read_property(property).map(|(_, bytes)| bytes)
}
#[must_use]
pub fn has_answer(&self) -> bool {
self.incoming.done.is_some()
}
#[must_use]
pub fn take_read(&mut self) -> Option<Option<Vec<u8>>> {
self.incoming.done.take()
}
pub fn service(&mut self) {
loop {
let event = unsafe { (self.lib.poll_for_event)(self.connection) };
if event.is_null() {
return;
}
let bytes = unsafe { core::slice::from_raw_parts(event.cast::<u8>(), 32).to_vec() };
unsafe { ffi::free_reply(event) };
self.handle(&bytes);
}
}
fn handle(&mut self, raw: &[u8]) {
match raw[0] & 0x7f {
ffi::event::SELECTION_REQUEST => self.answer_request(raw),
ffi::event::SELECTION_NOTIFY => self.collect_notify(raw),
ffi::event::PROPERTY_NOTIFY => self.property(raw),
ffi::event::CLIENT_MESSAGE => self.collect_xdnd(raw),
ffi::event::SELECTION_CLEAR => {
let cleared =
read_wire::<ffi::SelectionClearEvent>(raw).map_or(0, |event| event.selection);
if cleared == self.atom("XdndSelection") {
self.xdnd_offers.clear();
} else {
self.offers.clear();
}
self.outgoing.clear();
}
_ => {}
}
}
fn collect_xdnd(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::ClientMessageEvent>(raw) else {
return;
};
let status = self.atom("XdndStatus");
let finished = self.atom("XdndFinished");
let Some(drag) = self.drag.as_mut() else {
return;
};
if event.type_ == status {
drag.statuses += 1;
drag.status = Some((event.data[1] & 1 != 0, event.data[4]));
} else if event.type_ == finished {
drag.finished = Some((event.data[1] & 1 != 0, event.data[2]));
}
}
fn answer_request(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::SelectionRequestEvent>(raw) else {
return;
};
let targets_atom = self.atom("TARGETS");
let incr_atom = self.atom("INCR");
let xdnd_selection = self.atom("XdndSelection");
let offers = if event.selection == xdnd_selection {
self.xdnd_conversions += 1;
if let Some(drag) = self.drag.as_mut() {
drag.converted_at = Some(event.time);
}
core::mem::take(&mut self.xdnd_offers)
} else {
core::mem::take(&mut self.offers)
};
self.answer_from(&event, &offers, targets_atom, incr_atom);
if event.selection == xdnd_selection {
self.xdnd_offers = offers;
} else {
self.offers = offers;
}
}
fn answer_from(
&mut self,
event: &ffi::SelectionRequestEvent,
offers: &[Offer],
targets_atom: u32,
incr_atom: u32,
) {
let property = if event.property == 0 {
event.target
} else {
event.property
};
let mut answered = 0;
if event.target == targets_atom && !offers.is_empty() {
let mut list = vec![targets_atom];
list.extend(offers.iter().map(|offer| offer.target));
self.set_property(
event.requestor,
property,
ffi::value::ATOM_ATOM,
32,
&list
.iter()
.flat_map(|word| word.to_ne_bytes())
.collect::<Vec<u8>>(),
);
answered = property;
} else if let Some(index) = offers.iter().position(|offer| offer.target == event.target) {
let bytes = offers[index].bytes.clone();
match self.chunk {
Some(chunk) => {
let estimate = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
self.set_property(
event.requestor,
property,
incr_atom,
32,
&estimate.to_ne_bytes(),
);
self.select_property_changes(event.requestor);
self.outgoing.push(Outgoing {
requestor: event.requestor,
property,
target: event.target,
bytes,
offset: 0,
chunk,
terminated: false,
});
}
None => {
self.set_property(event.requestor, property, event.target, 8, &bytes);
}
}
answered = property;
}
let notify = ffi::SelectionNotifyEvent {
response_type: ffi::event::SELECTION_NOTIFY,
pad0: 0,
sequence: 0,
time: event.time,
requestor: event.requestor,
selection: event.selection,
target: event.target,
property: answered,
pad1: [0; 8],
};
unsafe {
(self.lib.send_event)(
self.connection,
0,
event.requestor,
0,
ptr::from_ref(¬ify).cast::<c_char>(),
);
}
self.flush();
}
fn collect_notify(&mut self, raw: &[u8]) {
let Some(event) = read_wire::<ffi::SelectionNotifyEvent>(raw) else {
return;
};
if !self.incoming.active {
return;
}
if event.property == 0 {
self.incoming.active = false;
self.incoming.done = Some(None);
return;
}
let incr = self.atom("INCR");
let Some((type_, bytes)) = self.read_property(event.property) else {
self.incoming.active = false;
self.incoming.done = Some(None);
return;
};
if type_ == incr {
self.incoming.incremental = true;
self.delete_property(event.property);
self.flush();
return;
}
self.incoming.active = false;
self.incoming.done = Some(Some(bytes));
}
fn property(&mut self, raw: &[u8]) {
const NEW_VALUE: u8 = 0;
const DELETED: u8 = 1;
let Some(event) = read_wire::<ffi::PropertyNotifyEvent>(raw) else {
return;
};
self.last_time = event.time;
self.property_notifies = self.property_notifies.wrapping_add(1);
if event.state == NEW_VALUE
&& let Some(index) = self
.pulls
.iter()
.position(|pull| pull.property == event.atom && !pull.done)
{
let bytes = self
.read_property(event.atom)
.map_or_else(Vec::new, |(_, bytes)| bytes);
self.delete_property(event.atom);
self.flush();
if bytes.is_empty() {
self.pulls[index].done = true;
} else {
self.pulls[index].bytes.extend_from_slice(&bytes);
}
return;
}
if event.state == NEW_VALUE && self.incoming.active && self.incoming.incremental {
let bytes = self
.read_property(event.atom)
.map_or_else(Vec::new, |(_, bytes)| bytes);
self.delete_property(event.atom);
self.flush();
if bytes.is_empty() {
self.incoming.active = false;
self.incoming.done = Some(Some(core::mem::take(&mut self.incoming.bytes)));
} else {
self.incoming.bytes.extend_from_slice(&bytes);
}
return;
}
if event.state == DELETED
&& let Some(index) = self
.outgoing
.iter()
.position(|out| out.requestor == event.window && out.property == event.atom)
{
self.push_chunk(index);
}
}
fn push_chunk(&mut self, index: usize) {
let (requestor, property, target, chunk, finished) = {
let out = &mut self.outgoing[index];
if out.terminated {
(out.requestor, out.property, out.target, Vec::new(), true)
} else if out.offset >= out.bytes.len() {
out.terminated = true;
(out.requestor, out.property, out.target, Vec::new(), true)
} else {
let end = out.bytes.len().min(out.offset + out.chunk);
let piece = out.bytes[out.offset..end].to_vec();
out.offset = end;
(out.requestor, out.property, out.target, piece, false)
}
};
self.set_property(requestor, property, target, 8, &chunk);
self.flush();
if finished {
self.outgoing.remove(index);
}
}
fn set_property(&self, window: u32, property: u32, type_: u32, format: u8, data: &[u8]) {
let elements = if format == 32 {
data.len() / 4
} else {
data.len()
};
unsafe {
(self.lib.change_property)(
self.connection,
ffi::value::PROP_MODE_REPLACE,
window,
property,
type_,
format,
u32::try_from(elements).unwrap_or(u32::MAX),
data.as_ptr().cast::<c_void>(),
);
}
}
fn delete_property(&self, property: u32) {
unsafe { (self.lib.delete_property)(self.connection, self.window, property) };
}
fn select_property_changes(&self, xid: u32) {
let values = [ffi::event_mask::PROPERTY_CHANGE];
unsafe {
(self.lib.change_window_attributes)(
self.connection,
xid,
ffi::cw::EVENT_MASK,
values.as_ptr().cast::<c_void>(),
);
}
}
#[must_use]
pub const fn root(&self) -> u32 {
self.root
}
#[must_use]
pub fn window_property(&mut self, xid: u32, name: &str) -> Option<Vec<u8>> {
let property = self.atom(name);
self.property_on(xid, property).map(|(_, bytes)| bytes)
}
const FIND_WINDOW_MAX_DEPTH: usize = 4;
#[must_use]
pub fn find_window(&mut self, wm_class: &str) -> Option<u32> {
self.find_window_at(self.root, wm_class, 0)
}
fn find_window_at(&mut self, xid: u32, wm_class: &str, depth: usize) -> Option<u32> {
if depth > Self::FIND_WINDOW_MAX_DEPTH {
return None;
}
let reply = unsafe {
let cookie = (self.lib.query_tree)(self.connection, xid);
(self.lib.query_tree_reply)(self.connection, cookie, ptr::null_mut())
};
if reply.is_null() {
return None;
}
let mut children = Vec::new();
unsafe {
let list = (self.lib.query_tree_children)(reply);
let length = (self.lib.query_tree_children_length)(reply);
if length > 0 && !list.is_null() {
children.extend_from_slice(core::slice::from_raw_parts(list, length as usize));
}
}
unsafe { ffi::free_reply(reply) };
for child in children {
if self
.window_property(child, "WM_CLASS")
.is_some_and(|bytes| wm_class_instance_is(&bytes, wm_class))
{
return Some(child);
}
if let Some(found) = self.find_window_at(child, wm_class, depth + 1) {
return Some(found);
}
}
None
}
fn read_property(&self, property: u32) -> Option<(u32, Vec<u8>)> {
self.property_on(self.window, property)
}
fn property_on(&self, xid: u32, property: u32) -> Option<(u32, Vec<u8>)> {
let reply = unsafe {
let cookie = (self.lib.get_property)(self.connection, 0, xid, property, 0, 0, 1 << 20);
(self.lib.get_property_reply)(self.connection, cookie, ptr::null_mut())
};
if reply.is_null() {
return None;
}
let answer = unsafe {
let type_ = (*reply).type_;
let length = (self.lib.get_property_value_length)(reply);
let data = (self.lib.get_property_value)(reply);
let bytes = if length > 0 && !data.is_null() {
core::slice::from_raw_parts(data.cast::<u8>(), length as usize).to_vec()
} else {
Vec::new()
};
(type_ == 0)
.then_some(())
.map_or(Some((type_, bytes)), |()| None)
};
unsafe { ffi::free_reply(reply) };
answer
}
}
impl Drop for Peer {
fn drop(&mut self) {
unsafe {
(self.lib.destroy_window)(self.connection, self.window);
(self.lib.flush)(self.connection);
(self.lib.disconnect)(self.connection);
}
}
}
pub const MISSING_XTEST: &CStr = c"libxcb-xtest is required by the X11 e2e harness";
#[must_use]
fn wm_class_instance_is(bytes: &[u8], instance: &str) -> bool {
CStr::from_bytes_until_nul(bytes).is_ok_and(|class| class.to_bytes() == instance.as_bytes())
}