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)]
67pub struct Atoms {
68 pub xdnd_aware: Atom,
70 pub xdnd_proxy: Atom,
71 pub xdnd_selection: Atom,
72 pub xdnd_enter: Atom,
73 pub xdnd_position: Atom,
74 pub xdnd_status: Atom,
75 pub xdnd_leave: Atom,
76 pub xdnd_drop: Atom,
77 pub xdnd_finished: Atom,
78 pub xdnd_type_list: Atom,
79 pub xdnd_action_copy: Atom,
80 pub xdnd_action_move: Atom,
81 pub xdnd_action_link: Atom,
82 pub xdnd_action_private: Atom,
83 pub xdnd_action_list: Atom,
84
85 pub incr: Atom,
87 pub targets: Atom,
88 pub timestamp: Atom,
89 pub teksilo_transfer: Atom,
92 pub teksilo_timestamp: Atom,
100
101 pub text_uri_list: Atom,
103 pub text_plain_utf8: Atom,
104 pub text_plain: Atom,
105 pub utf8_string: Atom,
106 pub string: Atom,
107
108 pub net_supported: Atom,
110 pub net_supporting_wm_check: Atom,
111 pub net_wm_moveresize: Atom,
112 pub motif_wm_hints: Atom,
113
114 pub net_wm_state: Atom,
116 pub net_wm_state_skip_taskbar: Atom,
117 pub net_wm_state_modal: Atom,
118 pub net_wm_state_hidden: Atom,
119
120 pub net_wm_window_type: Atom,
122 pub net_wm_window_type_dialog: Atom,
123}
124
125impl Atoms {
126 fn intern(conn: &RustConnection) -> Result<Self, X11Error> {
127 const NAMES: &[&[u8]] = &[
130 b"XdndAware",
131 b"XdndProxy",
132 b"XdndSelection",
133 b"XdndEnter",
134 b"XdndPosition",
135 b"XdndStatus",
136 b"XdndLeave",
137 b"XdndDrop",
138 b"XdndFinished",
139 b"XdndTypeList",
140 b"XdndActionCopy",
141 b"XdndActionMove",
142 b"XdndActionLink",
143 b"XdndActionPrivate",
144 b"XdndActionList",
145 b"INCR",
146 b"TARGETS",
147 b"TIMESTAMP",
148 b"_TEKSILO_DND_TRANSFER",
149 b"_TEKSILO_DND_TIMESTAMP",
150 b"text/uri-list",
151 b"text/plain;charset=utf-8",
152 b"text/plain",
153 b"UTF8_STRING",
154 b"STRING",
155 b"_NET_SUPPORTED",
156 b"_NET_SUPPORTING_WM_CHECK",
157 b"_NET_WM_MOVERESIZE",
158 b"_MOTIF_WM_HINTS",
159 b"_NET_WM_STATE",
160 b"_NET_WM_STATE_SKIP_TASKBAR",
161 b"_NET_WM_STATE_MODAL",
162 b"_NET_WM_STATE_HIDDEN",
163 b"_NET_WM_WINDOW_TYPE",
164 b"_NET_WM_WINDOW_TYPE_DIALOG",
165 ];
166
167 let cookies = NAMES
168 .iter()
169 .map(|name| conn.intern_atom(false, name))
170 .collect::<Result<Vec<_>, _>>()?;
171 let mut atoms = Vec::with_capacity(cookies.len());
172 for cookie in cookies {
173 atoms.push(cookie.reply()?.atom);
174 }
175 let mut next = atoms.into_iter();
176 let mut take = || next.next().expect("one atom per interned name");
177
178 Ok(Self {
179 xdnd_aware: take(),
180 xdnd_proxy: take(),
181 xdnd_selection: take(),
182 xdnd_enter: take(),
183 xdnd_position: take(),
184 xdnd_status: take(),
185 xdnd_leave: take(),
186 xdnd_drop: take(),
187 xdnd_finished: take(),
188 xdnd_type_list: take(),
189 xdnd_action_copy: take(),
190 xdnd_action_move: take(),
191 xdnd_action_link: take(),
192 xdnd_action_private: take(),
193 xdnd_action_list: take(),
194 incr: take(),
195 targets: take(),
196 timestamp: take(),
197 teksilo_transfer: take(),
198 teksilo_timestamp: take(),
199 text_uri_list: take(),
200 text_plain_utf8: take(),
201 text_plain: take(),
202 utf8_string: take(),
203 string: take(),
204 net_supported: take(),
205 net_supporting_wm_check: take(),
206 net_wm_moveresize: take(),
207 motif_wm_hints: take(),
208 net_wm_state: take(),
209 net_wm_state_skip_taskbar: take(),
210 net_wm_state_modal: take(),
211 net_wm_state_hidden: take(),
212 net_wm_window_type: take(),
213 net_wm_window_type_dialog: take(),
214 })
215 }
216
217 pub fn preferred_targets(&self) -> [Atom; 5] {
221 [
222 self.text_uri_list,
223 self.text_plain_utf8,
224 self.utf8_string,
225 self.text_plain,
226 self.string,
227 ]
228 }
229
230 pub fn atom_for_mime(&self, mime: &str) -> Option<Atom> {
233 match mime {
234 "text/uri-list" => Some(self.text_uri_list),
235 "text/plain;charset=utf-8" => Some(self.text_plain_utf8),
236 "text/plain" => Some(self.text_plain),
237 "UTF8_STRING" => Some(self.utf8_string),
238 "STRING" => Some(self.string),
239 _ => None,
240 }
241 }
242}
243
244#[derive(Debug, Clone)]
247pub struct PropertyValue {
248 pub type_: Atom,
249 pub format: u8,
250 pub bytes: Vec<u8>,
251}
252
253impl PropertyValue {
254 pub fn as_u32s(&self) -> Vec<u32> {
258 if self.format != 32 {
259 return Vec::new();
260 }
261 self.bytes
262 .as_chunks::<4>()
263 .0
264 .iter()
265 .map(|chunk| u32::from_ne_bytes(*chunk))
266 .collect()
267 }
268
269 pub fn as_u32(&self) -> Option<u32> {
271 self.as_u32s().first().copied()
272 }
273}
274
275pub struct X11Connection {
281 conn: RustConnection,
282 root: Window,
283 atoms: Atoms,
284 pending: RefCell<VecDeque<Event>>,
289}
290
291impl X11Connection {
292 pub fn open() -> Result<Self, X11Error> {
294 let (conn, screen_num) = x11rb::connect(None)?;
295 let root = conn.setup().roots[screen_num].root;
296 let atoms = Atoms::intern(&conn)?;
297 Ok(Self {
298 conn,
299 root,
300 atoms,
301 pending: RefCell::new(VecDeque::new()),
302 })
303 }
304
305 pub fn conn(&self) -> &RustConnection {
306 &self.conn
307 }
308
309 pub fn root(&self) -> Window {
310 self.root
311 }
312
313 pub fn atoms(&self) -> &Atoms {
314 &self.atoms
315 }
316
317 pub fn flush(&self) -> Result<(), X11Error> {
318 self.conn.flush()?;
319 Ok(())
320 }
321
322 pub fn get_property_full(
330 &self,
331 window: Window,
332 property: Atom,
333 type_: Atom,
334 ) -> Result<Option<PropertyValue>, X11Error> {
335 const CHUNK_UNITS: u32 = 1024;
337
338 let mut offset = 0u32;
339 let mut out: Option<PropertyValue> = None;
340 loop {
341 let reply = self
342 .conn
343 .get_property(false, window, property, type_, offset, CHUNK_UNITS)?
344 .reply()?;
345 if reply.type_ == x11rb::NONE {
346 return Ok(out);
347 }
348 let more = reply.bytes_after > 0;
349 let format = reply.format;
350 let reply_type = reply.type_;
351 let len = reply.value.len();
352 match &mut out {
353 Some(acc) => acc.bytes.extend_from_slice(&reply.value),
354 None => {
355 out = Some(PropertyValue {
356 type_: reply_type,
357 format,
358 bytes: reply.value,
359 })
360 }
361 }
362 if !more || len == 0 {
363 return Ok(out);
364 }
365 offset += (len as u32).div_ceil(4);
367 }
368 }
369
370 pub fn get_property_and_delete(
374 &self,
375 window: Window,
376 property: Atom,
377 ) -> Result<Option<PropertyValue>, X11Error> {
378 let reply = self
382 .conn
383 .get_property(true, window, property, AtomEnum::ANY, 0, u32::MAX / 4)?
384 .reply()?;
385 if reply.type_ == x11rb::NONE {
386 return Ok(None);
387 }
388 Ok(Some(PropertyValue {
389 type_: reply.type_,
390 format: reply.format,
391 bytes: reply.value,
392 }))
393 }
394
395 pub fn set_property32(
397 &self,
398 window: Window,
399 property: Atom,
400 type_: Atom,
401 data: &[u32],
402 ) -> Result<(), X11Error> {
403 self.conn
404 .change_property32(PropMode::REPLACE, window, property, type_, data)?
405 .check()?;
406 Ok(())
407 }
408
409 pub fn set_property8(
411 &self,
412 window: Window,
413 property: Atom,
414 type_: Atom,
415 data: &[u8],
416 ) -> Result<(), X11Error> {
417 self.conn
418 .change_property8(PropMode::REPLACE, window, property, type_, data)?
419 .check()?;
420 Ok(())
421 }
422
423 pub fn fetch_timestamp(&self, window: Window) -> Result<Timestamp, X11Error> {
434 self.conn
435 .change_property8(
436 PropMode::APPEND,
437 window,
438 self.atoms.teksilo_timestamp,
439 AtomEnum::STRING,
440 &[],
441 )?
442 .check()?;
443 self.conn.flush()?;
444
445 for _ in 0..64 {
447 let event = self.conn.wait_for_event()?;
448 if let Event::PropertyNotify(ref notify) = event
449 && notify.window == window
450 && notify.atom == self.atoms.teksilo_timestamp
451 {
452 return Ok(notify.time);
453 }
454 self.pending.borrow_mut().push_back(event);
455 }
456 Err(X11Error::Timeout(
457 "a PropertyNotify carrying a server timestamp",
458 ))
459 }
460
461 pub fn next_event(&self) -> Result<Event, X11Error> {
464 if let Some(event) = self.pending.borrow_mut().pop_front() {
465 return Ok(event);
466 }
467 Ok(self.conn.wait_for_event()?)
468 }
469
470 pub fn poll_event(&self) -> Result<Option<Event>, X11Error> {
472 if let Some(event) = self.pending.borrow_mut().pop_front() {
473 return Ok(Some(event));
474 }
475 Ok(self.conn.poll_for_event()?)
476 }
477
478 pub fn send_client_message(
486 &self,
487 destination: Window,
488 window_field: Window,
489 type_: Atom,
490 data: [u32; 5],
491 mask: EventMask,
492 ) -> Result<(), X11Error> {
493 use x11rb::protocol::xproto::ClientMessageEvent;
494
495 let event = ClientMessageEvent::new(32, window_field, type_, data);
496 self.conn
497 .send_event(false, destination, mask, event)?
498 .check()?;
499 Ok(())
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn property_value_reads_32_bit_words() {
509 let value = PropertyValue {
510 type_: 1,
511 format: 32,
512 bytes: 5u32
513 .to_ne_bytes()
514 .into_iter()
515 .chain(7u32.to_ne_bytes())
516 .collect(),
517 };
518 assert_eq!(value.as_u32s(), vec![5, 7]);
519 assert_eq!(value.as_u32(), Some(5));
520 }
521
522 #[test]
523 fn property_value_rejects_a_mismatched_format() {
524 let value = PropertyValue {
527 type_: 1,
528 format: 8,
529 bytes: vec![1, 2, 3, 4],
530 };
531 assert!(value.as_u32s().is_empty());
532 assert_eq!(value.as_u32(), None);
533 }
534
535 #[test]
536 fn property_value_ignores_a_trailing_partial_word() {
537 let value = PropertyValue {
538 type_: 1,
539 format: 32,
540 bytes: vec![1, 2, 3, 4, 5],
541 };
542 assert_eq!(value.as_u32s().len(), 1);
543 }
544}