1use std::collections::{BTreeMap, HashMap};
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 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 known_option: Option<OptionName>,
65 pub notifications: Vec<OptionNotification>,
67}
68
69type SessionOptions = HashMap<SessionName, OptionNode>;
70type WindowOptions = HashMap<WindowTarget, OptionNode>;
71type PaneOptions = HashMap<PaneTarget, OptionNode>;
72
73#[derive(Debug, Clone, PartialEq, Eq, Default)]
75pub struct OptionStore {
76 server_global: OptionNode,
77 session_global: OptionNode,
78 window_global: OptionNode,
79 sessions: SessionOptions,
80 windows: WindowOptions,
81 panes: PaneOptions,
82}
83
84impl OptionStore {
85 #[must_use]
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 #[must_use]
93 pub fn is_empty(&self) -> bool {
94 self.server_global.is_empty()
95 && self.session_global.is_empty()
96 && self.window_global.is_empty()
97 && self.sessions.values().all(OptionNode::is_empty)
98 && self.windows.values().all(OptionNode::is_empty)
99 && self.panes.values().all(OptionNode::is_empty)
100 }
101
102 pub fn set(
104 &mut self,
105 scope: ScopeSelector,
106 option: OptionName,
107 value: String,
108 mode: SetOptionMode,
109 ) -> Result<OptionMutationOutcome, RmuxError> {
110 let explicit_scope = legacy_scope_for_option(option, &scope);
111 self.set_by_name(
112 explicit_scope,
113 option_metadata(option).name(),
114 Some(value),
115 mode,
116 false,
117 false,
118 false,
119 )
120 }
121
122 #[allow(clippy::too_many_arguments)]
124 pub fn set_by_name(
125 &mut self,
126 scope: OptionScopeSelector,
127 name: &str,
128 value: Option<String>,
129 mode: SetOptionMode,
130 only_if_unset: bool,
131 unset: bool,
132 unset_pane_overrides: bool,
133 ) -> Result<OptionMutationOutcome, RmuxError> {
134 if unset_pane_overrides && !matches!(scope, OptionScopeSelector::Window(_)) {
135 return Err(RmuxError::InvalidSetOption(
136 "unset pane overrides only supports window scope".to_owned(),
137 ));
138 }
139
140 let query = validate_option_name_mutation(name, &scope, mode, value.as_deref(), unset)?;
141
142 if unset_pane_overrides {
143 self.unset_window_pane_overrides(&scope, query.canonical_name());
144 }
145
146 if unset {
147 self.unset_query(scope, &query, only_if_unset)
148 } else {
149 self.set_query(scope, &query, value.as_deref(), mode, only_if_unset)
150 }
151 }
152
153 pub fn remove_session(
155 &mut self,
156 session_name: &SessionName,
157 ) -> Option<HashMap<OptionName, String>> {
158 self.windows
159 .retain(|target, _| target.session_name() != session_name);
160 self.panes
161 .retain(|target, _| target.session_name() != session_name);
162 self.sessions
163 .remove(session_name)
164 .map(|node| node.into_known_values())
165 }
166
167 pub fn rename_session(
169 &mut self,
170 session_name: &SessionName,
171 new_name: SessionName,
172 ) -> Result<(), RmuxError> {
173 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
174 for (name, values) in &self.sessions {
175 let next_name = if name == session_name {
176 new_name.clone()
177 } else {
178 name.clone()
179 };
180 if renamed_sessions
181 .insert(next_name.clone(), values.clone())
182 .is_some()
183 {
184 return Err(RmuxError::Server(format!(
185 "session options already exist for session {next_name}"
186 )));
187 }
188 }
189
190 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
191 for (target, values) in &self.windows {
192 let next_target = if target.session_name() == session_name {
193 WindowTarget::with_window(new_name.clone(), target.window_index())
194 } else {
195 target.clone()
196 };
197 if renamed_windows
198 .insert(next_target.clone(), values.clone())
199 .is_some()
200 {
201 return Err(RmuxError::Server(format!(
202 "window options already exist for {next_target}"
203 )));
204 }
205 }
206
207 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
208 for (target, values) in &self.panes {
209 let next_target = if target.session_name() == session_name {
210 PaneTarget::with_window(
211 new_name.clone(),
212 target.window_index(),
213 target.pane_index(),
214 )
215 } else {
216 target.clone()
217 };
218 if renamed_panes
219 .insert(next_target.clone(), values.clone())
220 .is_some()
221 {
222 return Err(RmuxError::Server(format!(
223 "pane options already exist for {next_target}"
224 )));
225 }
226 }
227
228 self.sessions = renamed_sessions;
229 self.windows = renamed_windows;
230 self.panes = renamed_panes;
231 Ok(())
232 }
233
234 pub fn remove_window(&mut self, target: &WindowTarget) -> Option<HashMap<OptionName, String>> {
236 self.panes.retain(|pane_target, _| {
237 pane_target.session_name() != target.session_name()
238 || pane_target.window_index() != target.window_index()
239 });
240 self.windows
241 .remove(target)
242 .map(OptionNode::into_known_values)
243 }
244
245 pub fn copy_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
247 if let Some(source_window) = self.windows.get(source).cloned() {
248 self.windows.insert(
249 target.clone(),
250 source_window.with_scope(OptionScopeSelector::Window(target.clone())),
251 );
252 } else {
253 self.windows.remove(target);
254 }
255
256 self.panes.retain(|pane_target, _| {
257 pane_target.session_name() != target.session_name()
258 || pane_target.window_index() != target.window_index()
259 });
260
261 let source_panes = self
262 .panes
263 .iter()
264 .filter(|(pane_target, _)| {
265 pane_target.session_name() == source.session_name()
266 && pane_target.window_index() == source.window_index()
267 })
268 .map(|(pane_target, node)| {
269 let target_pane = PaneTarget::with_window(
270 target.session_name().clone(),
271 target.window_index(),
272 pane_target.pane_index(),
273 );
274 (
275 target_pane.clone(),
276 node.clone()
277 .with_scope(OptionScopeSelector::Pane(target_pane)),
278 )
279 })
280 .collect::<Vec<_>>();
281
282 self.panes.extend(source_panes);
283 }
284
285 pub fn swap_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
287 if source == target {
288 return;
289 }
290
291 let source_window = self.windows.remove(source);
292 let target_window = self.windows.remove(target);
293 if let Some(node) = source_window {
294 self.windows.insert(
295 target.clone(),
296 node.with_scope(OptionScopeSelector::Window(target.clone())),
297 );
298 }
299 if let Some(node) = target_window {
300 self.windows.insert(
301 source.clone(),
302 node.with_scope(OptionScopeSelector::Window(source.clone())),
303 );
304 }
305
306 let source_panes = remove_window_pane_options(&mut self.panes, source);
307 let target_panes = remove_window_pane_options(&mut self.panes, target);
308 self.panes
309 .extend(rekey_pane_options(source_panes, source, target));
310 self.panes
311 .extend(rekey_pane_options(target_panes, target, source));
312 }
313
314 pub fn move_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
316 if source == target {
317 return;
318 }
319
320 let source_window = self.windows.remove(source);
321 let _ = self.windows.remove(target);
322 if let Some(node) = source_window {
323 self.windows.insert(
324 target.clone(),
325 node.with_scope(OptionScopeSelector::Window(target.clone())),
326 );
327 }
328
329 let source_panes = remove_window_pane_options(&mut self.panes, source);
330 let _ = remove_window_pane_options(&mut self.panes, target);
331 self.panes
332 .extend(rekey_pane_options(source_panes, source, target));
333 }
334
335 pub fn remove_pane(&mut self, target: &PaneTarget) -> Option<HashMap<OptionName, String>> {
337 self.panes.remove(target).map(OptionNode::into_known_values)
338 }
339
340 pub fn remap_session_window_indices(
342 &mut self,
343 session_name: &SessionName,
344 index_map: &BTreeMap<u32, u32>,
345 ) -> Result<(), RmuxError> {
346 let mut remapped_windows = HashMap::with_capacity(self.windows.len());
347 for (target, values) in &self.windows {
348 let next_target = remapped_window_target(target, session_name, index_map);
349 if remapped_windows
350 .insert(next_target.clone(), values.clone())
351 .is_some()
352 {
353 return Err(RmuxError::Server(format!(
354 "window options already exist for {next_target}"
355 )));
356 }
357 }
358
359 let mut remapped_panes = HashMap::with_capacity(self.panes.len());
360 for (target, values) in &self.panes {
361 let next_target = remapped_pane_target(target, session_name, index_map);
362 if remapped_panes
363 .insert(next_target.clone(), values.clone())
364 .is_some()
365 {
366 return Err(RmuxError::Server(format!(
367 "pane options already exist for {next_target}"
368 )));
369 }
370 }
371
372 self.windows = remapped_windows;
373 self.panes = remapped_panes;
374 Ok(())
375 }
376
377 fn set_query(
378 &mut self,
379 scope: OptionScopeSelector,
380 query: &OptionQuery,
381 value: Option<&str>,
382 mode: SetOptionMode,
383 only_if_unset: bool,
384 ) -> Result<OptionMutationOutcome, RmuxError> {
385 let effective_before = self
386 .effective_value_for_scope(&scope, query)
387 .or_else(|| self.default_value_as_string(query));
388 let explicit_before = self.explicit_value_for_scope(&scope, query);
389 let default_entry = self.default_entry_for_scope(query, scope.clone());
390 let node = self.node_for_exact_scope_mut(&scope);
391 if only_if_unset && explicit_before.is_some() {
392 return Err(RmuxError::InvalidSetOption(format!(
393 "{} is already set",
394 query.canonical_name()
395 )));
396 }
397
398 if query.is_array()
399 && mode == SetOptionMode::Append
400 && query.index().is_none()
401 && value.unwrap_or_default().is_empty()
402 {
403 return Ok(build_mutation_outcome(query, scope));
404 }
405
406 if query.is_array() {
407 let entry = node
408 .entries
409 .entry(query.canonical_name().to_owned())
410 .or_insert_with(|| {
411 if is_global_scope(&scope) {
412 default_entry.unwrap_or_else(|| {
413 OptionEntry::new_empty_array(
414 query.canonical_name(),
415 query.known_option(),
416 scope.clone(),
417 query.value_type(),
418 )
419 })
420 } else {
421 OptionEntry::new_empty_array(
422 query.canonical_name(),
423 query.known_option(),
424 scope.clone(),
425 query.value_type(),
426 )
427 }
428 });
429 apply_array_mutation(
430 entry,
431 query,
432 value.unwrap_or_default(),
433 mode,
434 explicit_before.as_deref(),
435 )?;
436 } else {
437 let current = match (query.value_type(), mode) {
438 (OptionValueType::String, SetOptionMode::Append) => {
439 if explicit_before.is_some() || is_global_scope(&scope) {
440 explicit_before.clone().or_else(|| effective_before.clone())
441 } else {
442 None
443 }
444 }
445 (OptionValueType::String, SetOptionMode::Replace) => None,
446 _ => effective_before.clone(),
447 };
448 let normalized = normalize_scalar_value(query, value, current.as_deref())?;
449 node.entries.insert(
450 query.canonical_name().to_owned(),
451 OptionEntry::new_scalar(query, scope.clone(), normalized),
452 );
453 }
454
455 Ok(build_mutation_outcome(query, scope))
456 }
457
458 fn unset_query(
459 &mut self,
460 scope: OptionScopeSelector,
461 query: &OptionQuery,
462 only_if_unset: bool,
463 ) -> Result<OptionMutationOutcome, RmuxError> {
464 let default_entry = self.default_entry_for_scope(query, scope.clone());
465 let node = self.node_for_exact_scope_mut(&scope);
466 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
467 return Err(RmuxError::InvalidSetOption(format!(
468 "{} is already set",
469 query.canonical_name()
470 )));
471 }
472
473 if query.is_array() && query.index().is_some() {
474 let remove_node = if let Some(entry) = node.entries.get_mut(query.canonical_name()) {
475 entry.remove_array_index(query.index().unwrap(), query.separator());
476 entry.is_empty()
477 } else {
478 false
479 };
480 if remove_node {
481 node.entries.remove(query.canonical_name());
482 }
483 } else if is_global_scope(&scope) {
484 if let Some(default_entry) = default_entry {
485 node.entries
486 .insert(query.canonical_name().to_owned(), default_entry);
487 } else {
488 node.entries.remove(query.canonical_name());
489 }
490 } else {
491 node.entries.remove(query.canonical_name());
492 }
493
494 Ok(build_mutation_outcome(query, scope))
495 }
496
497 fn unset_window_pane_overrides(&mut self, scope: &OptionScopeSelector, name: &str) {
498 let OptionScopeSelector::Window(target) = scope else {
499 return;
500 };
501 self.panes.retain(|pane_target, node| {
502 let matches_window = pane_target.session_name() == target.session_name()
503 && pane_target.window_index() == target.window_index();
504 if matches_window {
505 node.entries.remove(name);
506 }
507 !node.is_empty()
508 });
509 }
510}
511
512fn remove_window_pane_options(
513 panes: &mut PaneOptions,
514 window: &WindowTarget,
515) -> Vec<(PaneTarget, OptionNode)> {
516 let pane_targets = panes
517 .keys()
518 .filter(|pane_target| {
519 pane_target.session_name() == window.session_name()
520 && pane_target.window_index() == window.window_index()
521 })
522 .cloned()
523 .collect::<Vec<_>>();
524 pane_targets
525 .into_iter()
526 .filter_map(|pane_target| panes.remove(&pane_target).map(|node| (pane_target, node)))
527 .collect()
528}
529
530fn rekey_pane_options(
531 panes: Vec<(PaneTarget, OptionNode)>,
532 source: &WindowTarget,
533 target: &WindowTarget,
534) -> Vec<(PaneTarget, OptionNode)> {
535 panes
536 .into_iter()
537 .map(move |(pane_target, node)| {
538 let next_target = PaneTarget::with_window(
539 target.session_name().clone(),
540 target.window_index(),
541 pane_target.pane_index(),
542 );
543 let next_node = node.with_scope(OptionScopeSelector::Pane(next_target.clone()));
544 debug_assert_eq!(pane_target.session_name(), source.session_name());
545 debug_assert_eq!(pane_target.window_index(), source.window_index());
546 (next_target, next_node)
547 })
548 .collect()
549}
550
551fn remapped_window_target(
552 target: &WindowTarget,
553 session_name: &SessionName,
554 index_map: &BTreeMap<u32, u32>,
555) -> WindowTarget {
556 if target.session_name() != session_name {
557 return target.clone();
558 }
559 index_map.get(&target.window_index()).copied().map_or_else(
560 || target.clone(),
561 |window_index| WindowTarget::with_window(session_name.clone(), window_index),
562 )
563}
564
565fn remapped_pane_target(
566 target: &PaneTarget,
567 session_name: &SessionName,
568 index_map: &BTreeMap<u32, u32>,
569) -> PaneTarget {
570 if target.session_name() != session_name {
571 return target.clone();
572 }
573 index_map.get(&target.window_index()).copied().map_or_else(
574 || target.clone(),
575 |window_index| {
576 PaneTarget::with_window(session_name.clone(), window_index, target.pane_index())
577 },
578 )
579}
580
581#[cfg(test)]
582#[path = "options/tests.rs"]
583mod tests;