use crate::{
pure::{geometry::Rect, Diff, ScreenClients, Snapshot, StackSet, Workspace},
x::{
manage_without_refresh,
property::{MapState, WmState},
Atom, Prop, WindowAttributes, XConn, XConnExt, XEvent,
},
Color, Error, Result,
};
use anymap::{any::Any, AnyMap};
use nix::sys::signal::{signal, SigHandler, Signal};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
any::TypeId,
cell::RefCell,
collections::{HashMap, HashSet},
fmt,
ops::Deref,
sync::Arc,
};
use tracing::{debug, error, info, span, trace, warn, Level};
pub mod bindings;
pub(crate) mod handle;
pub mod hooks;
pub mod layout;
use bindings::{KeyBindings, MouseBindings, MouseState};
use hooks::{EventHook, LayoutHook, ManageHook, StateHook};
use layout::LayoutStack;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct Xid(pub(crate) u32);
impl std::fmt::Display for Xid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for Xid {
type Target = u32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<u32> for Xid {
fn from(id: u32) -> Self {
Self(id)
}
}
impl From<Xid> for u32 {
fn from(id: Xid) -> Self {
id.0
}
}
pub type ClientSet = StackSet<Xid>;
pub type ClientSpace = Workspace<Xid>;
#[derive(Debug)]
pub struct State<X>
where
X: XConn,
{
pub config: Config<X>,
pub client_set: ClientSet,
pub(crate) extensions: AnyMap,
pub(crate) root: Xid,
pub(crate) mapped: HashSet<Xid>,
pub(crate) pending_unmap: HashMap<Xid, usize>,
pub(crate) current_event: Option<XEvent>,
pub(crate) diff: Diff<Xid>,
pub(crate) running: bool,
pub(crate) held_mouse_state: Option<MouseState>,
}
impl<X> State<X>
where
X: XConn,
{
pub(crate) fn try_new(config: Config<X>, x: &X) -> Result<Self> {
let mut client_set = StackSet::try_new(
config.default_layouts.clone(),
config.tags.iter(),
x.screen_details()?,
)?;
let ss = client_set.snapshot(vec![]);
let diff = Diff::new(ss.clone(), ss);
Ok(Self {
config,
client_set,
extensions: AnyMap::new(),
root: x.root(),
mapped: HashSet::new(),
pending_unmap: HashMap::new(),
current_event: None,
diff,
running: false,
held_mouse_state: None,
})
}
pub fn root(&self) -> Xid {
self.root
}
pub fn mapped_clients(&self) -> &HashSet<Xid> {
&self.mapped
}
pub fn current_event(&self) -> Option<&XEvent> {
self.current_event.as_ref()
}
pub fn extension<E: Any>(&self) -> Result<Arc<RefCell<E>>> {
self.extensions
.get()
.cloned()
.ok_or(Error::UnknownStateExtension {
type_id: TypeId::of::<E>(),
})
}
pub fn extension_or_default<E: Default + Any>(&mut self) -> Arc<RefCell<E>> {
if !self.extensions.contains::<Arc<RefCell<E>>>() {
self.add_extension(E::default());
}
self.extension().expect("to have defaulted if missing")
}
pub fn remove_extension<E: Any>(&mut self) -> Option<E> {
let arc: Arc<RefCell<E>> = self.extensions.remove()?;
match Arc::try_unwrap(arc) {
Ok(rc) => Some(rc.into_inner()),
Err(arc) => {
self.extensions.insert(arc);
None
}
}
}
pub fn add_extension<E: Any>(&mut self, extension: E) {
self.extensions.insert(Arc::new(RefCell::new(extension)));
}
pub(crate) fn position_and_snapshot(&mut self, x: &X) -> Snapshot<Xid> {
let positions = self.visible_client_positions(x);
self.client_set.snapshot(positions)
}
pub(crate) fn visible_client_positions(&mut self, x: &X) -> Vec<(Xid, Rect)> {
let mut float_positions: Vec<(Xid, Rect)> = Vec::new();
let mut positions: Vec<(Xid, Rect)> = Vec::new();
let mut hook = self.config.layout_hook.take();
let scs: Vec<ScreenClients<Xid>> = self
.client_set
.screens
.iter()
.map(|s| s.screen_clients(&self.client_set.floating))
.collect();
for (i, sc) in scs.into_iter().enumerate() {
let ScreenClients {
floating,
tiling,
tag,
r_s,
} = sc;
for (c, r_c) in floating.iter() {
float_positions.push((*c, r_c.applied_to(&r_s)));
}
let stack_positions = match hook {
Some(ref mut h) => {
let r_s = h.transform_initial_for_screen(i, r_s, self, x);
let s = self.client_set.screens.iter_mut().nth(i).unwrap();
let initial = s.workspace.apply_layout(&tag, &tiling, r_s);
h.transform_positions_for_screen(i, r_s, initial, self, x)
}
None => {
let s = self.client_set.screens.iter_mut().nth(i).unwrap();
s.workspace.apply_layout(&tag, &tiling, r_s)
}
};
positions.extend(stack_positions.into_iter().rev());
}
float_positions.reverse();
positions.extend(float_positions);
self.config.layout_hook = hook;
positions
}
}
pub struct Config<X>
where
X: XConn,
{
pub normal_border: Color,
pub focused_border: Color,
pub border_width: u32,
pub focus_follow_mouse: bool,
pub default_layouts: LayoutStack,
pub tags: Vec<String>,
pub floating_classes: Vec<String>,
pub startup_hook: Option<Box<dyn StateHook<X>>>,
pub event_hook: Option<Box<dyn EventHook<X>>>,
pub manage_hook: Option<Box<dyn ManageHook<X>>>,
pub refresh_hook: Option<Box<dyn StateHook<X>>>,
pub layout_hook: Option<Box<dyn LayoutHook<X>>>,
}
impl<X> fmt::Debug for Config<X>
where
X: XConn,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("normal_border", &self.normal_border)
.field("focused_border", &self.focused_border)
.field("border_width", &self.border_width)
.field("focus_follow_mouse", &self.focus_follow_mouse)
.field("default_layouts", &self.default_layouts)
.field("tags", &self.tags)
.field("floating_classes", &self.floating_classes)
.finish()
}
}
impl<X> Default for Config<X>
where
X: XConn,
{
fn default() -> Self {
let strings = |slice: &[&str]| slice.iter().map(|s| s.to_string()).collect();
Config {
normal_border: "#3c3836ff".try_into().expect("valid hex code"),
focused_border: "#cc241dff".try_into().expect("valid hex code"),
border_width: 2,
focus_follow_mouse: true,
default_layouts: LayoutStack::default(),
tags: strings(&["1", "2", "3", "4", "5", "6", "7", "8", "9"]),
floating_classes: strings(&["dmenu", "dunst"]),
startup_hook: None,
event_hook: None,
manage_hook: None,
refresh_hook: None,
layout_hook: None,
}
}
}
impl<X> Config<X>
where
X: XConn,
{
pub fn compose_or_set_startup_hook<H>(&mut self, hook: H)
where
H: StateHook<X> + 'static,
X: 'static,
{
self.startup_hook = match self.startup_hook.take() {
Some(h) => Some(hook.then_boxed(h)),
None => Some(hook.boxed()),
};
}
pub fn compose_or_set_event_hook<H>(&mut self, hook: H)
where
H: EventHook<X> + 'static,
X: 'static,
{
self.event_hook = match self.event_hook.take() {
Some(h) => Some(hook.then_boxed(h)),
None => Some(hook.boxed()),
};
}
pub fn compose_or_set_manage_hook<H>(&mut self, hook: H)
where
H: ManageHook<X> + 'static,
X: 'static,
{
self.manage_hook = match self.manage_hook.take() {
Some(h) => Some(hook.then_boxed(h)),
None => Some(hook.boxed()),
};
}
pub fn compose_or_set_refresh_hook<H>(&mut self, hook: H)
where
H: StateHook<X> + 'static,
X: 'static,
{
self.refresh_hook = match self.refresh_hook.take() {
Some(h) => Some(hook.then_boxed(h)),
None => Some(hook.boxed()),
};
}
pub fn compose_or_set_layout_hook<H>(&mut self, hook: H)
where
H: LayoutHook<X> + 'static,
X: 'static,
{
self.layout_hook = match self.layout_hook.take() {
Some(h) => Some(hook.then_boxed(h)),
None => Some(hook.boxed()),
};
}
}
#[derive(Debug)]
pub struct WindowManager<X>
where
X: XConn,
{
x: X,
pub state: State<X>,
key_bindings: KeyBindings<X>,
mouse_bindings: MouseBindings<X>,
}
impl<X> WindowManager<X>
where
X: XConn,
{
pub fn new(
config: Config<X>,
key_bindings: KeyBindings<X>,
mouse_bindings: MouseBindings<X>,
x: X,
) -> Result<Self> {
let state = State::try_new(config, &x)?;
Ok(Self {
x,
state,
key_bindings,
mouse_bindings,
})
}
pub fn add_extension<E: Any>(&mut self, extension: E) {
self.state.add_extension(extension);
}
pub fn run(mut self) -> Result<()> {
info!("registering SIGCHILD signal handler");
if let Err(e) = unsafe { signal(Signal::SIGCHLD, SigHandler::SigIgn) } {
panic!("unable to set signal handler: {}", e);
}
handle::mapping_notify(&self.key_bindings, &self.mouse_bindings, &self.x)?;
if let Some(mut h) = self.state.config.startup_hook.take() {
trace!("running user startup hook");
if let Err(e) = h.call(&mut self.state, &self.x) {
error!(%e, "error returned from user startup hook");
}
}
manage_existing_clients(&mut self.state, &self.x)?;
self.state.running = true;
while self.state.running {
match self.x.next_event() {
Ok(event) => {
let span = span!(target: "penrose", Level::INFO, "XEvent", %event);
let _enter = span.enter();
trace!(details = ?event, "event details");
self.state.current_event = Some(event.clone());
if let Err(e) = self.handle_xevent(event) {
error!(%e, "Error handling XEvent");
}
self.x.flush();
self.state.current_event = None;
}
Err(e) => self.handle_error(e),
}
}
Ok(())
}
fn handle_xevent(&mut self, event: XEvent) -> Result<()> {
use XEvent::*;
let WindowManager {
x,
state,
key_bindings,
mouse_bindings,
} = self;
let mut hook = state.config.event_hook.take();
let should_run = match hook {
Some(ref mut h) => {
trace!("running user event hook");
match h.call(&event, state, x) {
Ok(should_run) => should_run,
Err(e) => {
error!(%e, "error returned from user event hook");
true
}
}
}
None => true,
};
state.config.event_hook = hook;
if !should_run {
trace!("User event hook returned false: skipping default handling");
return Ok(());
}
match &event {
ClientMessage(m) => handle::client_message(m.clone(), state, x)?,
ConfigureNotify(e) if e.is_root => handle::detect_screens(state, x)?,
ConfigureNotify(_) => (), ConfigureRequest(e) => handle::configure_request(e, state, x)?,
Enter(p) => handle::enter(*p, state, x)?,
Expose(_) => (), FocusIn(id) => handle::focus_in(*id, state, x)?,
Destroy(xid) => handle::destroy(*xid, state, x)?,
KeyPress(code) => handle::keypress(*code, key_bindings, state, x)?,
Leave(p) => handle::leave(*p, state, x)?,
MappingNotify => handle::mapping_notify(key_bindings, mouse_bindings, x)?,
MapRequest(xid) => handle::map_request(*xid, state, x)?,
MouseEvent(e) => handle::mouse_event(e.clone(), mouse_bindings, state, x)?,
MotionNotify(e) => handle::motion_event(e.clone(), mouse_bindings, state, x)?,
PropertyNotify(_) => (), RandrNotify => handle::detect_screens(state, x)?,
ScreenChange => handle::screen_change(state, x)?,
UnmapNotify(xid) => handle::unmap_notify(*xid, state, x)?,
_ => (), }
Ok(())
}
fn handle_error(&mut self, e: Error) {
match e {
Error::UnknownClient(id) => {
debug!(%id, "XConn encountered an error due to an unknown client ID: removing client");
self.state.client_set.remove_client(&id);
}
_ => error!(%e, "Unhandled error pulling next x event"),
}
}
}
#[tracing::instrument(level = "info", skip(state, x))]
fn manage_existing_clients<X: XConn>(state: &mut State<X>, x: &X) -> Result<()> {
info!("managing existing clients");
let ws_map: HashMap<usize, String> = state
.client_set
.non_hidden_workspaces()
.map(|w| (w.id, w.tag.clone()))
.collect();
let first_tag = state.client_set.ordered_tags()[0].clone();
for id in x.existing_clients()? {
if !state.client_set.contains(&id) && client_should_be_manged(id, x) {
let workspace_id = match x.get_prop(id, Atom::NetWmDesktop.as_ref()) {
Ok(Some(Prop::Cardinal(ids))) => ids[0] as usize,
_ => 0, };
let tag = ws_map.get(&workspace_id).unwrap_or(&first_tag);
let title = x.window_title(id)?;
info!(%id, %title, %tag, "attempting to manage existing client");
manage_without_refresh(id, Some(tag), state, x)?;
}
}
match x.get_prop(state.root, Atom::NetActiveWindow.as_ref()) {
Ok(Some(Prop::Window(ids))) if state.client_set.contains(&ids[0]) => {
let id = ids[0];
info!(%id, "focusing _NET_ACTIVE_WINDOW client");
state.client_set.focus_client(&id);
}
_ => {
info!(%first_tag, "unable to determine an active window: focusing first tag");
state.client_set.focus_tag(&first_tag);
}
};
info!("triggering refresh");
x.refresh(state)
}
fn client_should_be_manged<X: XConn>(id: Xid, x: &X) -> bool {
let attrs = match x.get_window_attributes(id) {
Ok(attrs) => attrs,
_ => {
warn!(%id, "unable to pull window attributes for client: skipping.");
return false;
}
};
let wm_state = match x.get_wm_state(id) {
Ok(state) => state,
_ => {
warn!(%id, "unable to pull wm state for client: skipping.");
return false;
}
};
info!(%id, ?attrs, ?wm_state, "processing client");
let WindowAttributes {
override_redirect,
map_state,
..
} = attrs;
let viewable = map_state == MapState::Viewable;
let iconic = wm_state == Some(WmState::Iconic);
!override_redirect && (viewable || iconic)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pure::{test_xid_stack_set, Position};
fn stack_order(cs: &ClientSet) -> Vec<u32> {
let positions = cs.visible_client_positions();
positions.iter().map(|&(id, _)| *id).collect()
}
#[test]
fn floating_client_positions_are_respected() {
let mut s = test_xid_stack_set(5, 2);
for n in 0..4 {
s.insert(Xid(n));
}
let r = Rect::new(50, 50, 50, 50);
s.float_unchecked(Xid(1), r);
let positions = s.visible_client_positions();
assert!(positions.contains(&(Xid(1), r)), "{positions:?}")
}
#[test]
fn floating_clients_stay_on_their_assigned_screen() {
let mut s = test_xid_stack_set(5, 2);
for n in 0..4 {
s.insert(Xid(n));
}
let r = Rect::new(50, 50, 50, 50);
s.float_unchecked(Xid(1), r);
let positions = s.visible_client_positions();
assert!(positions.contains(&(Xid(1), r)), "{positions:?}");
s.move_client_to_tag(&Xid(1), "2");
let positions = s.visible_client_positions();
assert!(!positions.contains(&(Xid(1), r)), "{positions:?}");
assert!(
positions.contains(&(Xid(1), Rect::new(1050, 2050, 50, 50))),
"{positions:?}"
);
}
#[test]
fn floating_windows_are_returned_last() {
let mut s = test_xid_stack_set(5, 2);
for n in 1..6 {
s.insert(Xid(n));
}
s.float_unchecked(Xid(2), Rect::new(0, 0, 42, 42));
s.float_unchecked(Xid(3), Rect::new(0, 0, 69, 69));
assert_eq!(stack_order(&s), vec![1, 4, 5, 2, 3]);
}
#[test]
fn newly_added_windows_are_below_floating() {
let mut s = test_xid_stack_set(5, 2);
for n in 1..6 {
s.insert(Xid(n));
}
s.float_unchecked(Xid(2), Rect::new(0, 0, 42, 42));
s.float_unchecked(Xid(3), Rect::new(0, 0, 69, 69));
s.insert(Xid(6));
assert_eq!(stack_order(&s), vec![1, 4, 5, 6, 2, 3]);
}
#[test]
fn floating_clients_dont_break_insert_focus() {
let mut s = test_xid_stack_set(1, 1);
s.insert_at(Position::Focus, Xid(0));
s.float_unchecked(Xid(0), Rect::new(0, 0, 42, 42));
assert_eq!(s.current_client(), Some(&Xid(0)));
let mut expected = vec![0];
for n in 1..=5 {
s.insert_at(Position::Focus, Xid(n));
assert_eq!(s.current_client(), Some(&Xid(n)));
expected.insert(expected.len() - 1, n);
assert_eq!(stack_order(&s), expected, "{:?}", s.current_stack());
}
}
}