1use std::collections::HashMap;
2
3use rmux_proto::{
4 HookLifecycle, HookName, PaneTarget, RmuxError, ScopeSelector, SessionName, WindowTarget,
5};
6
7#[path = "hooks/bindings.rs"]
8mod bindings;
9#[path = "hooks/deferred.rs"]
10mod deferred;
11#[path = "hooks/identity.rs"]
12mod identity;
13#[path = "hooks/rules.rs"]
14mod rules;
15#[path = "hooks/targets.rs"]
16mod targets;
17#[path = "hooks/types.rs"]
18mod types;
19
20use bindings::HookBindings;
21use rules::{hook_class, hook_inventory, hook_is_visible_in_show_hooks, root_for_hook};
22pub use rules::{
23 hook_explicit_scope_for_target, hook_global_root, hook_natural_scope_for_session_target,
24 hook_natural_scope_for_target, validate_hook_registration, validate_hook_scope,
25};
26use types::HookClass;
27pub use types::{HookBindingView, HookDispatch, HookGlobalRoot, HookScopeIdentity, HookSetOptions};
28
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub struct HookStore {
32 session_global: HookBindings,
33 window_global: HookBindings,
34 sessions: HashMap<SessionName, HookBindings>,
35 windows: HashMap<WindowTarget, HookBindings>,
36 panes: HashMap<PaneTarget, HookBindings>,
37 windows_by_id: HashMap<rmux_proto::WindowId, HookBindings>,
38 panes_by_id: HashMap<rmux_proto::PaneId, HookBindings>,
39 window_aliases: HashMap<WindowTarget, rmux_proto::WindowId>,
40 pane_aliases: HashMap<PaneTarget, (rmux_proto::WindowId, rmux_proto::PaneId)>,
41}
42
43impl HookStore {
44 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 #[must_use]
52 pub fn is_empty(&self) -> bool {
53 self.session_global.is_empty()
54 && self.window_global.is_empty()
55 && self.sessions.values().all(HookBindings::is_empty)
56 && self.windows.values().all(HookBindings::is_empty)
57 && self.panes.values().all(HookBindings::is_empty)
58 && self.windows_by_id.values().all(HookBindings::is_empty)
59 && self.panes_by_id.values().all(HookBindings::is_empty)
60 }
61
62 pub fn set(
64 &mut self,
65 scope: ScopeSelector,
66 hook: HookName,
67 command: String,
68 lifecycle: HookLifecycle,
69 ) -> Result<u32, RmuxError> {
70 self.set_with_options(scope, hook, command, lifecycle, HookSetOptions::default())
71 }
72
73 pub fn set_with_options(
75 &mut self,
76 scope: ScopeSelector,
77 hook: HookName,
78 command: String,
79 lifecycle: HookLifecycle,
80 options: HookSetOptions,
81 ) -> Result<u32, RmuxError> {
82 validate_hook_scope(hook, &scope)?;
83 let bindings = self.bindings_for_scope_mut(hook, &scope);
84 Ok(bindings.set(hook, command, lifecycle, options))
85 }
86
87 pub fn unset(
89 &mut self,
90 scope: ScopeSelector,
91 hook: HookName,
92 index: Option<u32>,
93 ) -> Result<(), RmuxError> {
94 validate_hook_scope(hook, &scope)?;
95 match scope {
96 ScopeSelector::Global => {
97 self.global_bindings_mut(root_for_hook(hook))
98 .unset(hook, index);
99 }
100 ScopeSelector::Session(session_name) => {
101 let remove_scope = if let Some(bindings) = self.sessions.get_mut(&session_name) {
102 bindings.unset(hook, index);
103 bindings.is_empty()
104 } else {
105 false
106 };
107 if remove_scope {
108 self.sessions.remove(&session_name);
109 }
110 }
111 ScopeSelector::Window(target) => {
112 let identity = self.window_aliases.get(&target).copied();
113 let remove_scope = if let Some(window_id) = identity {
114 if let Some(bindings) = self.windows_by_id.get_mut(&window_id) {
115 bindings.unset(hook, index);
116 bindings.is_empty()
117 } else {
118 false
119 }
120 } else if let Some(bindings) = self.windows.get_mut(&target) {
121 bindings.unset(hook, index);
122 bindings.is_empty()
123 } else {
124 false
125 };
126 if remove_scope {
127 if let Some(window_id) = identity {
128 self.windows_by_id.remove(&window_id);
129 } else {
130 self.windows.remove(&target);
131 }
132 }
133 }
134 ScopeSelector::Pane(target) => {
135 let identity = self.pane_aliases.get(&target).copied();
136 let remove_scope = if let Some((_, pane_id)) = identity {
137 if let Some(bindings) = self.panes_by_id.get_mut(&pane_id) {
138 bindings.unset(hook, index);
139 bindings.is_empty()
140 } else {
141 false
142 }
143 } else if let Some(bindings) = self.panes.get_mut(&target) {
144 bindings.unset(hook, index);
145 bindings.is_empty()
146 } else {
147 false
148 };
149 if remove_scope {
150 if let Some((_, pane_id)) = identity {
151 self.panes_by_id.remove(&pane_id);
152 } else {
153 self.panes.remove(&target);
154 }
155 }
156 }
157 }
158 Ok(())
159 }
160
161 #[must_use]
163 pub fn global_command(&self, hook: HookName) -> Option<&str> {
164 self.global_bindings(root_for_hook(hook)).command(hook)
165 }
166
167 #[must_use]
169 pub fn global_command_at(&self, hook: HookName, index: u32) -> Option<&str> {
170 self.global_bindings(root_for_hook(hook))
171 .command_at(hook, index)
172 }
173
174 #[must_use]
176 pub fn global_lifecycle(&self, hook: HookName) -> Option<HookLifecycle> {
177 self.global_bindings(root_for_hook(hook)).lifecycle(hook)
178 }
179
180 #[must_use]
182 pub fn global_lifecycle_at(&self, hook: HookName, index: u32) -> Option<HookLifecycle> {
183 self.global_bindings(root_for_hook(hook))
184 .lifecycle_at(hook, index)
185 }
186
187 #[must_use]
189 pub fn session_command(&self, session_name: &SessionName, hook: HookName) -> Option<&str> {
190 self.sessions
191 .get(session_name)
192 .and_then(|bindings| bindings.command(hook))
193 }
194
195 #[must_use]
197 pub fn session_command_at(
198 &self,
199 session_name: &SessionName,
200 hook: HookName,
201 index: u32,
202 ) -> Option<&str> {
203 self.sessions
204 .get(session_name)
205 .and_then(|bindings| bindings.command_at(hook, index))
206 }
207
208 #[must_use]
210 pub fn session_lifecycle(
211 &self,
212 session_name: &SessionName,
213 hook: HookName,
214 ) -> Option<HookLifecycle> {
215 self.sessions
216 .get(session_name)
217 .and_then(|bindings| bindings.lifecycle(hook))
218 }
219
220 #[must_use]
222 pub fn session_lifecycle_at(
223 &self,
224 session_name: &SessionName,
225 hook: HookName,
226 index: u32,
227 ) -> Option<HookLifecycle> {
228 self.sessions
229 .get(session_name)
230 .and_then(|bindings| bindings.lifecycle_at(hook, index))
231 }
232
233 #[must_use]
235 pub fn window_command(&self, target: &WindowTarget, hook: HookName) -> Option<&str> {
236 self.windows
237 .get(target)
238 .and_then(|bindings| bindings.command(hook))
239 .or_else(|| {
240 self.window_aliases
241 .get(target)
242 .and_then(|window_id| self.windows_by_id.get(window_id))
243 .and_then(|bindings| bindings.command(hook))
244 })
245 }
246
247 #[must_use]
249 pub fn pane_command(&self, target: &PaneTarget, hook: HookName) -> Option<&str> {
250 self.panes
251 .get(target)
252 .and_then(|bindings| bindings.command(hook))
253 .or_else(|| {
254 self.pane_aliases
255 .get(target)
256 .and_then(|(_, pane_id)| self.panes_by_id.get(pane_id))
257 .and_then(|bindings| bindings.command(hook))
258 })
259 }
260
261 #[must_use]
263 pub fn global_bindings_view(
264 &self,
265 root: HookGlobalRoot,
266 hook: Option<HookName>,
267 ) -> Vec<HookBindingView> {
268 self.global_bindings(root).views(hook)
269 }
270
271 #[must_use]
273 pub fn session_bindings_view(
274 &self,
275 session_name: &SessionName,
276 hook: Option<HookName>,
277 ) -> Vec<HookBindingView> {
278 self.sessions
279 .get(session_name)
280 .map_or_else(Vec::new, |bindings| bindings.views(hook))
281 }
282
283 #[must_use]
285 pub fn window_bindings_view(
286 &self,
287 target: &WindowTarget,
288 hook: Option<HookName>,
289 ) -> Vec<HookBindingView> {
290 self.windows
291 .get(target)
292 .or_else(|| {
293 self.window_aliases
294 .get(target)
295 .and_then(|window_id| self.windows_by_id.get(window_id))
296 })
297 .map_or_else(Vec::new, |bindings| bindings.views(hook))
298 }
299
300 #[must_use]
302 pub fn pane_bindings_view(
303 &self,
304 target: &PaneTarget,
305 hook: Option<HookName>,
306 ) -> Vec<HookBindingView> {
307 self.panes
308 .get(target)
309 .or_else(|| {
310 self.pane_aliases
311 .get(target)
312 .and_then(|(_, pane_id)| self.panes_by_id.get(pane_id))
313 })
314 .map_or_else(Vec::new, |bindings| bindings.views(hook))
315 }
316
317 #[must_use]
319 pub fn shipped_global_hooks(root: HookGlobalRoot, hook: Option<HookName>) -> Vec<HookName> {
320 hook_inventory()
321 .into_iter()
322 .filter(|candidate| hook.map(|expected| *candidate == expected).unwrap_or(true))
323 .filter(|candidate| {
324 hook_is_visible_in_show_hooks(*candidate) && root_for_hook(*candidate) == root
325 })
326 .collect()
327 }
328
329 #[must_use]
331 pub fn dispatch(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
332 match hook_class(hook) {
333 HookClass::Session => self.dispatch_session(scope, hook),
334 HookClass::Window => self.dispatch_window(scope, hook),
335 HookClass::Pane => self.dispatch_pane(scope, hook),
336 }
337 }
338
339 fn bindings_for_scope_mut(
340 &mut self,
341 hook: HookName,
342 scope: &ScopeSelector,
343 ) -> &mut HookBindings {
344 match scope {
345 ScopeSelector::Global => self.global_bindings_mut(root_for_hook(hook)),
346 ScopeSelector::Session(session_name) => {
347 self.sessions.entry(session_name.clone()).or_default()
348 }
349 ScopeSelector::Window(target) => {
350 if let Some(window_id) = self.window_aliases.get(target).copied() {
351 self.windows_by_id.entry(window_id).or_default()
352 } else {
353 self.windows.entry(target.clone()).or_default()
354 }
355 }
356 ScopeSelector::Pane(target) => {
357 if let Some((_, pane_id)) = self.pane_aliases.get(target).copied() {
358 self.panes_by_id.entry(pane_id).or_default()
359 } else {
360 self.panes.entry(target.clone()).or_default()
361 }
362 }
363 }
364 }
365
366 fn global_bindings(&self, root: HookGlobalRoot) -> &HookBindings {
367 match root {
368 HookGlobalRoot::Session => &self.session_global,
369 HookGlobalRoot::Window => &self.window_global,
370 }
371 }
372
373 fn global_bindings_mut(&mut self, root: HookGlobalRoot) -> &mut HookBindings {
374 match root {
375 HookGlobalRoot::Session => &mut self.session_global,
376 HookGlobalRoot::Window => &mut self.window_global,
377 }
378 }
379
380 fn dispatch_session(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
381 let session_name = match scope {
382 ScopeSelector::Session(session_name) => Some(session_name.clone()),
383 ScopeSelector::Window(target) => Some(target.session_name().clone()),
384 ScopeSelector::Pane(target) => Some(target.session_name().clone()),
385 ScopeSelector::Global => None,
386 };
387
388 if let Some(session_name) = session_name {
389 let (dispatches, remove_scope) =
390 if let Some(bindings) = self.sessions.get_mut(&session_name) {
391 let dispatches = bindings.dispatch(hook);
392 let should_remove = bindings.is_empty();
393 (dispatches, should_remove)
394 } else {
395 (Vec::new(), false)
396 };
397 if remove_scope {
398 self.sessions.remove(&session_name);
399 }
400 if !dispatches.is_empty() {
401 return dispatches;
402 }
403 }
404
405 self.session_global.dispatch(hook)
406 }
407
408 fn dispatch_window(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
409 let target = match scope {
410 ScopeSelector::Window(target) => Some(target.clone()),
411 ScopeSelector::Pane(target) => Some(WindowTarget::with_window(
412 target.session_name().clone(),
413 target.window_index(),
414 )),
415 ScopeSelector::Global | ScopeSelector::Session(_) => None,
416 };
417
418 if let Some(target) = target {
419 let identity = self.window_aliases.get(&target).copied();
420 let (dispatches, remove_scope) = self
421 .windows
422 .get_mut(&target)
423 .or_else(|| identity.and_then(|window_id| self.windows_by_id.get_mut(&window_id)))
424 .map_or((Vec::new(), false), |bindings| {
425 let dispatches = bindings.dispatch(hook);
426 (dispatches, bindings.is_empty())
427 });
428 if remove_scope {
429 if let Some(window_id) = identity {
430 self.windows_by_id.remove(&window_id);
431 } else {
432 self.windows.remove(&target);
433 }
434 }
435 if !dispatches.is_empty() {
436 return dispatches;
437 }
438 }
439
440 self.window_global.dispatch(hook)
441 }
442
443 fn dispatch_pane(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
444 if let ScopeSelector::Pane(target) = scope {
445 let target = target.clone();
446 let identity = self.pane_aliases.get(&target).copied();
447 let (dispatches, remove_scope) = self
448 .panes
449 .get_mut(&target)
450 .or_else(|| identity.and_then(|(_, pane_id)| self.panes_by_id.get_mut(&pane_id)))
451 .map_or((Vec::new(), false), |bindings| {
452 let dispatches = bindings.dispatch(hook);
453 (dispatches, bindings.is_empty())
454 });
455 if remove_scope {
456 if let Some((_, pane_id)) = identity {
457 self.panes_by_id.remove(&pane_id);
458 } else {
459 self.panes.remove(&target);
460 }
461 }
462 if !dispatches.is_empty() {
463 return dispatches;
464 }
465 }
466
467 self.dispatch_window(scope, hook)
468 }
469}
470
471#[cfg(test)]
472#[path = "hooks/tests.rs"]
473mod tests;