1use crate::{
11 core::{
12 bindings::{KeyBindings, KeyPress, MouseBindings},
13 client::Client,
14 data_types::{Point, Region},
15 screen::Screen,
16 },
17 draw::Color,
18};
19
20use penrose_proc::stubbed_companion_trait;
21
22pub mod atom;
23pub mod event;
24pub mod property;
25
26pub use atom::{
27 Atom, AtomIter, AUTO_FLOAT_WINDOW_TYPES, EWMH_SUPPORTED_ATOMS, UNMANAGED_WINDOW_TYPES,
28};
29pub use event::{
30 ClientEventMask, ClientMessage, ClientMessageData, ClientMessageKind, ConfigureEvent,
31 ExposeEvent, PointerChange, PropertyEvent, XEvent,
32};
33pub use property::{
34 MapState, Prop, WindowAttributes, WindowClass, WindowState, WmHints, WmNormalHints,
35 WmNormalHintsFlags,
36};
37
38pub type Xid = u32;
40
41const WM_NAME: &str = "penrose";
42
43#[derive(thiserror::Error, Debug)]
45pub enum XError {
46 #[error("The underlying connection to the X server is closed")]
48 ConnectionClosed,
49
50 #[error("Invalid client message format: {0} (expected 8, 16 or 32)")]
52 InvalidClientMessageData(u8),
53
54 #[error("The {0} property is not set for client {1}")]
56 MissingProperty(String, Xid),
57
58 #[error("Unhandled error: {0}")]
61 Raw(String),
62
63 #[error(transparent)]
67 Strum(#[from] strum::ParseError),
68
69 #[error("{0} is not a known atom")]
71 UnknownAtom(Xid),
72
73 #[error("{0} is not a known client")]
75 UnknownClient(Xid),
76
77 #[cfg(feature = "xcb")]
84 #[error(transparent)]
85 Xcb(#[from] crate::xcb::XcbError),
86
87 #[cfg(feature = "x11rb")]
91 #[error(transparent)]
92 X11rb(#[from] crate::x11rb::X11rbError),
93}
94
95pub type Result<T> = std::result::Result<T, XError>;
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub enum ClientConfig {
102 BorderPx(u32),
104 Position(Region),
106 StackAbove,
108}
109
110#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
113pub enum ClientAttr {
114 BorderColor(u32),
116 ClientEventMask,
118 RootEventMask,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum KeyPressParseAttempt {
125 KeyPress(KeyPress),
127 XEvent(XEvent),
129}
130
131#[stubbed_companion_trait(doc_hidden = "true")]
133pub trait XAtomQuerier {
134 #[stub(Err(XError::Raw("mocked".into())))]
136 fn atom_name(&self, atom: Xid) -> Result<String>;
137
138 #[stub(Err(XError::Raw("mocked".into())))]
140 fn atom_id(&self, name: &str) -> Result<Xid>;
141}
142
143#[stubbed_companion_trait(doc_hidden = "true")]
145pub trait XState: XAtomQuerier {
146 #[stub(42)]
148 fn root(&self) -> Xid;
149
150 #[stub(Ok(vec![]))]
152 fn current_screens(&self) -> Result<Vec<Screen>>;
153
154 #[stub(Ok(Point::default()))]
156 fn cursor_position(&self) -> Result<Point>;
157
158 #[stub(Ok(()))]
161 fn warp_cursor(&self, win_id: Option<Xid>, screen: &Screen) -> Result<()>;
162
163 #[stub(Ok(Region::default()))]
165 fn client_geometry(&self, id: Xid) -> Result<Region>;
166
167 #[stub(Ok(vec![]))]
169 fn active_clients(&self) -> Result<Vec<Xid>>;
170
171 #[stub(Ok(0))]
173 fn focused_client(&self) -> Result<Xid>;
174}
175
176#[stubbed_companion_trait(doc_hidden = "true")]
178pub trait XEventHandler {
179 #[stub(true)]
181 fn flush(&self) -> bool;
182
183 #[stub(Err(XError::Raw("mocked".into())))]
185 fn wait_for_event(&self) -> Result<XEvent>;
186
187 #[stub(Err(XError::Raw("mocked".into())))]
194 fn send_client_event(&self, msg: ClientMessage) -> Result<()>;
195
196 #[stub(Err(XError::Raw("mocked".into())))]
198 fn build_client_event(&self, kind: ClientMessageKind) -> Result<ClientMessage>;
199}
200
201#[stubbed_companion_trait(doc_hidden = "true")]
203pub trait XClientHandler {
204 #[stub(Ok(()))]
206 fn map_client(&self, id: Xid) -> Result<()>;
207
208 #[stub(Ok(()))]
210 fn unmap_client(&self, id: Xid) -> Result<()>;
211
212 #[stub(Ok(()))]
214 fn destroy_client(&self, id: Xid) -> Result<()>;
215
216 #[stub(Ok(()))]
218 fn kill_client(&self, id: Xid) -> Result<()>;
219
220 #[stub(Ok(()))]
222 fn focus_client(&self, id: Xid) -> Result<()>;
223
224 fn map_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
226 if let Some(c) = win {
227 if !c.mapped {
228 c.mapped = true;
229 self.map_client(c.id())?;
230 }
231 }
232 Ok(())
233 }
234
235 fn unmap_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
237 if let Some(c) = win {
238 if c.mapped {
239 c.mapped = false;
240 self.unmap_client(c.id())?;
241 }
242 }
243 Ok(())
244 }
245}
246
247#[stubbed_companion_trait(doc_hidden = "true")]
249pub trait XClientProperties {
250 #[stub(Ok(vec![]))]
255 fn list_props(&self, id: Xid) -> Result<Vec<String>>;
256
257 #[stub(Err(XError::Raw("mocked".into())))]
261 fn get_prop(&self, id: Xid, name: &str) -> Result<Prop>;
262
263 #[stub(Ok(()))]
265 fn delete_prop(&self, id: Xid, name: &str) -> Result<()>;
266
267 #[stub(Ok(()))]
269 fn change_prop(&self, id: Xid, name: &str, val: Prop) -> Result<()>;
270
271 #[stub(Ok(()))]
277 fn set_client_state(&self, id: Xid, wm_state: WindowState) -> Result<()>;
278
279 fn client_supports_protocol(&self, id: Xid, proto: &str) -> Result<bool> {
288 match self.get_prop(id, Atom::WmProtocols.as_ref()) {
289 Ok(Prop::Atom(protocols)) => Ok(protocols.iter().any(|p| p == proto)),
290 Ok(p) => Err(XError::Raw(format!("Expected atoms, got {:?}", p))),
291 Err(XError::MissingProperty(_, _)) => Ok(false),
292 Err(e) => Err(e),
293 }
294 }
295
296 fn client_accepts_focus(&self, id: Xid) -> bool {
298 match self.get_prop(id, Atom::WmHints.as_ref()) {
299 Ok(Prop::WmHints(WmHints { accepts_input, .. })) => accepts_input,
300 _ => true,
301 }
302 }
303
304 fn toggle_client_fullscreen(&self, id: Xid, client_is_fullscreen: bool) -> Result<()> {
306 let data = if client_is_fullscreen {
307 vec![]
308 } else {
309 vec![Atom::NetWmStateFullscreen.as_ref().to_string()]
310 };
311
312 self.change_prop(id, Atom::NetWmState.as_ref(), Prop::Atom(data))
313 }
314
315 fn client_name(&self, id: Xid) -> Result<String> {
319 match self.get_prop(id, Atom::NetWmName.as_ref()) {
320 Ok(Prop::UTF8String(strs)) if !strs.is_empty() && !strs[0].is_empty() => {
321 Ok(strs[0].clone())
322 }
323
324 _ => match self.get_prop(id, Atom::WmName.as_ref()) {
325 Ok(Prop::UTF8String(strs)) if !strs.is_empty() => Ok(strs[0].clone()),
326 Err(e) => Err(e),
327 _ => Ok(String::new()),
328 },
329 }
330 }
331
332 fn client_should_float(&self, id: Xid, floating_classes: &[&str]) -> bool {
334 if let Ok(prop) = self.get_prop(id, Atom::WmTransientFor.as_ref()) {
335 trace!(?prop, "window is transient: setting to floating state");
336 return true;
337 }
338
339 if let Ok(Prop::UTF8String(strs)) = self.get_prop(id, Atom::WmClass.as_ref()) {
340 if strs.iter().any(|c| floating_classes.contains(&c.as_ref())) {
341 return true;
342 }
343 }
344
345 let float_types: Vec<&str> = AUTO_FLOAT_WINDOW_TYPES.iter().map(|a| a.as_ref()).collect();
346 if let Ok(Prop::Atom(atoms)) = self.get_prop(id, Atom::NetWmWindowType.as_ref()) {
347 atoms.iter().any(|a| float_types.contains(&a.as_ref()))
348 } else {
349 false
350 }
351 }
352}
353
354#[stubbed_companion_trait(doc_hidden = "true")]
356pub trait XClientConfig {
357 #[stub(Ok(()))]
359 fn configure_client(&self, id: Xid, data: &[ClientConfig]) -> Result<()>;
360
361 #[stub(Ok(()))]
363 fn set_client_attributes(&self, id: Xid, data: &[ClientAttr]) -> Result<()>;
364
365 #[stub(Err(XError::Raw("mocked".into())))]
367 fn get_window_attributes(&self, id: Xid) -> Result<WindowAttributes>;
368
369 fn position_client(&self, id: Xid, r: Region, border: u32, stack_above: bool) -> Result<()> {
378 let mut data = vec![ClientConfig::Position(r), ClientConfig::BorderPx(border)];
379 if stack_above {
380 data.push(ClientConfig::StackAbove);
381 }
382 self.configure_client(id, &data)
383 }
384
385 fn raise_client(&self, id: Xid) -> Result<()> {
387 self.configure_client(id, &[ClientConfig::StackAbove])
388 }
389
390 fn set_client_border_color(&self, id: Xid, color: Color) -> Result<()> {
392 self.set_client_attributes(id, &[ClientAttr::BorderColor(color.rgb_u32())])
393 }
394}
395
396#[stubbed_companion_trait(doc_hidden = "true")]
398pub trait XKeyboardHandler {
399 #[stub(Ok(()))]
401 fn grab_keyboard(&self) -> Result<()>;
402
403 #[stub(Ok(()))]
405 fn ungrab_keyboard(&self) -> Result<()>;
406
407 #[stub(Ok(None))]
412 fn next_keypress(&self) -> Result<Option<KeyPressParseAttempt>>;
413
414 #[stub(Err(XError::Raw("mocked".into())))]
417 fn next_keypress_blocking(&self) -> Result<KeyPressParseAttempt>;
418}
419
420#[stubbed_companion_trait(doc_hidden = "true")]
428pub trait XConn:
429 XState + XEventHandler + XClientHandler + XClientProperties + XClientConfig + Sized
430{
431 #[cfg(feature = "serde")]
433 #[stub(Ok(()))]
434 fn hydrate(&mut self) -> Result<()>;
435
436 #[stub(Ok(()))]
445 fn init(&self) -> Result<()>;
446
447 #[stub(0)]
452 fn check_window(&self) -> Xid;
453
454 #[stub(Ok(()))]
456 fn cleanup(&self) -> Result<()>;
457
458 #[stub(Ok(()))]
464 fn grab_keys(
465 &self,
466 key_bindings: &KeyBindings<Self>,
467 mouse_bindings: &MouseBindings<Self>,
468 ) -> Result<()>;
469
470 fn mark_new_client(&self, id: Xid) -> Result<()> {
479 self.set_client_attributes(id, &[ClientAttr::ClientEventMask])
480 }
481
482 fn set_wm_properties(&self, workspaces: &[String]) -> Result<()> {
484 let root = self.root();
485 let check_win = self.check_window();
486 for &win in &[check_win, root] {
487 self.change_prop(
488 win,
489 Atom::NetSupportingWmCheck.as_ref(),
490 Prop::Window(vec![check_win]),
491 )?;
492
493 self.change_prop(
494 win,
495 Atom::WmName.as_ref(),
496 Prop::UTF8String(vec![WM_NAME.into()]),
497 )?;
498 }
499
500 self.change_prop(
502 root,
503 Atom::NetSupported.as_ref(),
504 Prop::Atom(
505 EWMH_SUPPORTED_ATOMS
506 .iter()
507 .map(|a| a.as_ref().to_string())
508 .collect(),
509 ),
510 )?;
511 self.update_desktops(workspaces)?;
512 self.delete_prop(root, Atom::NetClientList.as_ref())?;
513 self.delete_prop(root, Atom::NetClientListStacking.as_ref())
514 }
515
516 fn update_desktops(&self, workspaces: &[String]) -> Result<()> {
518 let root = self.root();
519 self.change_prop(
520 root,
521 Atom::NetNumberOfDesktops.as_ref(),
522 Prop::Cardinal(workspaces.len() as u32),
523 )?;
524 self.change_prop(
525 root,
526 Atom::NetDesktopNames.as_ref(),
527 Prop::UTF8String(workspaces.to_vec()),
528 )
529 }
530
531 fn update_known_clients(&self, clients: &[Xid]) -> Result<()> {
533 let root = self.root();
534 self.change_prop(
535 root,
536 Atom::NetClientList.as_ref(),
537 Prop::Window(clients.to_vec()),
538 )?;
539 self.change_prop(
540 root,
541 Atom::NetClientListStacking.as_ref(),
542 Prop::Window(clients.to_vec()),
543 )
544 }
545
546 fn set_current_workspace(&self, wix: usize) -> Result<()> {
548 self.change_prop(
549 self.root(),
550 Atom::NetCurrentDesktop.as_ref(),
551 Prop::Cardinal(wix as u32),
552 )
553 }
554
555 fn set_root_window_name(&self, name: &str) -> Result<()> {
557 self.change_prop(
558 self.root(),
559 Atom::WmName.as_ref(),
560 Prop::UTF8String(vec![name.to_string()]),
561 )
562 }
563
564 fn set_client_workspace(&self, id: Xid, wix: usize) -> Result<()> {
566 self.change_prop(id, Atom::NetWmDesktop.as_ref(), Prop::Cardinal(wix as u32))
567 }
568
569 #[tracing::instrument(level = "trace", skip(self))]
571 fn is_managed_client(&self, c: &Client) -> bool {
572 let unmanaged_types: Vec<String> = UNMANAGED_WINDOW_TYPES
573 .iter()
574 .map(|t| t.as_ref().to_string())
575 .collect();
576 trace!(ty = ?c.wm_type, "checking window type to see we should manage");
577 return c.wm_type.iter().all(|ty| !unmanaged_types.contains(ty));
578 }
579
580 fn active_managed_clients(&self, floating_classes: &[&str]) -> Result<Vec<Client>> {
582 Ok(self
583 .active_clients()?
584 .into_iter()
585 .filter_map(|id| {
586 let attrs_ok = self.get_window_attributes(id).map_or(true, |a| {
587 !a.override_redirect
588 && a.window_class == WindowClass::InputOutput
589 && a.map_state == MapState::Viewable
590 });
591 if attrs_ok {
592 trace!(id, "parsing existing client");
593 let wix = match self.get_prop(id, Atom::NetWmDesktop.as_ref()) {
594 Ok(Prop::Cardinal(wix)) => wix,
595 _ => 0, };
597
598 let c = Client::new(self, id, wix as usize, floating_classes);
599 if self.is_managed_client(&c) {
600 return Some(c);
601 }
602 }
603 None
604 })
605 .collect())
606 }
607}
608
609#[cfg(test)]
610pub use mock_conn::MockXConn;
611
612#[cfg(test)]
613mod mock_conn {
614 use super::*;
615 use std::{cell::Cell, fmt};
616
617 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
618 pub struct MockXConn {
619 screens: Vec<Screen>,
620 #[cfg_attr(feature = "serde", serde(skip))]
621 events: Cell<Vec<XEvent>>,
622 focused: Cell<Xid>,
623 unmanaged_ids: Vec<Xid>,
624 }
625
626 impl fmt::Debug for MockXConn {
627 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628 f.debug_struct("MockXConn")
629 .field("screens", &self.screens)
630 .field("remaining_events", &self.remaining_events())
631 .field("focused", &self.focused.get())
632 .field("unmanaged_ids", &self.unmanaged_ids)
633 .finish()
634 }
635 }
636
637 impl MockXConn {
638 pub fn new(screens: Vec<Screen>, events: Vec<XEvent>, unmanaged_ids: Vec<Xid>) -> Self {
640 MockXConn {
641 screens,
642 events: Cell::new(events),
643 focused: Cell::new(0),
644 unmanaged_ids,
645 }
646 }
647
648 fn remaining_events(&self) -> Vec<XEvent> {
649 let remaining = self.events.replace(vec![]);
650 self.events.set(remaining.clone());
651 remaining
652 }
653 }
654
655 __impl_stub_xcon! {
656 for MockXConn;
657
658 atom_queries: {
659 fn mock_atom_id(&self, name: &str) -> Result<Xid> {
660 Ok(name.len() as u32)
661 }
662 }
663 client_properties: {
664 fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
665 if name == Atom::WmName.as_ref() || name == Atom::NetWmName.as_ref() {
666 Ok(Prop::UTF8String(vec!["mock name".into()]))
667 } else {
668 Err(XError::MissingProperty(name.into(), id))
669 }
670 }
671 }
672 client_handler: {
673 fn mock_focus_client(&self, id: Xid) -> Result<()> {
674 self.focused.replace(id);
675 Ok(())
676 }
677 }
678 client_config: {}
679 event_handler: {
680 fn mock_wait_for_event(&self) -> Result<XEvent> {
681 let mut remaining = self.events.replace(vec![]);
682 if remaining.is_empty() {
683 return Err(XError::ConnectionClosed)
684 }
685 let next = remaining.remove(0);
686 self.events.set(remaining);
687 Ok(next)
688 }
689
690 fn mock_send_client_event(&self, _: ClientMessage) -> Result<()> {
691 Ok(())
692 }
693 }
694 state: {
695 fn mock_current_screens(&self) -> Result<Vec<Screen>> {
696 Ok(self.screens.clone())
697 }
698
699 fn mock_focused_client(&self) -> Result<Xid> {
700 Ok(self.focused.get())
701 }
702 }
703 conn: {
704 fn mock_is_managed_client(&self, c: &Client) -> bool {
705 !self.unmanaged_ids.contains(&c.id())
706 }
707 }
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 use std::str::FromStr;
716
717 struct WmNameXConn {
718 wm_name: bool,
719 net_wm_name: bool,
720 empty_net_wm_name: bool,
721 }
722
723 impl StubXClientProperties for WmNameXConn {
724 fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
725 match Atom::from_str(name)? {
726 Atom::WmName if self.wm_name => Ok(Prop::UTF8String(vec!["wm_name".into()])),
727 Atom::WmName if self.net_wm_name && self.empty_net_wm_name => {
728 Ok(Prop::UTF8String(vec!["".into()]))
729 }
730 Atom::NetWmName if self.net_wm_name => {
731 Ok(Prop::UTF8String(vec!["net_wm_name".into()]))
732 }
733 Atom::NetWmName if self.empty_net_wm_name => Ok(Prop::UTF8String(vec!["".into()])),
734 _ => Err(XError::MissingProperty(name.into(), id)),
735 }
736 }
737 }
738
739 test_cases! {
740 window_name;
741 args: (wm_name: bool, net_wm_name: bool, empty_net_wm_name: bool, expected: &str);
742
743 case: wm_name_only => (true, false, false, "wm_name");
744 case: net_wm_name_only => (false, true, false, "net_wm_name");
745 case: both_prefers_net => (true, true, false, "net_wm_name");
746 case: net_wm_name_empty => (true, false, true, "wm_name");
747
748 body: {
749 let conn = WmNameXConn {
750 wm_name,
751 net_wm_name,
752 empty_net_wm_name,
753 };
754 assert_eq!(&conn.client_name(42).unwrap(), expected);
755 }
756 }
757}