Skip to main content

rmux_core/
options.rs

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/// Option rendering mode for `show-options`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ShowOptionsMode {
39    /// Render the fully resolved view for each known option.
40    Resolved,
41    /// Render the resolved view and mark inherited values with `*`, matching `show-options -A`.
42    ResolvedWithInheritanceMarkers,
43    /// Render only entries explicitly present in the selected tree.
44    Explicit,
45}
46
47/// A server-visible option mutation side effect.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct OptionNotification {
50    /// The canonical option name.
51    pub name: String,
52    /// The exact scope that was mutated.
53    pub scope: OptionScopeSelector,
54    /// The effect bitmask associated with the option.
55    pub effects: OptionChangeMask,
56}
57
58/// Outcome for a successful option mutation.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct OptionMutationOutcome {
61    /// The canonical option name.
62    pub name: String,
63    /// The exact scope mutated by this outcome.
64    pub scope: OptionScopeSelector,
65    /// The known wire option, when the option is part of the closed V1 registry.
66    pub known_option: Option<OptionName>,
67    /// Exact explicit value before the mutation at the mutated scope.
68    pub old_explicit: Option<String>,
69    /// Exact explicit value after the mutation at the mutated scope.
70    pub new_explicit: Option<String>,
71    /// Whether the exact explicit value changed.
72    ///
73    /// Idempotent sets and no-op unsets return `false`; event producers should
74    /// use this bit to avoid echoing mutations that did not change pane state.
75    pub changed: bool,
76    /// Side effects the server may react to.
77    pub notifications: Vec<OptionNotification>,
78    /// Additional exact-scope mutations caused by this operation.
79    pub related: Vec<OptionMutationOutcome>,
80}
81
82type SessionOptions = HashMap<SessionName, OptionNode>;
83type WindowOptions = HashMap<WindowTarget, OptionNode>;
84type PaneOptions = HashMap<PaneTarget, OptionNode>;
85
86/// In-memory storage for supported RMUX option values.
87#[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    /// Creates an empty option store with no explicit overrides.
99    #[must_use]
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Returns whether no explicit option overrides are present.
105    #[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    /// Applies a mutation for a known legacy option.
116    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    /// Applies a mutation using a tmux-style string option name.
136    #[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        // tmux 3.7b -U (oracle-probed 2026-07-09): acts like -u at the
148        // resolved scope, and additionally clears the window's pane
149        // overrides only when that scope is a window. Plain `set -U` unsets
150        // the session copy alone; `set -pU` unsets the pane copy alone.
151        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    /// Removes all option overrides owned by the given session.
171    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    /// Rekeys all option overrides owned by the given session.
185    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    /// Removes all window and pane option overrides owned by the given window.
252    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    /// Copies exact window and pane overrides from one winlink slot to another.
263    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    /// Swaps exact window and pane overrides between two winlink slots.
303    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    /// Moves exact window and pane overrides from one winlink slot to another.
332    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    /// Removes all pane option overrides owned by the given pane.
353    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    /// Copies exact pane-local overrides between two aliases of one pane.
358    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    /// Rekeys pane-local overrides after pane indices change within one window.
373    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    /// Moves exact pane-local overrides from one pane slot to another.
407    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    /// Swaps exact pane-local overrides between two pane slots.
413    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    /// Atomically rekeys or removes exact pane-local overrides.
422    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    /// Rekeys window and pane option overrides after a session window reindex.
478    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;