-
Notifications
You must be signed in to change notification settings - Fork 87
/
Copy pathnamed_scratchpads.rs
208 lines (183 loc) · 6.6 KB
/
named_scratchpads.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! Support for managing multiple floating scratchpad programs that can be
//! toggled on or off on the active workspace.
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};
/// The tag used for a placeholder Workspace that holds scratchpad windows when
/// they are currently hidden.
pub const NSP_TAG: &str = "NSP";
/// A toggle-able client program that can be shown and hidden via a keybinding.
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,
{
/// Create a new named scratchpad.
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,
},
)
}
}
// Private wrapper type to ensure that only this module can access this state extension
struct NamedScratchPadState<X: XConn>(HashMap<Cow<'static, str>, NamedScratchPad<X>>);
/// Add the required hooks to manage EWMH compliance to an existing [crate::core::Config].
///
/// See the module level docs for details of what functionality is provided by
/// this extension.
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
}
/// Store clients matching NamedScratchPad queries and run the associated [ManageHook].
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(())
}
/// Remove destroyed clients from internal scratchpad state
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)
}
/// Toggle the visibility of a NamedScratchPad.
///
/// This will spawn the requested client program if it isn't currently running or
/// move it to the focused workspace. If the scratchpad is currently visible it
/// will be hidden.
#[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) {
// Active client somewhere in the StackSet
Some(NamedScratchPad {
client: Some(id),
hook,
..
}) if state.client_set.contains(id) => {
debug!(%id, %name, "NamedScratchPad client exists in state");
(*id, hook)
}
// No active client or client is no longer in state
Some(nsp) => {
debug!(%nsp.prog, %name, ?nsp.client, "spawning NamedScratchPad program");
nsp.client = None;
return spawn(nsp.prog.as_ref());
}
// The user created a ToggleNamedScratchPad but didn't register the scratchpad
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) {
// Toggle off: hiding the client on our invisible workspace
debug!(%id, "current workspace contains target client: moving to NSP tag");
state.client_set.move_client_to_tag(&id, NSP_TAG);
} else {
// Toggle on / bring to current workspace
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)
}
}