1use std::cell::RefCell;
22use std::collections::VecDeque;
23
24use x11rb::connection::Connection;
25use x11rb::errors::{ConnectError, ConnectionError, ReplyError};
26use x11rb::protocol::Event;
27use x11rb::protocol::xproto::{
28 Atom, AtomEnum, ConnectionExt as _, EventMask, PropMode, Timestamp, Window,
29};
30use x11rb::rust_connection::RustConnection;
31use x11rb::wrapper::ConnectionExt as _;
32
33#[derive(Debug, thiserror::Error)]
35pub enum X11Error {
36 #[error("could not connect to the X server: {0}")]
37 Connect(#[from] ConnectError),
38 #[error("X11 connection error: {0}")]
39 Connection(#[from] ConnectionError),
40 #[error("X11 request failed: {0}")]
41 Reply(#[from] ReplyError),
42 #[error("could not allocate an X11 resource id: {0}")]
43 IdAllocation(#[from] x11rb::errors::ReplyOrIdError),
44 #[error("timed out waiting for {0}")]
45 Timeout(&'static str),
46}
47
48pub fn ignore_errors<C: x11rb::connection::RequestConnection>(
54 result: Result<x11rb::cookie::VoidCookie<'_, C>, ConnectionError>,
55) {
56 if let Ok(cookie) = result {
57 let _ = cookie.check();
58 }
59}
60
61#[derive(Debug, Clone)]
66pub struct Atoms {
67 pub xdnd_aware: Atom,
69 pub xdnd_proxy: Atom,
70 pub xdnd_selection: Atom,
71 pub xdnd_enter: Atom,
72 pub xdnd_position: Atom,
73 pub xdnd_status: Atom,
74 pub xdnd_leave: Atom,
75 pub xdnd_drop: Atom,
76 pub xdnd_finished: Atom,
77 pub xdnd_type_list: Atom,
78 pub xdnd_action_copy: Atom,
79 pub xdnd_action_move: Atom,
80 pub xdnd_action_link: Atom,
81 pub xdnd_action_private: Atom,
82 pub xdnd_action_list: Atom,
83
84 pub incr: Atom,
86 pub targets: Atom,
87 pub timestamp: Atom,
88 pub teksilo_transfer: Atom,
91 pub teksilo_timestamp: Atom,
99
100 pub text_uri_list: Atom,
102 pub text_plain_utf8: Atom,
103 pub text_plain: Atom,
104 pub utf8_string: Atom,
105 pub string: Atom,
106
107 pub net_supported: Atom,
109 pub net_supporting_wm_check: Atom,
110 pub net_wm_moveresize: Atom,
111 pub motif_wm_hints: Atom,
112}
113
114impl Atoms {
115 fn intern(conn: &RustConnection) -> Result<Self, X11Error> {
116 const NAMES: &[&[u8]] = &[
119 b"XdndAware",
120 b"XdndProxy",
121 b"XdndSelection",
122 b"XdndEnter",
123 b"XdndPosition",
124 b"XdndStatus",
125 b"XdndLeave",
126 b"XdndDrop",
127 b"XdndFinished",
128 b"XdndTypeList",
129 b"XdndActionCopy",
130 b"XdndActionMove",
131 b"XdndActionLink",
132 b"XdndActionPrivate",
133 b"XdndActionList",
134 b"INCR",
135 b"TARGETS",
136 b"TIMESTAMP",
137 b"_TEKSILO_DND_TRANSFER",
138 b"_TEKSILO_DND_TIMESTAMP",
139 b"text/uri-list",
140 b"text/plain;charset=utf-8",
141 b"text/plain",
142 b"UTF8_STRING",
143 b"STRING",
144 b"_NET_SUPPORTED",
145 b"_NET_SUPPORTING_WM_CHECK",
146 b"_NET_WM_MOVERESIZE",
147 b"_MOTIF_WM_HINTS",
148 ];
149
150 let cookies = NAMES
151 .iter()
152 .map(|name| conn.intern_atom(false, name))
153 .collect::<Result<Vec<_>, _>>()?;
154 let mut atoms = Vec::with_capacity(cookies.len());
155 for cookie in cookies {
156 atoms.push(cookie.reply()?.atom);
157 }
158 let mut next = atoms.into_iter();
159 let mut take = || next.next().expect("one atom per interned name");
160
161 Ok(Self {
162 xdnd_aware: take(),
163 xdnd_proxy: take(),
164 xdnd_selection: take(),
165 xdnd_enter: take(),
166 xdnd_position: take(),
167 xdnd_status: take(),
168 xdnd_leave: take(),
169 xdnd_drop: take(),
170 xdnd_finished: take(),
171 xdnd_type_list: take(),
172 xdnd_action_copy: take(),
173 xdnd_action_move: take(),
174 xdnd_action_link: take(),
175 xdnd_action_private: take(),
176 xdnd_action_list: take(),
177 incr: take(),
178 targets: take(),
179 timestamp: take(),
180 teksilo_transfer: take(),
181 teksilo_timestamp: take(),
182 text_uri_list: take(),
183 text_plain_utf8: take(),
184 text_plain: take(),
185 utf8_string: take(),
186 string: take(),
187 net_supported: take(),
188 net_supporting_wm_check: take(),
189 net_wm_moveresize: take(),
190 motif_wm_hints: take(),
191 })
192 }
193
194 pub fn preferred_targets(&self) -> [Atom; 5] {
198 [
199 self.text_uri_list,
200 self.text_plain_utf8,
201 self.utf8_string,
202 self.text_plain,
203 self.string,
204 ]
205 }
206
207 pub fn atom_for_mime(&self, mime: &str) -> Option<Atom> {
210 match mime {
211 "text/uri-list" => Some(self.text_uri_list),
212 "text/plain;charset=utf-8" => Some(self.text_plain_utf8),
213 "text/plain" => Some(self.text_plain),
214 "UTF8_STRING" => Some(self.utf8_string),
215 "STRING" => Some(self.string),
216 _ => None,
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
224pub struct PropertyValue {
225 pub type_: Atom,
226 pub format: u8,
227 pub bytes: Vec<u8>,
228}
229
230impl PropertyValue {
231 pub fn as_u32s(&self) -> Vec<u32> {
235 if self.format != 32 {
236 return Vec::new();
237 }
238 self.bytes
239 .as_chunks::<4>()
240 .0
241 .iter()
242 .map(|chunk| u32::from_ne_bytes(*chunk))
243 .collect()
244 }
245
246 pub fn as_u32(&self) -> Option<u32> {
248 self.as_u32s().first().copied()
249 }
250}
251
252pub struct X11Connection {
258 conn: RustConnection,
259 root: Window,
260 atoms: Atoms,
261 pending: RefCell<VecDeque<Event>>,
266}
267
268impl X11Connection {
269 pub fn open() -> Result<Self, X11Error> {
271 let (conn, screen_num) = x11rb::connect(None)?;
272 let root = conn.setup().roots[screen_num].root;
273 let atoms = Atoms::intern(&conn)?;
274 Ok(Self {
275 conn,
276 root,
277 atoms,
278 pending: RefCell::new(VecDeque::new()),
279 })
280 }
281
282 pub fn conn(&self) -> &RustConnection {
283 &self.conn
284 }
285
286 pub fn root(&self) -> Window {
287 self.root
288 }
289
290 pub fn atoms(&self) -> &Atoms {
291 &self.atoms
292 }
293
294 pub fn flush(&self) -> Result<(), X11Error> {
295 self.conn.flush()?;
296 Ok(())
297 }
298
299 pub fn get_property_full(
307 &self,
308 window: Window,
309 property: Atom,
310 type_: Atom,
311 ) -> Result<Option<PropertyValue>, X11Error> {
312 const CHUNK_UNITS: u32 = 1024;
314
315 let mut offset = 0u32;
316 let mut out: Option<PropertyValue> = None;
317 loop {
318 let reply = self
319 .conn
320 .get_property(false, window, property, type_, offset, CHUNK_UNITS)?
321 .reply()?;
322 if reply.type_ == x11rb::NONE {
323 return Ok(out);
324 }
325 let more = reply.bytes_after > 0;
326 let format = reply.format;
327 let reply_type = reply.type_;
328 let len = reply.value.len();
329 match &mut out {
330 Some(acc) => acc.bytes.extend_from_slice(&reply.value),
331 None => {
332 out = Some(PropertyValue {
333 type_: reply_type,
334 format,
335 bytes: reply.value,
336 })
337 }
338 }
339 if !more || len == 0 {
340 return Ok(out);
341 }
342 offset += (len as u32).div_ceil(4);
344 }
345 }
346
347 pub fn get_property_and_delete(
351 &self,
352 window: Window,
353 property: Atom,
354 ) -> Result<Option<PropertyValue>, X11Error> {
355 let reply = self
359 .conn
360 .get_property(true, window, property, AtomEnum::ANY, 0, u32::MAX / 4)?
361 .reply()?;
362 if reply.type_ == x11rb::NONE {
363 return Ok(None);
364 }
365 Ok(Some(PropertyValue {
366 type_: reply.type_,
367 format: reply.format,
368 bytes: reply.value,
369 }))
370 }
371
372 pub fn set_property32(
374 &self,
375 window: Window,
376 property: Atom,
377 type_: Atom,
378 data: &[u32],
379 ) -> Result<(), X11Error> {
380 self.conn
381 .change_property32(PropMode::REPLACE, window, property, type_, data)?
382 .check()?;
383 Ok(())
384 }
385
386 pub fn set_property8(
388 &self,
389 window: Window,
390 property: Atom,
391 type_: Atom,
392 data: &[u8],
393 ) -> Result<(), X11Error> {
394 self.conn
395 .change_property8(PropMode::REPLACE, window, property, type_, data)?
396 .check()?;
397 Ok(())
398 }
399
400 pub fn fetch_timestamp(&self, window: Window) -> Result<Timestamp, X11Error> {
411 self.conn
412 .change_property8(
413 PropMode::APPEND,
414 window,
415 self.atoms.teksilo_timestamp,
416 AtomEnum::STRING,
417 &[],
418 )?
419 .check()?;
420 self.conn.flush()?;
421
422 for _ in 0..64 {
424 let event = self.conn.wait_for_event()?;
425 if let Event::PropertyNotify(ref notify) = event
426 && notify.window == window
427 && notify.atom == self.atoms.teksilo_timestamp
428 {
429 return Ok(notify.time);
430 }
431 self.pending.borrow_mut().push_back(event);
432 }
433 Err(X11Error::Timeout(
434 "a PropertyNotify carrying a server timestamp",
435 ))
436 }
437
438 pub fn next_event(&self) -> Result<Event, X11Error> {
441 if let Some(event) = self.pending.borrow_mut().pop_front() {
442 return Ok(event);
443 }
444 Ok(self.conn.wait_for_event()?)
445 }
446
447 pub fn poll_event(&self) -> Result<Option<Event>, X11Error> {
449 if let Some(event) = self.pending.borrow_mut().pop_front() {
450 return Ok(Some(event));
451 }
452 Ok(self.conn.poll_for_event()?)
453 }
454
455 pub fn send_client_message(
463 &self,
464 destination: Window,
465 window_field: Window,
466 type_: Atom,
467 data: [u32; 5],
468 mask: EventMask,
469 ) -> Result<(), X11Error> {
470 use x11rb::protocol::xproto::ClientMessageEvent;
471
472 let event = ClientMessageEvent::new(32, window_field, type_, data);
473 self.conn
474 .send_event(false, destination, mask, event)?
475 .check()?;
476 Ok(())
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn property_value_reads_32_bit_words() {
486 let value = PropertyValue {
487 type_: 1,
488 format: 32,
489 bytes: 5u32
490 .to_ne_bytes()
491 .into_iter()
492 .chain(7u32.to_ne_bytes())
493 .collect(),
494 };
495 assert_eq!(value.as_u32s(), vec![5, 7]);
496 assert_eq!(value.as_u32(), Some(5));
497 }
498
499 #[test]
500 fn property_value_rejects_a_mismatched_format() {
501 let value = PropertyValue {
504 type_: 1,
505 format: 8,
506 bytes: vec![1, 2, 3, 4],
507 };
508 assert!(value.as_u32s().is_empty());
509 assert_eq!(value.as_u32(), None);
510 }
511
512 #[test]
513 fn property_value_ignores_a_trailing_partial_word() {
514 let value = PropertyValue {
515 type_: 1,
516 format: 32,
517 bytes: vec![1, 2, 3, 4, 5],
518 };
519 assert_eq!(value.as_u32s().len(), 1);
520 }
521}