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/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 rename_session(
313 &mut self,
314 session_name: &SessionName,
315 new_name: SessionName,
316 ) -> Result<(), RmuxError> {
317 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
318 for (name, bindings) in &self.sessions {
319 let next_name = if name == session_name {
320 new_name.clone()
321 } else {
322 name.clone()
323 };
324 if renamed_sessions
325 .insert(next_name.clone(), bindings.clone())
326 .is_some()
327 {
328 return Err(RmuxError::Server(format!(
329 "hooks already exist for session {next_name}"
330 )));
331 }
332 }
333
334 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
335 for (target, bindings) in &self.windows {
336 let next_target = if target.session_name() == session_name {
337 WindowTarget::with_window(new_name.clone(), target.window_index())
338 } else {
339 target.clone()
340 };
341 if renamed_windows
342 .insert(next_target.clone(), bindings.clone())
343 .is_some()
344 {
345 return Err(RmuxError::Server(format!(
346 "hooks already exist for {next_target}"
347 )));
348 }
349 }
350
351 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
352 for (target, bindings) in &self.panes {
353 let next_target = if target.session_name() == session_name {
354 PaneTarget::with_window(
355 new_name.clone(),
356 target.window_index(),
357 target.pane_index(),
358 )
359 } else {
360 target.clone()
361 };
362 if renamed_panes
363 .insert(next_target.clone(), bindings.clone())
364 .is_some()
365 {
366 return Err(RmuxError::Server(format!(
367 "hooks already exist for {next_target}"
368 )));
369 }
370 }
371
372 self.sessions = renamed_sessions;
373 self.windows = renamed_windows;
374 self.panes = renamed_panes;
375 Ok(())
376 }
377
378 fn bindings_for_scope_mut(
379 &mut self,
380 hook: HookName,
381 scope: &ScopeSelector,
382 ) -> &mut HookBindings {
383 match scope {
384 ScopeSelector::Global => self.global_bindings_mut(root_for_hook(hook)),
385 ScopeSelector::Session(session_name) => {
386 self.sessions.entry(session_name.clone()).or_default()
387 }
388 ScopeSelector::Window(target) => self.windows.entry(target.clone()).or_default(),
389 ScopeSelector::Pane(target) => self.panes.entry(target.clone()).or_default(),
390 }
391 }
392
393 fn global_bindings(&self, root: HookGlobalRoot) -> &HookBindings {
394 match root {
395 HookGlobalRoot::Session => &self.session_global,
396 HookGlobalRoot::Window => &self.window_global,
397 }
398 }
399
400 fn global_bindings_mut(&mut self, root: HookGlobalRoot) -> &mut HookBindings {
401 match root {
402 HookGlobalRoot::Session => &mut self.session_global,
403 HookGlobalRoot::Window => &mut self.window_global,
404 }
405 }
406
407 fn dispatch_session(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
408 let session_name = match scope {
409 ScopeSelector::Session(session_name) => Some(session_name.clone()),
410 ScopeSelector::Window(target) => Some(target.session_name().clone()),
411 ScopeSelector::Pane(target) => Some(target.session_name().clone()),
412 ScopeSelector::Global => None,
413 };
414
415 if let Some(session_name) = session_name {
416 let (dispatches, remove_scope) =
417 if let Some(bindings) = self.sessions.get_mut(&session_name) {
418 let dispatches = bindings.dispatch(hook);
419 let should_remove = bindings.is_empty();
420 (dispatches, should_remove)
421 } else {
422 (Vec::new(), false)
423 };
424 if remove_scope {
425 self.sessions.remove(&session_name);
426 }
427 if !dispatches.is_empty() {
428 return dispatches;
429 }
430 }
431
432 self.session_global.dispatch(hook)
433 }
434
435 fn dispatch_window(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
436 let target = match scope {
437 ScopeSelector::Window(target) => Some(target.clone()),
438 ScopeSelector::Pane(target) => Some(WindowTarget::with_window(
439 target.session_name().clone(),
440 target.window_index(),
441 )),
442 ScopeSelector::Global | ScopeSelector::Session(_) => None,
443 };
444
445 if let Some(target) = target {
446 let (dispatches, remove_scope) = if let Some(bindings) = self.windows.get_mut(&target) {
447 let dispatches = bindings.dispatch(hook);
448 let should_remove = bindings.is_empty();
449 (dispatches, should_remove)
450 } else {
451 (Vec::new(), false)
452 };
453 if remove_scope {
454 self.windows.remove(&target);
455 }
456 if !dispatches.is_empty() {
457 return dispatches;
458 }
459 }
460
461 self.window_global.dispatch(hook)
462 }
463
464 fn dispatch_pane(&mut self, scope: &ScopeSelector, hook: HookName) -> Vec<HookDispatch> {
465 if let ScopeSelector::Pane(target) = scope {
466 let target = target.clone();
467 let (dispatches, remove_scope) = if let Some(bindings) = self.panes.get_mut(&target) {
468 let dispatches = bindings.dispatch(hook);
469 let should_remove = bindings.is_empty();
470 (dispatches, should_remove)
471 } else {
472 (Vec::new(), false)
473 };
474 if remove_scope {
475 self.panes.remove(&target);
476 }
477 if !dispatches.is_empty() {
478 return dispatches;
479 }
480 }
481
482 self.dispatch_window(scope, hook)
483 }
484}
485
486#[cfg(test)]
487#[path = "hooks/tests.rs"]
488mod tests;