use crate::{
core::{bindings::KeyEventHandler, hooks::ManageHook, State, WindowManager},
util::spawn,
x::{Query, XConn, XConnExt, XEvent},
Result, Xid,
};
use std::{borrow::Cow, collections::HashMap, fmt};
use tracing::{debug, error, warn};
pub const NSP_TAG: &str = "NSP";
pub struct NamedScratchPad<X>
where
X: XConn,
{
name: Cow<'static, str>,
prog: Cow<'static, str>,
client: Option<Xid>,
query: Box<dyn Query<X>>,
hook: Box<dyn ManageHook<X>>,
}
impl<X: XConn> fmt::Debug for NamedScratchPad<X> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedScratchpad")
.field("name", &self.name)
.field("prog", &self.prog)
.field("client", &self.client)
.finish()
}
}
impl<X> NamedScratchPad<X>
where
X: XConn,
{
pub fn new<Q, H>(
name: impl Into<Cow<'static, str>>,
prog: impl Into<Cow<'static, str>>,
query: Q,
manage_hook: H,
run_hook_on_toggle: bool,
) -> (Self, ToggleNamedScratchPad)
where
Q: Query<X> + 'static,
H: ManageHook<X> + 'static,
{
let name = name.into();
let nsp = Self {
name: name.clone(),
prog: prog.into(),
client: None,
query: Box::new(query),
hook: Box::new(manage_hook),
};
(
nsp,
ToggleNamedScratchPad {
name,
run_hook_on_toggle,
},
)
}
}
struct NamedScratchPadState<X: XConn>(HashMap<Cow<'static, str>, NamedScratchPad<X>>);
pub fn add_named_scratchpads<X>(
mut wm: WindowManager<X>,
scratchpads: Vec<NamedScratchPad<X>>,
) -> WindowManager<X>
where
X: XConn + 'static,
{
let state: HashMap<_, _> = scratchpads
.into_iter()
.map(|nsp| (nsp.name.clone(), nsp))
.collect();
wm.state.add_extension(NamedScratchPadState(state));
wm.state
.client_set
.add_invisible_workspace(NSP_TAG)
.expect("named scratchpad tag to be unique");
wm.state.config.compose_or_set_manage_hook(manage_hook);
wm.state.config.compose_or_set_event_hook(event_hook);
wm
}
pub fn manage_hook<X: XConn + 'static>(id: Xid, state: &mut State<X>, x: &X) -> Result<()> {
let s = state.extension::<NamedScratchPadState<X>>()?;
for sp in s.borrow_mut().0.values_mut() {
if sp.client.is_none() && sp.query.run(id, x)? {
debug!(scratchpad=sp.name.as_ref(), %id, "matched query for named scratchpad");
sp.client = Some(id);
return sp.hook.call(id, state, x);
}
}
Ok(())
}
pub fn event_hook<X: XConn + 'static>(event: &XEvent, state: &mut State<X>, _: &X) -> Result<bool> {
let destroyed = match event {
XEvent::Destroy(id) => id,
_ => return Ok(true),
};
let s = state.extension::<NamedScratchPadState<X>>()?;
for sp in s.borrow_mut().0.values_mut() {
if sp.client == Some(*destroyed) {
debug!(%sp.name, %destroyed, "scratchpad client destroyed");
sp.client = None;
break;
}
}
Ok(true)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToggleNamedScratchPad {
name: Cow<'static, str>,
run_hook_on_toggle: bool,
}
impl<X: XConn + 'static> KeyEventHandler<X> for ToggleNamedScratchPad {
#[tracing::instrument(level = "debug", skip(state, x))]
fn call(&mut self, state: &mut State<X>, x: &X) -> Result<()> {
let _s = state.extension::<NamedScratchPadState<X>>()?;
let mut s = _s.borrow_mut();
let name = self.name.as_ref();
let (id, hook) = match s.0.get_mut(&self.name) {
Some(NamedScratchPad {
client: Some(id),
hook,
..
}) if state.client_set.contains(id) => {
debug!(%id, %name, "NamedScratchPad client exists in state");
(*id, hook)
}
Some(nsp) => {
debug!(%nsp.prog, %name, ?nsp.client, "spawning NamedScratchPad program");
nsp.client = None;
return spawn(nsp.prog.as_ref());
}
None => {
warn!(%name, "toggle called for unknown scratchpad: did you remember to call add_named_scratchpads?");
return Ok(());
}
};
debug!(
%id,
%name,
current_tag = state.client_set.current_tag(),
current_screen = state.client_set.current_screen().index(),
"Toggling nsp client"
);
if state.client_set.current_workspace().contains(&id) {
debug!(%id, "current workspace contains target client: moving to NSP tag");
state.client_set.move_client_to_tag(&id, NSP_TAG);
} else {
debug!(%id, "current workspace does not contain target client: moving to tag");
state.client_set.move_client_to_current_tag(&id);
if self.run_hook_on_toggle {
if let Err(e) = hook.call(id, state, x) {
error!(%e, %name, %id, "unable to run NSP manage hook during toggle");
}
}
}
debug!(%id, %name, "running refresh following NamedScratchPad toggle");
x.refresh(state)
}
}