1use std::collections::{BTreeMap, 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/rules.rs"]
10mod rules;
11#[path = "hooks/types.rs"]
12mod types;
13
14use bindings::HookBindings;
15use rules::{hook_class, hook_inventory, hook_is_visible_in_show_hooks, root_for_hook};
16pub use rules::{hook_global_root, validate_hook_registration, validate_hook_scope};
17use types::HookClass;
18pub use types::{HookBindingView, HookDispatch, HookGlobalRoot, HookSetOptions};
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct HookStore {
23 session_global: HookBindings,
24 window_global: HookBindings,
25 sessions: HashMap<SessionName, HookBindings>,
26 windows: HashMap<WindowTarget, HookBindings>,
27 panes: HashMap<PaneTarget, HookBindings>,
28}
29
30impl HookStore {
31 #[must_use]
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 #[must_use]
39 pub fn is_empty(&self) -> bool {
40 self.session_global.is_empty()
41 && self.window_global.is_empty()
42 && self.sessions.values().all(HookBindings::is_empty)
43 && self.windows.values().all(HookBindings::is_empty)
44 && self.panes.values().all(HookBindings::is_empty)
45 }
46
47 pub fn set(
49 &mut self,
50 scope: ScopeSelector,
51 hook: HookName,
52 command: String,
53 lifecycle: HookLifecycle,
54 ) -> Result<u32, RmuxError> {
55 self.set_with_options(scope, hook, command, lifecycle, HookSetOptions::default())
56 }
57
58 pub fn set_with_options(
60 &mut self,
61 scope: ScopeSelector,
62 hook: HookName,
63 command: String,
64 lifecycle: HookLifecycle,
65 options: HookSetOptions,
66 ) -> Result<u32, RmuxError> {
67 validate_hook_scope(hook, &scope)?;
68 let bindings = self.bindings_for_scope_mut(hook, &scope);
69 Ok(bindings.set(hook, command, lifecycle, options))
70 }
71
72 pub fn unset(
74 &mut self,
75 scope: ScopeSelector,
76 hook: HookName,
77 index: Option<u32>,
78 ) -> Result<(), RmuxError> {
79 validate_hook_scope(hook, &scope)?;
80 match scope {
81 ScopeSelector::Global => {
82 self.global_bindings_mut(root_for_hook(hook))
83 .unset(hook, index);
84 }
85 ScopeSelector::Session(session_name) => {
86 let remove_scope = if let Some(bindings) = self.sessions.get_mut(&session_name) {
87 bindings.unset(hook, index);
88 bindings.is_empty()
89 } else {
90 false
91 };
92 if remove_scope {
93 self.sessions.remove(&session_name);
94 }
95 }
96 ScopeSelector::Window(target) => {
97 let remove_scope = if let Some(bindings) = self.windows.get_mut(&target) {
98 bindings.unset(hook, index);
99 bindings.is_empty()
100 } else {
101 false
102 };
103 if remove_scope {
104 self.windows.remove(&target);
105 }
106 }
107 ScopeSelector::Pane(target) => {
108 let remove_scope = if let Some(bindings) = self.panes.get_mut(&target) {
109 bindings.unset(hook, index);
110 bindings.is_empty()
111 } else {
112 false
113 };
114 if remove_scope {
115 self.panes.remove(&target);
116 }
117 }
118 }
119 Ok(())
120 }
121
122 #[must_use]
124 pub fn global_command(&self, hook: HookName) -> Option<&str> {
125 self.global_bindings(root_for_hook(hook)).command(hook)
126 }
127
128 #[must_use]
130 pub fn global_command_at(&self, hook: HookName, index: u32) -> Option<&str> {
131 self.global_bindings(root_for_hook(hook))
132 .command_at(hook, index)
133 }
134
135 #[must_use]
137 pub fn global_lifecycle(&self, hook: HookName) -> Option<HookLifecycle> {
138 self.global_bindings(root_for_hook(hook)).lifecycle(hook)
139 }
140
141 #[must_use]
143 pub fn global_lifecycle_at(&self, hook: HookName, index: u32) -> Option<HookLifecycle> {
144 self.global_bindings(root_for_hook(hook))
145 .lifecycle_at(hook, index)
146 }
147
148 #[must_use]
150 pub fn session_command(&self, session_name: &SessionName, hook: HookName) -> Option<&str> {
151 self.sessions
152 .get(session_name)
153 .and_then(|bindings| bindings.command(hook))
154 }
155
156 #[must_use]
158 pub fn session_command_at(
159 &self,
160 session_name: &SessionName,
161 hook: HookName,
162 index: u32,
163 ) -> Option<&str> {
164 self.sessions
165 .get(session_name)
166 .and_then(|bindings| bindings.command_at(hook, index))
167 }
168
169 #[must_use]
171 pub fn session_lifecycle(
172 &self,
173 session_name: &SessionName,
174 hook: HookName,
175 ) -> Option<HookLifecycle> {
176 self.sessions
177 .get(session_name)
178 .and_then(|bindings| bindings.lifecycle(hook))
179 }
180
181 #[must_use]
183 pub fn session_lifecycle_at(
184 &self,
185 session_name: &SessionName,
186 hook: HookName,
187 index: u32,
188 ) -> Option<HookLifecycle> {
189 self.sessions
190 .get(session_name)
191 .and_then(|bindings| bindings.lifecycle_at(hook, index))
192 }
193
194 #[must_use]
196 pub fn window_command(&self, target: &WindowTarget, hook: HookName) -> Option<&str> {
197 self.windows
198 .get(target)
199 .and_then(|bindings| bindings.command(hook))
200 }
201
202 #[must_use]
204 pub fn pane_command(&self, target: &PaneTarget, hook: HookName) -> Option<&str> {
205 self.panes
206 .get(target)
207 .and_then(|bindings| bindings.command(hook))
208 }
209
210 #[must_use]
212 pub fn global_bindings_view(
213 &self,
214 root: HookGlobalRoot,
215 hook: Option<HookName>,
216 ) -> Vec<HookBindingView> {
217 self.global_bindings(root).views(hook)
218 }
219
220 #[must_use]
222 pub fn session_bindings_view(
223 &self,
224 session_name: &SessionName,
225 hook: Option<HookName>,
226 ) -> Vec<HookBindingView> {
227 self.sessions
228 .get(session_name)
229 .map_or_else(Vec::new, |bindings| bindings.views(hook))
230 }
231
232 #[must_use]
234 pub fn window_bindings_view(
235 &self,
236 target: &WindowTarget,
237 hook: Option<HookName>,
238 ) -> Vec<HookBindingView> {
239 self.windows
240 .get(target)
241 .map_or_else(Vec::new, |bindings| bindings.views(hook))
242 }
243
244 #[must_use]
246 pub fn pane_bindings_view(
247 &self,
248 target: &PaneTarget,
249 hook: Option<HookName>,
250 ) -> Vec<HookBindingView> {
251 self.panes
252 .get(target)
253 .map_or_else(Vec::new, |bindings| bindings.views(hook))
254 }
255
256 #[must_use]
258 pub fn shipped_global_hooks(root: HookGlobalRoot, hook: Option<HookName>) -> Vec<HookName> {
259 hook_inventory()
260 .into_iter()
261 .filter(|candidate| hook.map(|expected| *candidate == expected).unwrap_or(true))
262 .filter(|candidate| {
263 hook_is_visible_in_show_hooks(*candidate) && root_for_hook(*candidate) == root
264 })
265 .collect()
266 }
267
268 #[must_use]
270 pub fn dispatch(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
271 match hook_class(hook) {
272 HookClass::Session => self.dispatch_session(scope, hook),
273 HookClass::Window => self.dispatch_window(scope, hook),
274 HookClass::Pane => self.dispatch_pane(scope, hook),
275 }
276 }
277
278 pub fn remove_session(&mut self, session_name: &SessionName) -> bool {
280 let mut removed = self.sessions.remove(session_name).is_some();
281 self.windows.retain(|target, _| {
282 let keep = target.session_name() != session_name;
283 removed |= !keep;
284 keep
285 });
286 self.panes.retain(|target, _| {
287 let keep = target.session_name() != session_name;
288 removed |= !keep;
289 keep
290 });
291 removed
292 }
293
294 pub fn remove_window(&mut self, target: &WindowTarget) -> bool {
296 let mut removed = self.windows.remove(target).is_some();
297 self.panes.retain(|pane_target, _| {
298 let keep = pane_target.session_name() != target.session_name()
299 || pane_target.window_index() != target.window_index();
300 removed |= !keep;
301 keep
302 });
303 removed
304 }
305
306 pub fn swap_window_hooks(&mut self, source: &WindowTarget, target: &WindowTarget) {
308 if source == target {
309 return;
310 }
311
312 let source_window = self.windows.remove(source);
313 let target_window = self.windows.remove(target);
314 if let Some(bindings) = source_window {
315 self.windows.insert(target.clone(), bindings);
316 }
317 if let Some(bindings) = target_window {
318 self.windows.insert(source.clone(), bindings);
319 }
320
321 let source_panes = remove_window_pane_hooks(&mut self.panes, source);
322 let target_panes = remove_window_pane_hooks(&mut self.panes, target);
323 self.panes
324 .extend(rekey_pane_hooks(source_panes, source, target));
325 self.panes
326 .extend(rekey_pane_hooks(target_panes, target, source));
327 }
328
329 pub fn move_window_hooks(&mut self, source: &WindowTarget, target: &WindowTarget) {
331 if source == target {
332 return;
333 }
334
335 let source_window = self.windows.remove(source);
336 let _ = self.windows.remove(target);
337 if let Some(bindings) = source_window {
338 self.windows.insert(target.clone(), bindings);
339 }
340
341 let source_panes = remove_window_pane_hooks(&mut self.panes, source);
342 let _ = remove_window_pane_hooks(&mut self.panes, target);
343 self.panes
344 .extend(rekey_pane_hooks(source_panes, source, target));
345 }
346
347 pub fn remove_pane(&mut self, target: &PaneTarget) -> bool {
349 self.panes.remove(target).is_some()
350 }
351
352 pub fn remap_session_window_indices(
354 &mut self,
355 session_name: &SessionName,
356 index_map: &BTreeMap<u32, u32>,
357 ) -> Result<(), RmuxError> {
358 let mut remapped_windows = HashMap::with_capacity(self.windows.len());
359 for (target, bindings) in &self.windows {
360 let next_target = remapped_window_target(target, session_name, index_map);
361 if remapped_windows
362 .insert(next_target.clone(), bindings.clone())
363 .is_some()
364 {
365 return Err(RmuxError::Server(format!(
366 "hooks already exist for {next_target}"
367 )));
368 }
369 }
370
371 let mut remapped_panes = HashMap::with_capacity(self.panes.len());
372 for (target, bindings) in &self.panes {
373 let next_target = remapped_pane_target(target, session_name, index_map);
374 if remapped_panes
375 .insert(next_target.clone(), bindings.clone())
376 .is_some()
377 {
378 return Err(RmuxError::Server(format!(
379 "hooks already exist for {next_target}"
380 )));
381 }
382 }
383
384 self.windows = remapped_windows;
385 self.panes = remapped_panes;
386 Ok(())
387 }
388
389 pub fn rename_session(
391 &mut self,
392 session_name: &SessionName,
393 new_name: SessionName,
394 ) -> Result<(), RmuxError> {
395 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
396 for (name, bindings) in &self.sessions {
397 let next_name = if name == session_name {
398 new_name.clone()
399 } else {
400 name.clone()
401 };
402 if renamed_sessions
403 .insert(next_name.clone(), bindings.clone())
404 .is_some()
405 {
406 return Err(RmuxError::Server(format!(
407 "hooks already exist for session {next_name}"
408 )));
409 }
410 }
411
412 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
413 for (target, bindings) in &self.windows {
414 let next_target = if target.session_name() == session_name {
415 WindowTarget::with_window(new_name.clone(), target.window_index())
416 } else {
417 target.clone()
418 };
419 if renamed_windows
420 .insert(next_target.clone(), bindings.clone())
421 .is_some()
422 {
423 return Err(RmuxError::Server(format!(
424 "hooks already exist for {next_target}"
425 )));
426 }
427 }
428
429 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
430 for (target, bindings) in &self.panes {
431 let next_target = if target.session_name() == session_name {
432 PaneTarget::with_window(
433 new_name.clone(),
434 target.window_index(),
435 target.pane_index(),
436 )
437 } else {
438 target.clone()
439 };
440 if renamed_panes
441 .insert(next_target.clone(), bindings.clone())
442 .is_some()
443 {
444 return Err(RmuxError::Server(format!(
445 "hooks already exist for {next_target}"
446 )));
447 }
448 }
449
450 self.sessions = renamed_sessions;
451 self.windows = renamed_windows;
452 self.panes = renamed_panes;
453 Ok(())
454 }
455
456 fn bindings_for_scope_mut(
457 &mut self,
458 hook: HookName,
459 scope: &ScopeSelector,
460 ) -> &mut HookBindings {
461 match scope {
462 ScopeSelector::Global => self.global_bindings_mut(root_for_hook(hook)),
463 ScopeSelector::Session(session_name) => {
464 self.sessions.entry(session_name.clone()).or_default()
465 }
466 ScopeSelector::Window(target) => self.windows.entry(target.clone()).or_default(),
467 ScopeSelector::Pane(target) => self.panes.entry(target.clone()).or_default(),
468 }
469 }
470
471 fn global_bindings(&self, root: HookGlobalRoot) -> &HookBindings {
472 match root {
473 HookGlobalRoot::Session => &self.session_global,
474 HookGlobalRoot::Window => &self.window_global,
475 }
476 }
477
478 fn global_bindings_mut(&mut self, root: HookGlobalRoot) -> &mut HookBindings {
479 match root {
480 HookGlobalRoot::Session => &mut self.session_global,
481 HookGlobalRoot::Window => &mut self.window_global,
482 }
483 }
484
485 fn dispatch_session(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
486 let session_name = match scope {
487 ScopeSelector::Session(session_name) => Some(session_name.clone()),
488 ScopeSelector::Window(target) => Some(target.session_name().clone()),
489 ScopeSelector::Pane(target) => Some(target.session_name().clone()),
490 ScopeSelector::Global => None,
491 };
492
493 if let Some(session_name) = session_name {
494 let (dispatches, remove_scope) =
495 if let Some(bindings) = self.sessions.get_mut(&session_name) {
496 let dispatches = bindings.dispatch(hook);
497 let should_remove = bindings.is_empty();
498 (dispatches, should_remove)
499 } else {
500 (Vec::new(), false)
501 };
502 if remove_scope {
503 self.sessions.remove(&session_name);
504 }
505 if !dispatches.is_empty() {
506 return dispatches;
507 }
508 }
509
510 self.session_global.dispatch(hook)
511 }
512
513 fn dispatch_window(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
514 let target = match scope {
515 ScopeSelector::Window(target) => Some(target.clone()),
516 ScopeSelector::Pane(target) => Some(WindowTarget::with_window(
517 target.session_name().clone(),
518 target.window_index(),
519 )),
520 ScopeSelector::Global | ScopeSelector::Session(_) => None,
521 };
522
523 if let Some(target) = target {
524 let (dispatches, remove_scope) = if let Some(bindings) = self.windows.get_mut(&target) {
525 let dispatches = bindings.dispatch(hook);
526 let should_remove = bindings.is_empty();
527 (dispatches, should_remove)
528 } else {
529 (Vec::new(), false)
530 };
531 if remove_scope {
532 self.windows.remove(&target);
533 }
534 if !dispatches.is_empty() {
535 return dispatches;
536 }
537 }
538
539 self.window_global.dispatch(hook)
540 }
541
542 fn dispatch_pane(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
543 if let ScopeSelector::Pane(target) = scope {
544 let target = target.clone();
545 let (dispatches, remove_scope) = if let Some(bindings) = self.panes.get_mut(&target) {
546 let dispatches = bindings.dispatch(hook);
547 let should_remove = bindings.is_empty();
548 (dispatches, should_remove)
549 } else {
550 (Vec::new(), false)
551 };
552 if remove_scope {
553 self.panes.remove(&target);
554 }
555 if !dispatches.is_empty() {
556 return dispatches;
557 }
558 }
559
560 self.dispatch_window(scope, hook)
561 }
562}
563
564fn remove_window_pane_hooks(
565 panes: &mut HashMap<PaneTarget, HookBindings>,
566 window: &WindowTarget,
567) -> Vec<(PaneTarget, HookBindings)> {
568 let pane_targets = panes
569 .keys()
570 .filter(|pane_target| {
571 pane_target.session_name() == window.session_name()
572 && pane_target.window_index() == window.window_index()
573 })
574 .cloned()
575 .collect::<Vec<_>>();
576 pane_targets
577 .into_iter()
578 .filter_map(|pane_target| {
579 panes
580 .remove(&pane_target)
581 .map(|bindings| (pane_target, bindings))
582 })
583 .collect()
584}
585
586fn rekey_pane_hooks(
587 panes: Vec<(PaneTarget, HookBindings)>,
588 source: &WindowTarget,
589 target: &WindowTarget,
590) -> Vec<(PaneTarget, HookBindings)> {
591 panes
592 .into_iter()
593 .map(move |(pane_target, bindings)| {
594 debug_assert_eq!(pane_target.session_name(), source.session_name());
595 debug_assert_eq!(pane_target.window_index(), source.window_index());
596 (
597 PaneTarget::with_window(
598 target.session_name().clone(),
599 target.window_index(),
600 pane_target.pane_index(),
601 ),
602 bindings,
603 )
604 })
605 .collect()
606}
607
608fn remapped_window_target(
609 target: &WindowTarget,
610 session_name: &SessionName,
611 index_map: &BTreeMap<u32, u32>,
612) -> WindowTarget {
613 if target.session_name() != session_name {
614 return target.clone();
615 }
616 index_map.get(&target.window_index()).copied().map_or_else(
617 || target.clone(),
618 |window_index| WindowTarget::with_window(session_name.clone(), window_index),
619 )
620}
621
622fn remapped_pane_target(
623 target: &PaneTarget,
624 session_name: &SessionName,
625 index_map: &BTreeMap<u32, u32>,
626) -> PaneTarget {
627 if target.session_name() != session_name {
628 return target.clone();
629 }
630 index_map.get(&target.window_index()).copied().map_or_else(
631 || target.clone(),
632 |window_index| {
633 PaneTarget::with_window(session_name.clone(), window_index, target.pane_index())
634 },
635 )
636}
637
638#[cfg(test)]
639#[path = "hooks/tests.rs"]
640mod tests;