use crate::{
core::{
bindings::{KeyBindings, KeyPress, MouseBindings},
client::Client,
data_types::{Point, Region},
screen::Screen,
},
draw::Color,
};
use penrose_proc::stubbed_companion_trait;
pub mod atom;
pub mod event;
pub mod property;
pub use atom::{
Atom, AtomIter, AUTO_FLOAT_WINDOW_TYPES, EWMH_SUPPORTED_ATOMS, UNMANAGED_WINDOW_TYPES,
};
pub use event::{
ClientEventMask, ClientMessage, ClientMessageData, ClientMessageKind, ConfigureEvent,
ExposeEvent, PointerChange, PropertyEvent, XEvent,
};
pub use property::{
MapState, Prop, WindowAttributes, WindowClass, WindowState, WmHints, WmNormalHints,
WmNormalHintsFlags,
};
pub type Xid = u32;
const WM_NAME: &str = "penrose";
#[derive(thiserror::Error, Debug)]
pub enum XError {
#[error("The underlying connection to the X server is closed")]
ConnectionClosed,
#[error("Invalid client message format: {0} (expected 8, 16 or 32)")]
InvalidClientMessageData(u8),
#[error("The {0} property is not set for client {1}")]
MissingProperty(String, Xid),
#[error("Unhandled error: {0}")]
Raw(String),
#[error(transparent)]
Strum(#[from] strum::ParseError),
#[error("{0} is not a known atom")]
UnknownAtom(Xid),
#[error("{0} is not a known client")]
UnknownClient(Xid),
#[cfg(feature = "xcb")]
#[error(transparent)]
Xcb(#[from] crate::xcb::XcbError),
#[cfg(feature = "x11rb")]
#[error(transparent)]
X11rb(#[from] crate::x11rb::X11rbError),
}
pub type Result<T> = std::result::Result<T, XError>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ClientConfig {
BorderPx(u32),
Position(Region),
StackAbove,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ClientAttr {
BorderColor(u32),
ClientEventMask,
RootEventMask,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyPressParseAttempt {
KeyPress(KeyPress),
XEvent(XEvent),
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XAtomQuerier {
#[stub(Err(XError::Raw("mocked".into())))]
fn atom_name(&self, atom: Xid) -> Result<String>;
#[stub(Err(XError::Raw("mocked".into())))]
fn atom_id(&self, name: &str) -> Result<Xid>;
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XState: XAtomQuerier {
#[stub(42)]
fn root(&self) -> Xid;
#[stub(Ok(vec![]))]
fn current_screens(&self) -> Result<Vec<Screen>>;
#[stub(Ok(Point::default()))]
fn cursor_position(&self) -> Result<Point>;
#[stub(Ok(()))]
fn warp_cursor(&self, win_id: Option<Xid>, screen: &Screen) -> Result<()>;
#[stub(Ok(Region::default()))]
fn client_geometry(&self, id: Xid) -> Result<Region>;
#[stub(Ok(vec![]))]
fn active_clients(&self) -> Result<Vec<Xid>>;
#[stub(Ok(0))]
fn focused_client(&self) -> Result<Xid>;
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XEventHandler {
#[stub(true)]
fn flush(&self) -> bool;
#[stub(Err(XError::Raw("mocked".into())))]
fn wait_for_event(&self) -> Result<XEvent>;
#[stub(Err(XError::Raw("mocked".into())))]
fn send_client_event(&self, msg: ClientMessage) -> Result<()>;
#[stub(Err(XError::Raw("mocked".into())))]
fn build_client_event(&self, kind: ClientMessageKind) -> Result<ClientMessage>;
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XClientHandler {
#[stub(Ok(()))]
fn map_client(&self, id: Xid) -> Result<()>;
#[stub(Ok(()))]
fn unmap_client(&self, id: Xid) -> Result<()>;
#[stub(Ok(()))]
fn destroy_client(&self, id: Xid) -> Result<()>;
#[stub(Ok(()))]
fn kill_client(&self, id: Xid) -> Result<()>;
#[stub(Ok(()))]
fn focus_client(&self, id: Xid) -> Result<()>;
fn map_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
if let Some(c) = win {
if !c.mapped {
c.mapped = true;
self.map_client(c.id())?;
}
}
Ok(())
}
fn unmap_client_if_needed(&self, win: Option<&mut Client>) -> Result<()> {
if let Some(c) = win {
if c.mapped {
c.mapped = false;
self.unmap_client(c.id())?;
}
}
Ok(())
}
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XClientProperties {
#[stub(Ok(vec![]))]
fn list_props(&self, id: Xid) -> Result<Vec<String>>;
#[stub(Err(XError::Raw("mocked".into())))]
fn get_prop(&self, id: Xid, name: &str) -> Result<Prop>;
#[stub(Ok(()))]
fn delete_prop(&self, id: Xid, name: &str) -> Result<()>;
#[stub(Ok(()))]
fn change_prop(&self, id: Xid, name: &str, val: Prop) -> Result<()>;
#[stub(Ok(()))]
fn set_client_state(&self, id: Xid, wm_state: WindowState) -> Result<()>;
fn client_supports_protocol(&self, id: Xid, proto: &str) -> Result<bool> {
match self.get_prop(id, Atom::WmProtocols.as_ref()) {
Ok(Prop::Atom(protocols)) => Ok(protocols.iter().any(|p| p == proto)),
Ok(p) => Err(XError::Raw(format!("Expected atoms, got {:?}", p))),
Err(XError::MissingProperty(_, _)) => Ok(false),
Err(e) => Err(e),
}
}
fn client_accepts_focus(&self, id: Xid) -> bool {
match self.get_prop(id, Atom::WmHints.as_ref()) {
Ok(Prop::WmHints(WmHints { accepts_input, .. })) => accepts_input,
_ => true,
}
}
fn toggle_client_fullscreen(&self, id: Xid, client_is_fullscreen: bool) -> Result<()> {
let data = if client_is_fullscreen {
vec![]
} else {
vec![Atom::NetWmStateFullscreen.as_ref().to_string()]
};
self.change_prop(id, Atom::NetWmState.as_ref(), Prop::Atom(data))
}
fn client_name(&self, id: Xid) -> Result<String> {
match self.get_prop(id, Atom::NetWmName.as_ref()) {
Ok(Prop::UTF8String(strs)) if !strs.is_empty() && !strs[0].is_empty() => {
Ok(strs[0].clone())
}
_ => match self.get_prop(id, Atom::WmName.as_ref()) {
Ok(Prop::UTF8String(strs)) if !strs.is_empty() => Ok(strs[0].clone()),
Err(e) => Err(e),
_ => Ok(String::new()),
},
}
}
fn client_should_float(&self, id: Xid, floating_classes: &[&str]) -> bool {
if let Ok(prop) = self.get_prop(id, Atom::WmTransientFor.as_ref()) {
trace!(?prop, "window is transient: setting to floating state");
return true;
}
if let Ok(Prop::UTF8String(strs)) = self.get_prop(id, Atom::WmClass.as_ref()) {
if strs.iter().any(|c| floating_classes.contains(&c.as_ref())) {
return true;
}
}
let float_types: Vec<&str> = AUTO_FLOAT_WINDOW_TYPES.iter().map(|a| a.as_ref()).collect();
if let Ok(Prop::Atom(atoms)) = self.get_prop(id, Atom::NetWmWindowType.as_ref()) {
atoms.iter().any(|a| float_types.contains(&a.as_ref()))
} else {
false
}
}
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XClientConfig {
#[stub(Ok(()))]
fn configure_client(&self, id: Xid, data: &[ClientConfig]) -> Result<()>;
#[stub(Ok(()))]
fn set_client_attributes(&self, id: Xid, data: &[ClientAttr]) -> Result<()>;
#[stub(Err(XError::Raw("mocked".into())))]
fn get_window_attributes(&self, id: Xid) -> Result<WindowAttributes>;
fn position_client(&self, id: Xid, r: Region, border: u32, stack_above: bool) -> Result<()> {
let mut data = vec![ClientConfig::Position(r), ClientConfig::BorderPx(border)];
if stack_above {
data.push(ClientConfig::StackAbove);
}
self.configure_client(id, &data)
}
fn raise_client(&self, id: Xid) -> Result<()> {
self.configure_client(id, &[ClientConfig::StackAbove])
}
fn set_client_border_color(&self, id: Xid, color: Color) -> Result<()> {
self.set_client_attributes(id, &[ClientAttr::BorderColor(color.rgb_u32())])
}
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XKeyboardHandler {
#[stub(Ok(()))]
fn grab_keyboard(&self) -> Result<()>;
#[stub(Ok(()))]
fn ungrab_keyboard(&self) -> Result<()>;
#[stub(Ok(None))]
fn next_keypress(&self) -> Result<Option<KeyPressParseAttempt>>;
#[stub(Err(XError::Raw("mocked".into())))]
fn next_keypress_blocking(&self) -> Result<KeyPressParseAttempt>;
}
#[stubbed_companion_trait(doc_hidden = "true")]
pub trait XConn:
XState + XEventHandler + XClientHandler + XClientProperties + XClientConfig + Sized
{
#[cfg(feature = "serde")]
#[stub(Ok(()))]
fn hydrate(&mut self) -> Result<()>;
#[stub(Ok(()))]
fn init(&self) -> Result<()>;
#[stub(0)]
fn check_window(&self) -> Xid;
#[stub(Ok(()))]
fn cleanup(&self) -> Result<()>;
#[stub(Ok(()))]
fn grab_keys(
&self,
key_bindings: &KeyBindings<Self>,
mouse_bindings: &MouseBindings<Self>,
) -> Result<()>;
fn mark_new_client(&self, id: Xid) -> Result<()> {
self.set_client_attributes(id, &[ClientAttr::ClientEventMask])
}
fn set_wm_properties(&self, workspaces: &[String]) -> Result<()> {
let root = self.root();
let check_win = self.check_window();
for &win in &[check_win, root] {
self.change_prop(
win,
Atom::NetSupportingWmCheck.as_ref(),
Prop::Window(vec![check_win]),
)?;
self.change_prop(
win,
Atom::WmName.as_ref(),
Prop::UTF8String(vec![WM_NAME.into()]),
)?;
}
self.change_prop(
root,
Atom::NetSupported.as_ref(),
Prop::Atom(
EWMH_SUPPORTED_ATOMS
.iter()
.map(|a| a.as_ref().to_string())
.collect(),
),
)?;
self.update_desktops(workspaces)?;
self.delete_prop(root, Atom::NetClientList.as_ref())?;
self.delete_prop(root, Atom::NetClientListStacking.as_ref())
}
fn update_desktops(&self, workspaces: &[String]) -> Result<()> {
let root = self.root();
self.change_prop(
root,
Atom::NetNumberOfDesktops.as_ref(),
Prop::Cardinal(workspaces.len() as u32),
)?;
self.change_prop(
root,
Atom::NetDesktopNames.as_ref(),
Prop::UTF8String(workspaces.to_vec()),
)
}
fn update_known_clients(&self, clients: &[Xid]) -> Result<()> {
let root = self.root();
self.change_prop(
root,
Atom::NetClientList.as_ref(),
Prop::Window(clients.to_vec()),
)?;
self.change_prop(
root,
Atom::NetClientListStacking.as_ref(),
Prop::Window(clients.to_vec()),
)
}
fn set_current_workspace(&self, wix: usize) -> Result<()> {
self.change_prop(
self.root(),
Atom::NetCurrentDesktop.as_ref(),
Prop::Cardinal(wix as u32),
)
}
fn set_root_window_name(&self, name: &str) -> Result<()> {
self.change_prop(
self.root(),
Atom::WmName.as_ref(),
Prop::UTF8String(vec![name.to_string()]),
)
}
fn set_client_workspace(&self, id: Xid, wix: usize) -> Result<()> {
self.change_prop(id, Atom::NetWmDesktop.as_ref(), Prop::Cardinal(wix as u32))
}
#[tracing::instrument(level = "trace", skip(self))]
fn is_managed_client(&self, c: &Client) -> bool {
let unmanaged_types: Vec<String> = UNMANAGED_WINDOW_TYPES
.iter()
.map(|t| t.as_ref().to_string())
.collect();
trace!(ty = ?c.wm_type, "checking window type to see we should manage");
return c.wm_type.iter().all(|ty| !unmanaged_types.contains(ty));
}
fn active_managed_clients(&self, floating_classes: &[&str]) -> Result<Vec<Client>> {
Ok(self
.active_clients()?
.into_iter()
.filter_map(|id| {
let attrs_ok = self.get_window_attributes(id).map_or(true, |a| {
!a.override_redirect
&& a.window_class == WindowClass::InputOutput
&& a.map_state == MapState::Viewable
});
if attrs_ok {
trace!(id, "parsing existing client");
let wix = match self.get_prop(id, Atom::NetWmDesktop.as_ref()) {
Ok(Prop::Cardinal(wix)) => wix,
_ => 0, };
let c = Client::new(self, id, wix as usize, floating_classes);
if self.is_managed_client(&c) {
return Some(c);
}
}
None
})
.collect())
}
}
#[cfg(test)]
pub use mock_conn::MockXConn;
#[cfg(test)]
mod mock_conn {
use super::*;
use std::{cell::Cell, fmt};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MockXConn {
screens: Vec<Screen>,
#[cfg_attr(feature = "serde", serde(skip))]
events: Cell<Vec<XEvent>>,
focused: Cell<Xid>,
unmanaged_ids: Vec<Xid>,
}
impl fmt::Debug for MockXConn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MockXConn")
.field("screens", &self.screens)
.field("remaining_events", &self.remaining_events())
.field("focused", &self.focused.get())
.field("unmanaged_ids", &self.unmanaged_ids)
.finish()
}
}
impl MockXConn {
pub fn new(screens: Vec<Screen>, events: Vec<XEvent>, unmanaged_ids: Vec<Xid>) -> Self {
MockXConn {
screens,
events: Cell::new(events),
focused: Cell::new(0),
unmanaged_ids,
}
}
fn remaining_events(&self) -> Vec<XEvent> {
let remaining = self.events.replace(vec![]);
self.events.set(remaining.clone());
remaining
}
}
__impl_stub_xcon! {
for MockXConn;
atom_queries: {
fn mock_atom_id(&self, name: &str) -> Result<Xid> {
Ok(name.len() as u32)
}
}
client_properties: {
fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
if name == Atom::WmName.as_ref() || name == Atom::NetWmName.as_ref() {
Ok(Prop::UTF8String(vec!["mock name".into()]))
} else {
Err(XError::MissingProperty(name.into(), id))
}
}
}
client_handler: {
fn mock_focus_client(&self, id: Xid) -> Result<()> {
self.focused.replace(id);
Ok(())
}
}
client_config: {}
event_handler: {
fn mock_wait_for_event(&self) -> Result<XEvent> {
let mut remaining = self.events.replace(vec![]);
if remaining.is_empty() {
return Err(XError::ConnectionClosed)
}
let next = remaining.remove(0);
self.events.set(remaining);
Ok(next)
}
fn mock_send_client_event(&self, _: ClientMessage) -> Result<()> {
Ok(())
}
}
state: {
fn mock_current_screens(&self) -> Result<Vec<Screen>> {
Ok(self.screens.clone())
}
fn mock_focused_client(&self) -> Result<Xid> {
Ok(self.focused.get())
}
}
conn: {
fn mock_is_managed_client(&self, c: &Client) -> bool {
!self.unmanaged_ids.contains(&c.id())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
struct WmNameXConn {
wm_name: bool,
net_wm_name: bool,
empty_net_wm_name: bool,
}
impl StubXClientProperties for WmNameXConn {
fn mock_get_prop(&self, id: Xid, name: &str) -> Result<Prop> {
match Atom::from_str(name)? {
Atom::WmName if self.wm_name => Ok(Prop::UTF8String(vec!["wm_name".into()])),
Atom::WmName if self.net_wm_name && self.empty_net_wm_name => {
Ok(Prop::UTF8String(vec!["".into()]))
}
Atom::NetWmName if self.net_wm_name => {
Ok(Prop::UTF8String(vec!["net_wm_name".into()]))
}
Atom::NetWmName if self.empty_net_wm_name => Ok(Prop::UTF8String(vec!["".into()])),
_ => Err(XError::MissingProperty(name.into(), id)),
}
}
}
test_cases! {
window_name;
args: (wm_name: bool, net_wm_name: bool, empty_net_wm_name: bool, expected: &str);
case: wm_name_only => (true, false, false, "wm_name");
case: net_wm_name_only => (false, true, false, "net_wm_name");
case: both_prefers_net => (true, true, false, "net_wm_name");
case: net_wm_name_empty => (true, false, true, "wm_name");
body: {
let conn = WmNameXConn {
wm_name,
net_wm_name,
empty_net_wm_name,
};
assert_eq!(&conn.client_name(42).unwrap(), expected);
}
}
}