use crate::{
core::{hooks::EventHook, State},
pure::{geometry::RelativeRect, Stack},
x::{Query, XConn, XConnExt, XEvent},
Result, Xid,
};
use std::collections::HashMap;
use tracing::{info, warn};
#[derive(Default, Debug)]
struct WindowSwallowingState {
swallowed: HashMap<Xid, Xid>, stack_before_close: Option<Stack<Xid>>,
floating_before_close: HashMap<Xid, RelativeRect>,
}
impl WindowSwallowingState {
fn stash_state<X: XConn>(&mut self, state: &mut State<X>) -> Result<bool> {
self.stack_before_close = state.client_set.current_stack().cloned();
self.floating_before_close
.clone_from(&state.client_set.floating);
Ok(true)
}
fn clear_state_for(&mut self, id: Xid) {
self.swallowed.remove(&id);
self.stack_before_close = None;
}
fn try_restore_parent<X: XConn>(
&mut self,
child: Xid,
state: &mut State<X>,
x: &X,
) -> Result<bool> {
warn!(%child, ?self, "checking if we need to restore");
let parent = match self.swallowed.get(&child) {
Some(&parent) => parent,
None => return Ok(true),
};
info!(%parent, %child, "destroyed window has a swallowed parent: restoring");
let len = state.client_set.current_workspace().clients().count();
let mut old_stack = match self.stack_before_close.take() {
Some(s) if s.len() - 1 == len && s.focus == child => s,
_ => {
warn!(%parent, %child, "stashed state was invalid: inserting parent directly");
state.client_set.insert(parent);
self.clear_state_for(child);
return Ok(true);
}
};
info!(%parent, %child, "restoring swallowed parent in place of child");
transfer_floating_state(child, parent, &mut self.floating_before_close);
state
.client_set
.floating
.clone_from(&self.floating_before_close);
old_stack.focus = parent;
state.client_set.modify_occupied(|_| old_stack);
x.refresh(state)?;
self.clear_state_for(child);
Ok(false)
}
}
#[derive(Debug)]
pub struct WindowSwallowing<X: XConn> {
parent: Box<dyn Query<X>>,
child: Option<Box<dyn Query<X>>>,
}
impl<X: XConn> WindowSwallowing<X> {
pub fn boxed<Q>(parent: Q) -> Box<dyn EventHook<X>>
where
X: 'static,
Q: Query<X> + 'static,
{
Box::new(Self {
parent: Box::new(parent),
child: None,
})
}
fn queries_hold(&self, id: Xid, parent: Xid, x: &X) -> bool {
let parent_matches = x.query_or(false, &*self.parent, parent);
let child_matches = match &self.child {
Some(q) => x.query_or(false, &**q, id),
None => true,
};
parent_matches && child_matches
}
fn handle_map_request(
&mut self,
child: Xid,
wss: &mut WindowSwallowingState,
state: &mut State<X>,
x: &X,
) -> Result<bool> {
let parent = match state.client_set.current_client() {
Some(&parent) => parent,
None => return Ok(true), };
if !self.queries_hold(child, parent, x) || !is_child_of(child, parent, x) {
return Ok(true);
}
info!(%parent, %child, "matched queries for window swallowing");
wss.swallowed.insert(child, parent);
state.client_set.modify_occupied(|mut s| {
s.focus = child;
s
});
transfer_floating_state(parent, child, &mut state.client_set.floating);
Ok(false)
}
}
impl<X: XConn> EventHook<X> for WindowSwallowing<X> {
fn call(&mut self, event: &XEvent, state: &mut State<X>, x: &X) -> Result<bool> {
let _wss = state.extension_or_default::<WindowSwallowingState>();
let mut wss = _wss.borrow_mut();
match *event {
XEvent::MapRequest(id) => self.handle_map_request(id, &mut wss, state, x),
XEvent::ConfigureRequest(_) => wss.stash_state(state),
XEvent::Destroy(id) => wss.try_restore_parent(id, state, x),
_ => Ok(true),
}
}
}
fn transfer_floating_state(from: Xid, to: Xid, floating: &mut HashMap<Xid, RelativeRect>) {
if let Some(r) = floating.remove(&from) {
floating.insert(to, r);
}
}
fn is_child_of<X: XConn>(id: Xid, parent: Xid, x: &X) -> bool {
match (x.window_pid(parent), x.window_pid(id)) {
(Some(p_pid), Some(c_pid)) => parent_pid_chain(c_pid).contains(&p_pid),
_ => false,
}
}
fn parent_pid(pid: u32) -> Option<u32> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let s_parent_pid = stat.split_whitespace().nth(3).expect("/proc to be valid");
s_parent_pid.parse().ok()
}
fn parent_pid_chain(mut pid: u32) -> Vec<u32> {
let mut parents = vec![];
while let Some(parent) = parent_pid(pid) {
parents.push(parent);
pid = parent;
}
parents
}