1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use rmux_proto::types::OptionScopeSelector;
4use rmux_proto::{
5 OptionName, PaneTarget, RmuxError, ScopeSelector, SessionName, SetOptionMode, WindowTarget,
6};
7
8#[path = "options/access.rs"]
9mod access;
10#[path = "options/mutation.rs"]
11mod mutation;
12#[path = "options/registry.rs"]
13mod registry;
14#[path = "options/render.rs"]
15mod render;
16#[path = "options/scope.rs"]
17mod scope;
18#[path = "options/show.rs"]
19mod show;
20#[path = "options/storage.rs"]
21mod storage;
22
23use mutation::{
24 apply_array_mutation, build_mutation_outcome, is_global_scope, legacy_scope_for_option,
25 normalize_scalar_value,
26};
27pub use mutation::{validate_option_mutation, validate_option_name_mutation};
28pub use registry::{
29 option_affects_alerts, option_affects_rendering, option_name_by_name, resolve_option_name,
30 resolve_option_name_typed, OptionLookupError, OptionQuery,
31};
32use registry::{option_metadata, OptionChangeMask, OptionValueType};
33pub use scope::default_global_scope_for_option_name;
34use storage::{OptionEntry, OptionNode};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ShowOptionsMode {
39 Resolved,
41 ResolvedWithInheritanceMarkers,
43 Explicit,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct OptionNotification {
50 pub name: String,
52 pub scope: OptionScopeSelector,
54 pub effects: OptionChangeMask,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct OptionMutationOutcome {
61 pub name: String,
63 pub scope: OptionScopeSelector,
65 pub known_option: Option<OptionName>,
67 pub old_explicit: Option<String>,
69 pub new_explicit: Option<String>,
71 pub changed: bool,
76 pub notifications: Vec<OptionNotification>,
78 pub related: Vec<OptionMutationOutcome>,
80}
81
82type SessionOptions = HashMap<SessionName, OptionNode>;
83type WindowOptions = HashMap<WindowTarget, OptionNode>;
84type PaneOptions = HashMap<PaneTarget, OptionNode>;
85
86#[derive(Debug, Clone, PartialEq, Eq, Default)]
88pub struct OptionStore {
89 server_global: OptionNode,
90 session_global: OptionNode,
91 window_global: OptionNode,
92 sessions: SessionOptions,
93 windows: WindowOptions,
94 panes: PaneOptions,
95}
96
97impl OptionStore {
98 #[must_use]
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 #[must_use]
106 pub fn is_empty(&self) -> bool {
107 self.server_global.is_empty()
108 && self.session_global.is_empty()
109 && self.window_global.is_empty()
110 && self.sessions.values().all(OptionNode::is_empty)
111 && self.windows.values().all(OptionNode::is_empty)
112 && self.panes.values().all(OptionNode::is_empty)
113 }
114
115 pub fn set(
117 &mut self,
118 scope: ScopeSelector,
119 option: OptionName,
120 value: String,
121 mode: SetOptionMode,
122 ) -> Result<OptionMutationOutcome, RmuxError> {
123 let explicit_scope = legacy_scope_for_option(option, &scope);
124 self.set_by_name(
125 explicit_scope,
126 option_metadata(option).name(),
127 Some(value),
128 mode,
129 false,
130 false,
131 false,
132 )
133 }
134
135 #[allow(clippy::too_many_arguments)]
137 pub fn set_by_name(
138 &mut self,
139 scope: OptionScopeSelector,
140 name: &str,
141 value: Option<String>,
142 mode: SetOptionMode,
143 only_if_unset: bool,
144 unset: bool,
145 unset_pane_overrides: bool,
146 ) -> Result<OptionMutationOutcome, RmuxError> {
147 let query = validate_option_name_mutation(name, &scope, mode, value.as_deref(), unset)?;
152 let unset_pane_scope = (unset_pane_overrides
153 && matches!(scope, OptionScopeSelector::Window(_)))
154 .then(|| scope.clone());
155
156 let mut outcome = if unset {
157 self.unset_query(scope, &query, only_if_unset)
158 } else {
159 self.set_query(scope, &query, value.as_deref(), mode, only_if_unset)
160 }?;
161 let related = if let Some(scope) = unset_pane_scope {
162 self.unset_window_pane_overrides(&scope, &query)
163 } else {
164 Vec::new()
165 };
166 outcome.related.extend(related);
167 Ok(outcome)
168 }
169
170 pub fn remove_session(
172 &mut self,
173 session_name: &SessionName,
174 ) -> Option<HashMap<OptionName, String>> {
175 self.windows
176 .retain(|target, _| target.session_name() != session_name);
177 self.panes
178 .retain(|target, _| target.session_name() != session_name);
179 self.sessions
180 .remove(session_name)
181 .map(|node| node.into_known_values())
182 }
183
184 pub fn rename_session(
186 &mut self,
187 session_name: &SessionName,
188 new_name: SessionName,
189 ) -> Result<(), RmuxError> {
190 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
191 for (name, values) in &self.sessions {
192 let next_name = if name == session_name {
193 new_name.clone()
194 } else {
195 name.clone()
196 };
197 if renamed_sessions
198 .insert(next_name.clone(), values.clone())
199 .is_some()
200 {
201 return Err(RmuxError::Server(format!(
202 "session options already exist for session {next_name}"
203 )));
204 }
205 }
206
207 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
208 for (target, values) in &self.windows {
209 let next_target = if target.session_name() == session_name {
210 WindowTarget::with_window(new_name.clone(), target.window_index())
211 } else {
212 target.clone()
213 };
214 if renamed_windows
215 .insert(next_target.clone(), values.clone())
216 .is_some()
217 {
218 return Err(RmuxError::Server(format!(
219 "window options already exist for {next_target}"
220 )));
221 }
222 }
223
224 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
225 for (target, values) in &self.panes {
226 let next_target = if target.session_name() == session_name {
227 PaneTarget::with_window(
228 new_name.clone(),
229 target.window_index(),
230 target.pane_index(),
231 )
232 } else {
233 target.clone()
234 };
235 if renamed_panes
236 .insert(next_target.clone(), values.clone())
237 .is_some()
238 {
239 return Err(RmuxError::Server(format!(
240 "pane options already exist for {next_target}"
241 )));
242 }
243 }
244
245 self.sessions = renamed_sessions;
246 self.windows = renamed_windows;
247 self.panes = renamed_panes;
248 Ok(())
249 }
250
251 pub fn remove_window(&mut self, target: &WindowTarget) -> Option<HashMap<OptionName, String>> {
253 self.panes.retain(|pane_target, _| {
254 pane_target.session_name() != target.session_name()
255 || pane_target.window_index() != target.window_index()
256 });
257 self.windows
258 .remove(target)
259 .map(OptionNode::into_known_values)
260 }
261
262 pub fn copy_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
264 if let Some(source_window) = self.windows.get(source).cloned() {
265 self.windows.insert(
266 target.clone(),
267 source_window.with_scope(OptionScopeSelector::Window(target.clone())),
268 );
269 } else {
270 self.windows.remove(target);
271 }
272
273 self.panes.retain(|pane_target, _| {
274 pane_target.session_name() != target.session_name()
275 || pane_target.window_index() != target.window_index()
276 });
277
278 let source_panes = self
279 .panes
280 .iter()
281 .filter(|(pane_target, _)| {
282 pane_target.session_name() == source.session_name()
283 && pane_target.window_index() == source.window_index()
284 })
285 .map(|(pane_target, node)| {
286 let target_pane = PaneTarget::with_window(
287 target.session_name().clone(),
288 target.window_index(),
289 pane_target.pane_index(),
290 );
291 (
292 target_pane.clone(),
293 node.clone()
294 .with_scope(OptionScopeSelector::Pane(target_pane)),
295 )
296 })
297 .collect::<Vec<_>>();
298
299 self.panes.extend(source_panes);
300 }
301
302 pub fn swap_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
304 if source == target {
305 return;
306 }
307
308 let source_window = self.windows.remove(source);
309 let target_window = self.windows.remove(target);
310 if let Some(node) = source_window {
311 self.windows.insert(
312 target.clone(),
313 node.with_scope(OptionScopeSelector::Window(target.clone())),
314 );
315 }
316 if let Some(node) = target_window {
317 self.windows.insert(
318 source.clone(),
319 node.with_scope(OptionScopeSelector::Window(source.clone())),
320 );
321 }
322
323 let source_panes = remove_window_pane_options(&mut self.panes, source);
324 let target_panes = remove_window_pane_options(&mut self.panes, target);
325 self.panes
326 .extend(rekey_pane_options(source_panes, source, target));
327 self.panes
328 .extend(rekey_pane_options(target_panes, target, source));
329 }
330
331 pub fn move_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
333 if source == target {
334 return;
335 }
336
337 let source_window = self.windows.remove(source);
338 let _ = self.windows.remove(target);
339 if let Some(node) = source_window {
340 self.windows.insert(
341 target.clone(),
342 node.with_scope(OptionScopeSelector::Window(target.clone())),
343 );
344 }
345
346 let source_panes = remove_window_pane_options(&mut self.panes, source);
347 let _ = remove_window_pane_options(&mut self.panes, target);
348 self.panes
349 .extend(rekey_pane_options(source_panes, source, target));
350 }
351
352 pub fn remove_pane(&mut self, target: &PaneTarget) -> Option<HashMap<OptionName, String>> {
354 self.panes.remove(target).map(OptionNode::into_known_values)
355 }
356
357 pub fn copy_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
359 if source == target {
360 return;
361 }
362 if let Some(source_pane) = self.panes.get(source).cloned() {
363 self.panes.insert(
364 target.clone(),
365 source_pane.with_scope(OptionScopeSelector::Pane(target.clone())),
366 );
367 } else {
368 self.panes.remove(target);
369 }
370 }
371
372 pub fn remap_pane_indices(
374 &mut self,
375 session_name: &SessionName,
376 window_index: u32,
377 index_map: &BTreeMap<u32, u32>,
378 ) -> Result<(), RmuxError> {
379 if index_map.is_empty() {
380 return Ok(());
381 }
382
383 let mut remapped_panes = HashMap::with_capacity(self.panes.len());
384 for (target, values) in &self.panes {
385 let next_target =
386 remapped_pane_index_target(target, session_name, window_index, index_map);
387 if remapped_panes
388 .insert(
389 next_target.clone(),
390 values
391 .clone()
392 .with_scope(OptionScopeSelector::Pane(next_target.clone())),
393 )
394 .is_some()
395 {
396 return Err(RmuxError::Server(format!(
397 "pane options already exist for {next_target}"
398 )));
399 }
400 }
401
402 self.panes = remapped_panes;
403 Ok(())
404 }
405
406 pub fn transfer_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
408 self.rekey_pane_overrides(&[(source.clone(), Some(target.clone()))])
409 .expect("single pane override transfer cannot collide");
410 }
411
412 pub fn swap_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
414 self.rekey_pane_overrides(&[
415 (source.clone(), Some(target.clone())),
416 (target.clone(), Some(source.clone())),
417 ])
418 .expect("two-way pane override swap cannot collide");
419 }
420
421 pub fn rekey_pane_overrides(
423 &mut self,
424 mappings: &[(PaneTarget, Option<PaneTarget>)],
425 ) -> Result<(), RmuxError> {
426 if mappings.is_empty() {
427 return Ok(());
428 }
429
430 let mut targets = HashSet::new();
431 for (_, target) in mappings {
432 if let Some(target) = target {
433 if !targets.insert(target.clone()) {
434 return Err(RmuxError::Server(format!(
435 "pane options remap has duplicate destination {target}"
436 )));
437 }
438 }
439 }
440
441 let mut removed = Vec::with_capacity(mappings.len());
442 for (source, _) in mappings {
443 if let Some(node) = self.panes.remove(source) {
444 removed.push((source.clone(), node));
445 }
446 }
447
448 for (_, target) in mappings {
449 if let Some(target) = target {
450 let _ = self.panes.remove(target);
451 }
452 }
453
454 for (source, node) in removed {
455 let Some((_, Some(target))) = mappings
456 .iter()
457 .find(|(mapped_source, _)| *mapped_source == source)
458 else {
459 continue;
460 };
461 if self
462 .panes
463 .insert(
464 target.clone(),
465 node.with_scope(OptionScopeSelector::Pane(target.clone())),
466 )
467 .is_some()
468 {
469 return Err(RmuxError::Server(format!(
470 "pane options already exist for {target}"
471 )));
472 }
473 }
474 Ok(())
475 }
476
477 pub fn remap_session_window_indices(
479 &mut self,
480 session_name: &SessionName,
481 index_map: &BTreeMap<u32, u32>,
482 ) -> Result<(), RmuxError> {
483 let mut remapped_windows = HashMap::with_capacity(self.windows.len());
484 for (target, values) in &self.windows {
485 let next_target = remapped_window_target(target, session_name, index_map);
486 if remapped_windows
487 .insert(next_target.clone(), values.clone())
488 .is_some()
489 {
490 return Err(RmuxError::Server(format!(
491 "window options already exist for {next_target}"
492 )));
493 }
494 }
495
496 let mut remapped_panes = HashMap::with_capacity(self.panes.len());
497 for (target, values) in &self.panes {
498 let next_target = remapped_pane_target(target, session_name, index_map);
499 if remapped_panes
500 .insert(next_target.clone(), values.clone())
501 .is_some()
502 {
503 return Err(RmuxError::Server(format!(
504 "pane options already exist for {next_target}"
505 )));
506 }
507 }
508
509 self.windows = remapped_windows;
510 self.panes = remapped_panes;
511 Ok(())
512 }
513
514 fn set_query(
515 &mut self,
516 scope: OptionScopeSelector,
517 query: &OptionQuery,
518 value: Option<&str>,
519 mode: SetOptionMode,
520 only_if_unset: bool,
521 ) -> Result<OptionMutationOutcome, RmuxError> {
522 let effective_before = self
523 .effective_value_for_scope(&scope, query)
524 .or_else(|| self.default_value_as_string(query));
525 let explicit_before = self.explicit_value_for_scope(&scope, query);
526 let default_entry = self.default_entry_for_scope(query, scope.clone());
527 let node = self.node_for_exact_scope_mut(&scope);
528 if only_if_unset && explicit_before.is_some() {
529 return Err(RmuxError::InvalidSetOption(format!(
530 "{} is already set",
531 query.canonical_name()
532 )));
533 }
534
535 if query.is_array()
536 && mode == SetOptionMode::Append
537 && query.index().is_none()
538 && value.unwrap_or_default().is_empty()
539 {
540 return Ok(build_mutation_outcome(
541 query,
542 scope,
543 explicit_before.clone(),
544 explicit_before,
545 ));
546 }
547
548 if query.is_array() {
549 let entry = node
550 .entries
551 .entry(query.canonical_name().to_owned())
552 .or_insert_with(|| {
553 if is_global_scope(&scope) {
554 default_entry.unwrap_or_else(|| {
555 OptionEntry::new_empty_array(
556 query.canonical_name(),
557 query.known_option(),
558 scope.clone(),
559 query.value_type(),
560 )
561 })
562 } else {
563 OptionEntry::new_empty_array(
564 query.canonical_name(),
565 query.known_option(),
566 scope.clone(),
567 query.value_type(),
568 )
569 }
570 });
571 apply_array_mutation(
572 entry,
573 query,
574 value.unwrap_or_default(),
575 mode,
576 explicit_before.as_deref(),
577 )?;
578 } else {
579 let current = match (query.value_type(), mode) {
580 (OptionValueType::String, SetOptionMode::Append) => {
581 if explicit_before.is_some() || is_global_scope(&scope) {
582 explicit_before.clone().or_else(|| effective_before.clone())
583 } else {
584 None
585 }
586 }
587 (OptionValueType::String, SetOptionMode::Replace) => None,
588 _ => effective_before.clone(),
589 };
590 let normalized = normalize_scalar_value(query, value, current.as_deref())?;
591 node.entries.insert(
592 query.canonical_name().to_owned(),
593 OptionEntry::new_scalar(query, scope.clone(), normalized),
594 );
595 }
596
597 let explicit_after = self.explicit_value_for_scope(&scope, query);
598 Ok(build_mutation_outcome(
599 query,
600 scope,
601 explicit_before,
602 explicit_after,
603 ))
604 }
605
606 fn unset_query(
607 &mut self,
608 scope: OptionScopeSelector,
609 query: &OptionQuery,
610 only_if_unset: bool,
611 ) -> Result<OptionMutationOutcome, RmuxError> {
612 let explicit_before = self.explicit_value_for_scope(&scope, query);
613 let default_entry = self.default_entry_for_scope(query, scope.clone());
614 let node = self.node_for_exact_scope_mut(&scope);
615 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
616 return Err(RmuxError::InvalidSetOption(format!(
617 "{} is already set",
618 query.canonical_name()
619 )));
620 }
621
622 if query.is_array() && query.index().is_some() {
623 let remove_node = if let Some(entry) = node.entries.get_mut(query.canonical_name()) {
624 entry.remove_array_index(query.index().unwrap(), query.separator());
625 entry.is_empty()
626 } else {
627 false
628 };
629 if remove_node {
630 node.entries.remove(query.canonical_name());
631 }
632 } else if is_global_scope(&scope) {
633 if let Some(default_entry) = default_entry {
634 node.entries
635 .insert(query.canonical_name().to_owned(), default_entry);
636 } else {
637 node.entries.remove(query.canonical_name());
638 }
639 } else {
640 node.entries.remove(query.canonical_name());
641 }
642
643 let explicit_after = self.explicit_value_for_scope(&scope, query);
644 Ok(build_mutation_outcome(
645 query,
646 scope,
647 explicit_before,
648 explicit_after,
649 ))
650 }
651
652 fn unset_window_pane_overrides(
653 &mut self,
654 scope: &OptionScopeSelector,
655 query: &OptionQuery,
656 ) -> Vec<OptionMutationOutcome> {
657 let OptionScopeSelector::Window(target) = scope else {
658 return Vec::new();
659 };
660 let name = query.canonical_name();
661 let mut outcomes = Vec::new();
662 self.panes.retain(|pane_target, node| {
663 let matches_window = pane_target.session_name() == target.session_name()
664 && pane_target.window_index() == target.window_index();
665 if matches_window {
666 if let Some(old_explicit) = node
667 .entries
668 .get(name)
669 .and_then(|entry| entry.value(query.index()))
670 .map(str::to_owned)
671 {
672 outcomes.push(build_mutation_outcome(
673 query,
674 OptionScopeSelector::Pane(pane_target.clone()),
675 Some(old_explicit),
676 None,
677 ));
678 }
679 node.entries.remove(name);
680 }
681 !node.is_empty()
682 });
683 outcomes
684 }
685}
686
687fn remove_window_pane_options(
688 panes: &mut PaneOptions,
689 window: &WindowTarget,
690) -> Vec<(PaneTarget, OptionNode)> {
691 let pane_targets = panes
692 .keys()
693 .filter(|pane_target| {
694 pane_target.session_name() == window.session_name()
695 && pane_target.window_index() == window.window_index()
696 })
697 .cloned()
698 .collect::<Vec<_>>();
699 pane_targets
700 .into_iter()
701 .filter_map(|pane_target| panes.remove(&pane_target).map(|node| (pane_target, node)))
702 .collect()
703}
704
705fn rekey_pane_options(
706 panes: Vec<(PaneTarget, OptionNode)>,
707 source: &WindowTarget,
708 target: &WindowTarget,
709) -> Vec<(PaneTarget, OptionNode)> {
710 panes
711 .into_iter()
712 .map(move |(pane_target, node)| {
713 let next_target = PaneTarget::with_window(
714 target.session_name().clone(),
715 target.window_index(),
716 pane_target.pane_index(),
717 );
718 let next_node = node.with_scope(OptionScopeSelector::Pane(next_target.clone()));
719 debug_assert_eq!(pane_target.session_name(), source.session_name());
720 debug_assert_eq!(pane_target.window_index(), source.window_index());
721 (next_target, next_node)
722 })
723 .collect()
724}
725
726fn remapped_window_target(
727 target: &WindowTarget,
728 session_name: &SessionName,
729 index_map: &BTreeMap<u32, u32>,
730) -> WindowTarget {
731 if target.session_name() != session_name {
732 return target.clone();
733 }
734 index_map.get(&target.window_index()).copied().map_or_else(
735 || target.clone(),
736 |window_index| WindowTarget::with_window(session_name.clone(), window_index),
737 )
738}
739
740fn remapped_pane_target(
741 target: &PaneTarget,
742 session_name: &SessionName,
743 index_map: &BTreeMap<u32, u32>,
744) -> PaneTarget {
745 if target.session_name() != session_name {
746 return target.clone();
747 }
748 index_map.get(&target.window_index()).copied().map_or_else(
749 || target.clone(),
750 |window_index| {
751 PaneTarget::with_window(session_name.clone(), window_index, target.pane_index())
752 },
753 )
754}
755
756fn remapped_pane_index_target(
757 target: &PaneTarget,
758 session_name: &SessionName,
759 window_index: u32,
760 index_map: &BTreeMap<u32, u32>,
761) -> PaneTarget {
762 if target.session_name() != session_name || target.window_index() != window_index {
763 return target.clone();
764 }
765 index_map.get(&target.pane_index()).copied().map_or_else(
766 || target.clone(),
767 |pane_index| PaneTarget::with_window(session_name.clone(), window_index, pane_index),
768 )
769}
770
771#[cfg(test)]
772#[path = "options/tests.rs"]
773mod tests;