1use std::collections::{HashMap, HashSet};
2
3#[cfg(test)]
4use rmux_proto::SessionName;
5use rmux_proto::{
6 HookLifecycle, HookName, PaneId, PaneTarget, ScopeSelector, WindowId, WindowTarget,
7};
8
9use super::rules::{hook_class, root_for_hook};
10use super::types::HookClass;
11use super::{
12 HookBindingView, HookBindings, HookDispatch, HookScopeIdentity, HookSetOptions, HookStore,
13};
14
15impl HookStore {
16 pub fn set_with_identity(
18 &mut self,
19 scope: HookScopeIdentity,
20 hook: HookName,
21 command: String,
22 lifecycle: HookLifecycle,
23 options: HookSetOptions,
24 ) -> u32 {
25 self.identity_bindings_mut(&scope, hook)
26 .set(hook, command, lifecycle, options)
27 }
28
29 pub fn unset_with_identity(
31 &mut self,
32 scope: &HookScopeIdentity,
33 hook: HookName,
34 index: Option<u32>,
35 ) {
36 match scope {
37 HookScopeIdentity::Global => {
38 self.global_bindings_mut(root_for_hook(hook))
39 .unset(hook, index);
40 }
41 HookScopeIdentity::Session(session_name) => {
42 let remove_scope = self.sessions.get_mut(session_name).is_some_and(|bindings| {
43 bindings.unset(hook, index);
44 bindings.is_empty()
45 });
46 if remove_scope {
47 self.sessions.remove(session_name);
48 }
49 }
50 HookScopeIdentity::Window { window_id, .. } => {
51 let remove_scope = self
52 .windows_by_id
53 .get_mut(window_id)
54 .is_some_and(|bindings| {
55 bindings.unset(hook, index);
56 bindings.is_empty()
57 });
58 if remove_scope {
59 self.windows_by_id.remove(window_id);
60 }
61 }
62 HookScopeIdentity::Pane { pane_id, .. } => {
63 let remove_scope = self.panes_by_id.get_mut(pane_id).is_some_and(|bindings| {
64 bindings.unset(hook, index);
65 bindings.is_empty()
66 });
67 if remove_scope {
68 self.panes_by_id.remove(pane_id);
69 }
70 }
71 }
72 }
73
74 #[must_use]
76 pub fn bindings_view_with_identity(
77 &self,
78 scope: &HookScopeIdentity,
79 hook: Option<HookName>,
80 ) -> Vec<HookBindingView> {
81 match scope {
82 HookScopeIdentity::Global => Vec::new(),
83 HookScopeIdentity::Session(session_name) => self
84 .sessions
85 .get(session_name)
86 .map_or_else(Vec::new, |bindings| bindings.views(hook)),
87 HookScopeIdentity::Window { window_id, .. } => self
88 .windows_by_id
89 .get(window_id)
90 .map_or_else(Vec::new, |bindings| bindings.views(hook)),
91 HookScopeIdentity::Pane { pane_id, .. } => self
92 .panes_by_id
93 .get(pane_id)
94 .map_or_else(Vec::new, |bindings| bindings.views(hook)),
95 }
96 }
97
98 #[must_use]
100 pub fn dispatch_with_identity(
101 &mut self,
102 scope: &HookScopeIdentity,
103 hook: HookName,
104 ) -> Vec<HookDispatch> {
105 match hook_class(hook) {
106 HookClass::Session => self.dispatch_identity_session(scope, hook),
107 HookClass::Window => self.dispatch_identity_window(scope, hook),
108 HookClass::Pane => self.dispatch_identity_pane(scope, hook),
109 }
110 }
111
112 #[must_use]
115 pub fn dispatch_with_identity_or_scope(
116 &mut self,
117 identity: &HookScopeIdentity,
118 scope: &ScopeSelector,
119 hook: HookName,
120 ) -> Vec<HookDispatch> {
121 let (resolved_identity, _) = self.resolved_identity_binding(identity, hook);
122 if !matches!(resolved_identity, HookScopeIdentity::Global) {
123 return self.dispatch_with_identity(identity, hook);
124 }
125 let (resolved_scope, _) = self.resolved_dispatch_binding(scope, hook);
126 if !matches!(resolved_scope, ScopeSelector::Global) {
127 return self.dispatch(scope, hook);
128 }
129 self.dispatch_with_identity(identity, hook)
130 }
131
132 pub fn dispatch_deferred_with_identity(
135 &mut self,
136 snapshot: &mut Self,
137 scope: &HookScopeIdentity,
138 hook: HookName,
139 ) -> Vec<HookDispatch> {
140 let (resolved_scope, one_shots) = snapshot.resolved_identity_binding(scope, hook);
141 let dispatches = snapshot.dispatch_with_identity(scope, hook);
142 if dispatches.is_empty() {
143 return dispatches;
144 }
145 for index in one_shots {
146 self.unset_with_identity(&resolved_scope, hook, Some(index));
147 }
148 dispatches
149 }
150
151 pub fn dispatch_deferred_with_identity_or_scope(
153 &mut self,
154 snapshot: &mut Self,
155 identity: &HookScopeIdentity,
156 scope: &ScopeSelector,
157 hook: HookName,
158 ) -> Vec<HookDispatch> {
159 let (resolved_identity, _) = snapshot.resolved_identity_binding(identity, hook);
160 if !matches!(resolved_identity, HookScopeIdentity::Global) {
161 return self.dispatch_deferred_with_identity(snapshot, identity, hook);
162 }
163 let (resolved_scope, _) = snapshot.resolved_dispatch_binding(scope, hook);
164 if !matches!(resolved_scope, ScopeSelector::Global) {
165 return self.dispatch_deferred_from(snapshot, scope, hook);
166 }
167 self.dispatch_deferred_with_identity(snapshot, identity, hook)
168 }
169
170 pub fn retain_identities(
172 &mut self,
173 window_ids: &HashSet<WindowId>,
174 pane_ids: &HashSet<PaneId>,
175 ) {
176 self.windows_by_id
177 .retain(|window_id, _| window_ids.contains(window_id));
178 self.panes_by_id
179 .retain(|pane_id, _| pane_ids.contains(pane_id));
180 }
181
182 pub fn replace_identity_aliases(
185 &mut self,
186 window_aliases: HashMap<WindowTarget, WindowId>,
187 pane_aliases: HashMap<PaneTarget, (WindowId, PaneId)>,
188 ) {
189 let legacy_windows = std::mem::take(&mut self.windows);
190 for (target, bindings) in legacy_windows {
191 if let Some(window_id) = window_aliases.get(&target) {
192 self.windows_by_id.entry(*window_id).or_insert(bindings);
193 } else {
194 self.windows.insert(target, bindings);
195 }
196 }
197 let legacy_panes = std::mem::take(&mut self.panes);
198 for (target, bindings) in legacy_panes {
199 if let Some((_, pane_id)) = pane_aliases.get(&target) {
200 self.panes_by_id.entry(*pane_id).or_insert(bindings);
201 } else {
202 self.panes.insert(target, bindings);
203 }
204 }
205
206 let window_ids = window_aliases.values().copied().collect::<HashSet<_>>();
207 let pane_ids = pane_aliases
208 .values()
209 .map(|(_, pane_id)| *pane_id)
210 .collect::<HashSet<_>>();
211 self.retain_identities(&window_ids, &pane_ids);
212 self.window_aliases = window_aliases;
213 self.pane_aliases = pane_aliases;
214 }
215
216 #[must_use]
218 pub fn window_command_by_id(&self, window_id: WindowId, hook: HookName) -> Option<&str> {
219 self.windows_by_id
220 .get(&window_id)
221 .and_then(|bindings| bindings.command(hook))
222 }
223
224 #[must_use]
226 pub fn pane_command_by_id(&self, pane_id: PaneId, hook: HookName) -> Option<&str> {
227 self.panes_by_id
228 .get(&pane_id)
229 .and_then(|bindings| bindings.command(hook))
230 }
231
232 fn identity_bindings_mut(
233 &mut self,
234 scope: &HookScopeIdentity,
235 hook: HookName,
236 ) -> &mut HookBindings {
237 match scope {
238 HookScopeIdentity::Global => self.global_bindings_mut(root_for_hook(hook)),
239 HookScopeIdentity::Session(session_name) => {
240 self.sessions.entry(session_name.clone()).or_default()
241 }
242 HookScopeIdentity::Window { window_id, .. } => {
243 self.windows_by_id.entry(*window_id).or_default()
244 }
245 HookScopeIdentity::Pane { pane_id, .. } => {
246 self.panes_by_id.entry(*pane_id).or_default()
247 }
248 }
249 }
250
251 fn dispatch_identity_session(
252 &mut self,
253 scope: &HookScopeIdentity,
254 hook: HookName,
255 ) -> Vec<HookDispatch> {
256 if let Some(session_name) = scope.session_name() {
257 let (dispatches, remove_scope) =
258 self.sessions
259 .get_mut(session_name)
260 .map_or((Vec::new(), false), |bindings| {
261 let dispatches = bindings.dispatch(hook);
262 (dispatches, bindings.is_empty())
263 });
264 if remove_scope {
265 self.sessions.remove(session_name);
266 }
267 if !dispatches.is_empty() {
268 return dispatches;
269 }
270 }
271 self.session_global.dispatch(hook)
272 }
273
274 fn dispatch_identity_window(
275 &mut self,
276 scope: &HookScopeIdentity,
277 hook: HookName,
278 ) -> Vec<HookDispatch> {
279 let window_id = match scope {
280 HookScopeIdentity::Window { window_id, .. }
281 | HookScopeIdentity::Pane { window_id, .. } => Some(*window_id),
282 HookScopeIdentity::Global | HookScopeIdentity::Session(_) => None,
283 };
284 if let Some(window_id) = window_id {
285 let (dispatches, remove_scope) =
286 self.windows_by_id
287 .get_mut(&window_id)
288 .map_or((Vec::new(), false), |bindings| {
289 let dispatches = bindings.dispatch(hook);
290 (dispatches, bindings.is_empty())
291 });
292 if remove_scope {
293 self.windows_by_id.remove(&window_id);
294 }
295 if !dispatches.is_empty() {
296 return dispatches;
297 }
298 }
299 self.window_global.dispatch(hook)
300 }
301
302 fn dispatch_identity_pane(
303 &mut self,
304 scope: &HookScopeIdentity,
305 hook: HookName,
306 ) -> Vec<HookDispatch> {
307 if let HookScopeIdentity::Pane { pane_id, .. } = scope {
308 let (dispatches, remove_scope) =
309 self.panes_by_id
310 .get_mut(pane_id)
311 .map_or((Vec::new(), false), |bindings| {
312 let dispatches = bindings.dispatch(hook);
313 (dispatches, bindings.is_empty())
314 });
315 if remove_scope {
316 self.panes_by_id.remove(pane_id);
317 }
318 if !dispatches.is_empty() {
319 return dispatches;
320 }
321 }
322 self.dispatch_identity_window(scope, hook)
323 }
324
325 fn resolved_identity_binding(
326 &self,
327 scope: &HookScopeIdentity,
328 hook: HookName,
329 ) -> (HookScopeIdentity, Vec<u32>) {
330 if hook_class(hook) == HookClass::Pane {
331 if let HookScopeIdentity::Pane {
332 session_name,
333 window_id,
334 ..
335 } = scope
336 {
337 if self.identity_scope_has_binding(scope, hook) {
338 return (
339 scope.clone(),
340 self.one_shot_indices_for_identity(scope, hook),
341 );
342 }
343 let window_scope = HookScopeIdentity::Window {
344 session_name: session_name.clone(),
345 window_id: *window_id,
346 };
347 if self.identity_scope_has_binding(&window_scope, hook) {
348 let indices = self.one_shot_indices_for_identity(&window_scope, hook);
349 return (window_scope, indices);
350 }
351 }
352 let global = HookScopeIdentity::Global;
353 let indices = self.one_shot_indices_for_identity(&global, hook);
354 return (global, indices);
355 }
356
357 let local_scope = match hook_class(hook) {
358 HookClass::Session => scope
359 .session_name()
360 .cloned()
361 .map(HookScopeIdentity::Session),
362 HookClass::Window => match scope {
363 HookScopeIdentity::Window {
364 session_name,
365 window_id,
366 }
367 | HookScopeIdentity::Pane {
368 session_name,
369 window_id,
370 ..
371 } => Some(HookScopeIdentity::Window {
372 session_name: session_name.clone(),
373 window_id: *window_id,
374 }),
375 HookScopeIdentity::Global | HookScopeIdentity::Session(_) => None,
376 },
377 HookClass::Pane => unreachable!("pane hooks handled above"),
378 };
379
380 if let Some(local_scope) = local_scope {
381 let indices = self.one_shot_indices_for_identity(&local_scope, hook);
382 if self.identity_scope_has_binding(&local_scope, hook) {
383 return (local_scope, indices);
384 }
385 }
386
387 let global = HookScopeIdentity::Global;
388 let indices = self.one_shot_indices_for_identity(&global, hook);
389 (global, indices)
390 }
391
392 fn identity_scope_has_binding(&self, scope: &HookScopeIdentity, hook: HookName) -> bool {
393 match scope {
394 HookScopeIdentity::Global => self.global_bindings(root_for_hook(hook)),
395 HookScopeIdentity::Session(session_name) => match self.sessions.get(session_name) {
396 Some(bindings) => bindings,
397 None => return false,
398 },
399 HookScopeIdentity::Window { window_id, .. } => {
400 match self.windows_by_id.get(window_id) {
401 Some(bindings) => bindings,
402 None => return false,
403 }
404 }
405 HookScopeIdentity::Pane { pane_id, .. } => match self.panes_by_id.get(pane_id) {
406 Some(bindings) => bindings,
407 None => return false,
408 },
409 }
410 .command(hook)
411 .is_some()
412 }
413
414 fn one_shot_indices_for_identity(&self, scope: &HookScopeIdentity, hook: HookName) -> Vec<u32> {
415 match scope {
416 HookScopeIdentity::Global => self
417 .global_bindings(root_for_hook(hook))
418 .one_shot_indices(hook),
419 HookScopeIdentity::Session(session_name) => self
420 .sessions
421 .get(session_name)
422 .map_or_else(Vec::new, |bindings| bindings.one_shot_indices(hook)),
423 HookScopeIdentity::Window { window_id, .. } => self
424 .windows_by_id
425 .get(window_id)
426 .map_or_else(Vec::new, |bindings| bindings.one_shot_indices(hook)),
427 HookScopeIdentity::Pane { pane_id, .. } => self
428 .panes_by_id
429 .get(pane_id)
430 .map_or_else(Vec::new, |bindings| bindings.one_shot_indices(hook)),
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 fn session_name(value: &str) -> SessionName {
440 SessionName::new(value).expect("valid session name")
441 }
442
443 fn pane_scope(session: &str) -> HookScopeIdentity {
444 HookScopeIdentity::Pane {
445 session_name: session_name(session),
446 window_id: WindowId::new(7),
447 pane_id: PaneId::new(11),
448 }
449 }
450
451 #[test]
452 fn aliases_share_one_pane_binding_and_one_shot_consumption() {
453 let mut store = HookStore::new();
454 store.set_with_identity(
455 pane_scope("owner"),
456 HookName::PaneExited,
457 "set-option @once yes".to_owned(),
458 HookLifecycle::OneShot,
459 HookSetOptions::default(),
460 );
461
462 let first = store.dispatch_with_identity(&pane_scope("peer"), HookName::PaneExited);
463 let second = store.dispatch_with_identity(&pane_scope("owner"), HookName::PaneExited);
464
465 assert_eq!(first.len(), 1);
466 assert!(second.is_empty());
467 }
468
469 #[test]
470 fn window_aliases_share_bindings_but_session_hooks_do_not() {
471 let owner = HookScopeIdentity::Window {
472 session_name: session_name("owner"),
473 window_id: WindowId::new(7),
474 };
475 let peer = HookScopeIdentity::Window {
476 session_name: session_name("peer"),
477 window_id: WindowId::new(7),
478 };
479 let mut store = HookStore::new();
480 store.set_with_identity(
481 owner,
482 HookName::WindowLayoutChanged,
483 "set-option @window yes".to_owned(),
484 HookLifecycle::Persistent,
485 HookSetOptions::default(),
486 );
487 store.set_with_identity(
488 HookScopeIdentity::Session(session_name("owner")),
489 HookName::ClientAttached,
490 "set-option @session yes".to_owned(),
491 HookLifecycle::Persistent,
492 HookSetOptions::default(),
493 );
494
495 assert_eq!(
496 store
497 .dispatch_with_identity(&peer, HookName::WindowLayoutChanged)
498 .len(),
499 1
500 );
501 assert!(store
502 .dispatch_with_identity(&peer, HookName::ClientAttached)
503 .is_empty());
504 }
505
506 #[test]
507 fn retaining_live_ids_purges_destroyed_objects_only() {
508 let mut store = HookStore::new();
509 for pane_id in [PaneId::new(1), PaneId::new(2)] {
510 store.set_with_identity(
511 HookScopeIdentity::Pane {
512 session_name: session_name("owner"),
513 window_id: WindowId::new(pane_id.as_u32()),
514 pane_id,
515 },
516 HookName::PaneExited,
517 format!("set-option @pane{} yes", pane_id.as_u32()),
518 HookLifecycle::Persistent,
519 HookSetOptions::default(),
520 );
521 }
522
523 store.retain_identities(
524 &HashSet::from([WindowId::new(2)]),
525 &HashSet::from([PaneId::new(2)]),
526 );
527
528 assert_eq!(
529 store.pane_command_by_id(PaneId::new(1), HookName::PaneExited),
530 None
531 );
532 assert!(store
533 .pane_command_by_id(PaneId::new(2), HookName::PaneExited)
534 .is_some());
535 }
536}