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::{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 remove_pane(&mut self, target: &PaneTarget) -> bool {
308 self.panes.remove(target).is_some()
309 }
310
311 pub fn remap_session_window_indices(
313 &mut self,
314 session_name: &SessionName,
315 index_map: &BTreeMap<u32, u32>,
316 ) -> Result<(), RmuxError> {
317 let mut remapped_windows = HashMap::with_capacity(self.windows.len());
318 for (target, bindings) in &self.windows {
319 let next_target = remapped_window_target(target, session_name, index_map);
320 if remapped_windows
321 .insert(next_target.clone(), bindings.clone())
322 .is_some()
323 {
324 return Err(RmuxError::Server(format!(
325 "hooks already exist for {next_target}"
326 )));
327 }
328 }
329
330 let mut remapped_panes = HashMap::with_capacity(self.panes.len());
331 for (target, bindings) in &self.panes {
332 let next_target = remapped_pane_target(target, session_name, index_map);
333 if remapped_panes
334 .insert(next_target.clone(), bindings.clone())
335 .is_some()
336 {
337 return Err(RmuxError::Server(format!(
338 "hooks already exist for {next_target}"
339 )));
340 }
341 }
342
343 self.windows = remapped_windows;
344 self.panes = remapped_panes;
345 Ok(())
346 }
347
348 pub fn rename_session(
350 &mut self,
351 session_name: &SessionName,
352 new_name: SessionName,
353 ) -> Result<(), RmuxError> {
354 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
355 for (name, bindings) in &self.sessions {
356 let next_name = if name == session_name {
357 new_name.clone()
358 } else {
359 name.clone()
360 };
361 if renamed_sessions
362 .insert(next_name.clone(), bindings.clone())
363 .is_some()
364 {
365 return Err(RmuxError::Server(format!(
366 "hooks already exist for session {next_name}"
367 )));
368 }
369 }
370
371 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
372 for (target, bindings) in &self.windows {
373 let next_target = if target.session_name() == session_name {
374 WindowTarget::with_window(new_name.clone(), target.window_index())
375 } else {
376 target.clone()
377 };
378 if renamed_windows
379 .insert(next_target.clone(), bindings.clone())
380 .is_some()
381 {
382 return Err(RmuxError::Server(format!(
383 "hooks already exist for {next_target}"
384 )));
385 }
386 }
387
388 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
389 for (target, bindings) in &self.panes {
390 let next_target = if target.session_name() == session_name {
391 PaneTarget::with_window(
392 new_name.clone(),
393 target.window_index(),
394 target.pane_index(),
395 )
396 } else {
397 target.clone()
398 };
399 if renamed_panes
400 .insert(next_target.clone(), bindings.clone())
401 .is_some()
402 {
403 return Err(RmuxError::Server(format!(
404 "hooks already exist for {next_target}"
405 )));
406 }
407 }
408
409 self.sessions = renamed_sessions;
410 self.windows = renamed_windows;
411 self.panes = renamed_panes;
412 Ok(())
413 }
414
415 fn bindings_for_scope_mut(
416 &mut self,
417 hook: HookName,
418 scope: &ScopeSelector,
419 ) -> &mut HookBindings {
420 match scope {
421 ScopeSelector::Global => self.global_bindings_mut(root_for_hook(hook)),
422 ScopeSelector::Session(session_name) => {
423 self.sessions.entry(session_name.clone()).or_default()
424 }
425 ScopeSelector::Window(target) => self.windows.entry(target.clone()).or_default(),
426 ScopeSelector::Pane(target) => self.panes.entry(target.clone()).or_default(),
427 }
428 }
429
430 fn global_bindings(&self, root: HookGlobalRoot) -> &HookBindings {
431 match root {
432 HookGlobalRoot::Session => &self.session_global,
433 HookGlobalRoot::Window => &self.window_global,
434 }
435 }
436
437 fn global_bindings_mut(&mut self, root: HookGlobalRoot) -> &mut HookBindings {
438 match root {
439 HookGlobalRoot::Session => &mut self.session_global,
440 HookGlobalRoot::Window => &mut self.window_global,
441 }
442 }
443
444 fn dispatch_session(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
445 let session_name = match scope {
446 ScopeSelector::Session(session_name) => Some(session_name.clone()),
447 ScopeSelector::Window(target) => Some(target.session_name().clone()),
448 ScopeSelector::Pane(target) => Some(target.session_name().clone()),
449 ScopeSelector::Global => None,
450 };
451
452 if let Some(session_name) = session_name {
453 let (dispatches, remove_scope) =
454 if let Some(bindings) = self.sessions.get_mut(&session_name) {
455 let dispatches = bindings.dispatch(hook);
456 let should_remove = bindings.is_empty();
457 (dispatches, should_remove)
458 } else {
459 (Vec::new(), false)
460 };
461 if remove_scope {
462 self.sessions.remove(&session_name);
463 }
464 if !dispatches.is_empty() {
465 return dispatches;
466 }
467 }
468
469 self.session_global.dispatch(hook)
470 }
471
472 fn dispatch_window(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
473 let target = match scope {
474 ScopeSelector::Window(target) => Some(target.clone()),
475 ScopeSelector::Pane(target) => Some(WindowTarget::with_window(
476 target.session_name().clone(),
477 target.window_index(),
478 )),
479 ScopeSelector::Global | ScopeSelector::Session(_) => None,
480 };
481
482 if let Some(target) = target {
483 let (dispatches, remove_scope) = if let Some(bindings) = self.windows.get_mut(&target) {
484 let dispatches = bindings.dispatch(hook);
485 let should_remove = bindings.is_empty();
486 (dispatches, should_remove)
487 } else {
488 (Vec::new(), false)
489 };
490 if remove_scope {
491 self.windows.remove(&target);
492 }
493 if !dispatches.is_empty() {
494 return dispatches;
495 }
496 }
497
498 self.window_global.dispatch(hook)
499 }
500
501 fn dispatch_pane(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
502 if let ScopeSelector::Pane(target) = scope {
503 let target = target.clone();
504 let (dispatches, remove_scope) = if let Some(bindings) = self.panes.get_mut(&target) {
505 let dispatches = bindings.dispatch(hook);
506 let should_remove = bindings.is_empty();
507 (dispatches, should_remove)
508 } else {
509 (Vec::new(), false)
510 };
511 if remove_scope {
512 self.panes.remove(&target);
513 }
514 if !dispatches.is_empty() {
515 return dispatches;
516 }
517 }
518
519 self.dispatch_window(scope, hook)
520 }
521}
522
523fn remapped_window_target(
524 target: &WindowTarget,
525 session_name: &SessionName,
526 index_map: &BTreeMap<u32, u32>,
527) -> WindowTarget {
528 if target.session_name() != session_name {
529 return target.clone();
530 }
531 index_map.get(&target.window_index()).copied().map_or_else(
532 || target.clone(),
533 |window_index| WindowTarget::with_window(session_name.clone(), window_index),
534 )
535}
536
537fn remapped_pane_target(
538 target: &PaneTarget,
539 session_name: &SessionName,
540 index_map: &BTreeMap<u32, u32>,
541) -> PaneTarget {
542 if target.session_name() != session_name {
543 return target.clone();
544 }
545 index_map.get(&target.window_index()).copied().map_or_else(
546 || target.clone(),
547 |window_index| {
548 PaneTarget::with_window(session_name.clone(), window_index, target.pane_index())
549 },
550 )
551}
552
553#[cfg(test)]
554#[path = "hooks/tests.rs"]
555mod tests;