use std::collections::{BTreeMap, HashMap, HashSet};
use rmux_proto::types::OptionScopeSelector;
use rmux_proto::{
OptionName, PaneTarget, RmuxError, ScopeSelector, SessionName, SetOptionMode, WindowTarget,
};
#[path = "options/access.rs"]
mod access;
#[path = "options/mutation.rs"]
mod mutation;
#[path = "options/registry.rs"]
mod registry;
#[path = "options/render.rs"]
mod render;
#[path = "options/scope.rs"]
mod scope;
#[path = "options/show.rs"]
mod show;
#[path = "options/storage.rs"]
mod storage;
use mutation::{
apply_array_mutation, build_mutation_outcome, is_global_scope, legacy_scope_for_option,
normalize_scalar_value,
};
pub use mutation::{validate_option_mutation, validate_option_name_mutation};
pub use registry::{
option_affects_alerts, option_affects_rendering, option_name_by_name, resolve_option_name,
resolve_option_name_typed, OptionLookupError, OptionQuery,
};
use registry::{option_metadata, OptionChangeMask, OptionValueType};
pub use scope::default_global_scope_for_option_name;
use storage::{OptionEntry, OptionNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShowOptionsMode {
Resolved,
ResolvedWithInheritanceMarkers,
Explicit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptionNotification {
pub name: String,
pub scope: OptionScopeSelector,
pub effects: OptionChangeMask,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptionMutationOutcome {
pub name: String,
pub scope: OptionScopeSelector,
pub known_option: Option<OptionName>,
pub old_explicit: Option<String>,
pub new_explicit: Option<String>,
pub changed: bool,
pub notifications: Vec<OptionNotification>,
pub related: Vec<OptionMutationOutcome>,
}
type SessionOptions = HashMap<SessionName, OptionNode>;
type WindowOptions = HashMap<WindowTarget, OptionNode>;
type PaneOptions = HashMap<PaneTarget, OptionNode>;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct OptionStore {
server_global: OptionNode,
session_global: OptionNode,
window_global: OptionNode,
sessions: SessionOptions,
windows: WindowOptions,
panes: PaneOptions,
}
impl OptionStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.server_global.is_empty()
&& self.session_global.is_empty()
&& self.window_global.is_empty()
&& self.sessions.values().all(OptionNode::is_empty)
&& self.windows.values().all(OptionNode::is_empty)
&& self.panes.values().all(OptionNode::is_empty)
}
pub fn set(
&mut self,
scope: ScopeSelector,
option: OptionName,
value: String,
mode: SetOptionMode,
) -> Result<OptionMutationOutcome, RmuxError> {
let explicit_scope = legacy_scope_for_option(option, &scope);
self.set_by_name(
explicit_scope,
option_metadata(option).name(),
Some(value),
mode,
false,
false,
false,
)
}
#[allow(clippy::too_many_arguments)]
pub fn set_by_name(
&mut self,
scope: OptionScopeSelector,
name: &str,
value: Option<String>,
mode: SetOptionMode,
only_if_unset: bool,
unset: bool,
unset_pane_overrides: bool,
) -> Result<OptionMutationOutcome, RmuxError> {
let query = validate_option_name_mutation(name, &scope, mode, value.as_deref(), unset)?;
let unset_pane_scope = (unset_pane_overrides
&& matches!(scope, OptionScopeSelector::Window(_)))
.then(|| scope.clone());
let mut outcome = if unset {
self.unset_query(scope, &query, only_if_unset)
} else {
self.set_query(scope, &query, value.as_deref(), mode, only_if_unset)
}?;
let related = if let Some(scope) = unset_pane_scope {
self.unset_window_pane_overrides(&scope, &query)
} else {
Vec::new()
};
outcome.related.extend(related);
Ok(outcome)
}
pub fn remove_session(
&mut self,
session_name: &SessionName,
) -> Option<HashMap<OptionName, String>> {
self.windows
.retain(|target, _| target.session_name() != session_name);
self.panes
.retain(|target, _| target.session_name() != session_name);
self.sessions
.remove(session_name)
.map(|node| node.into_known_values())
}
pub fn rename_session(
&mut self,
session_name: &SessionName,
new_name: SessionName,
) -> Result<(), RmuxError> {
let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
for (name, values) in &self.sessions {
let next_name = if name == session_name {
new_name.clone()
} else {
name.clone()
};
if renamed_sessions
.insert(next_name.clone(), values.clone())
.is_some()
{
return Err(RmuxError::Server(format!(
"session options already exist for session {next_name}"
)));
}
}
let mut renamed_windows = HashMap::with_capacity(self.windows.len());
for (target, values) in &self.windows {
let next_target = if target.session_name() == session_name {
WindowTarget::with_window(new_name.clone(), target.window_index())
} else {
target.clone()
};
if renamed_windows
.insert(next_target.clone(), values.clone())
.is_some()
{
return Err(RmuxError::Server(format!(
"window options already exist for {next_target}"
)));
}
}
let mut renamed_panes = HashMap::with_capacity(self.panes.len());
for (target, values) in &self.panes {
let next_target = if target.session_name() == session_name {
PaneTarget::with_window(
new_name.clone(),
target.window_index(),
target.pane_index(),
)
} else {
target.clone()
};
if renamed_panes
.insert(next_target.clone(), values.clone())
.is_some()
{
return Err(RmuxError::Server(format!(
"pane options already exist for {next_target}"
)));
}
}
self.sessions = renamed_sessions;
self.windows = renamed_windows;
self.panes = renamed_panes;
Ok(())
}
pub fn remove_window(&mut self, target: &WindowTarget) -> Option<HashMap<OptionName, String>> {
self.panes.retain(|pane_target, _| {
pane_target.session_name() != target.session_name()
|| pane_target.window_index() != target.window_index()
});
self.windows
.remove(target)
.map(OptionNode::into_known_values)
}
pub fn copy_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
if let Some(source_window) = self.windows.get(source).cloned() {
self.windows.insert(
target.clone(),
source_window.with_scope(OptionScopeSelector::Window(target.clone())),
);
} else {
self.windows.remove(target);
}
self.panes.retain(|pane_target, _| {
pane_target.session_name() != target.session_name()
|| pane_target.window_index() != target.window_index()
});
let source_panes = self
.panes
.iter()
.filter(|(pane_target, _)| {
pane_target.session_name() == source.session_name()
&& pane_target.window_index() == source.window_index()
})
.map(|(pane_target, node)| {
let target_pane = PaneTarget::with_window(
target.session_name().clone(),
target.window_index(),
pane_target.pane_index(),
);
(
target_pane.clone(),
node.clone()
.with_scope(OptionScopeSelector::Pane(target_pane)),
)
})
.collect::<Vec<_>>();
self.panes.extend(source_panes);
}
pub fn swap_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
if source == target {
return;
}
let source_window = self.windows.remove(source);
let target_window = self.windows.remove(target);
if let Some(node) = source_window {
self.windows.insert(
target.clone(),
node.with_scope(OptionScopeSelector::Window(target.clone())),
);
}
if let Some(node) = target_window {
self.windows.insert(
source.clone(),
node.with_scope(OptionScopeSelector::Window(source.clone())),
);
}
let source_panes = remove_window_pane_options(&mut self.panes, source);
let target_panes = remove_window_pane_options(&mut self.panes, target);
self.panes
.extend(rekey_pane_options(source_panes, source, target));
self.panes
.extend(rekey_pane_options(target_panes, target, source));
}
pub fn move_window_overrides(&mut self, source: &WindowTarget, target: &WindowTarget) {
if source == target {
return;
}
let source_window = self.windows.remove(source);
let _ = self.windows.remove(target);
if let Some(node) = source_window {
self.windows.insert(
target.clone(),
node.with_scope(OptionScopeSelector::Window(target.clone())),
);
}
let source_panes = remove_window_pane_options(&mut self.panes, source);
let _ = remove_window_pane_options(&mut self.panes, target);
self.panes
.extend(rekey_pane_options(source_panes, source, target));
}
pub fn remove_pane(&mut self, target: &PaneTarget) -> Option<HashMap<OptionName, String>> {
self.panes.remove(target).map(OptionNode::into_known_values)
}
pub fn copy_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
if source == target {
return;
}
if let Some(source_pane) = self.panes.get(source).cloned() {
self.panes.insert(
target.clone(),
source_pane.with_scope(OptionScopeSelector::Pane(target.clone())),
);
} else {
self.panes.remove(target);
}
}
pub fn remap_pane_indices(
&mut self,
session_name: &SessionName,
window_index: u32,
index_map: &BTreeMap<u32, u32>,
) -> Result<(), RmuxError> {
if index_map.is_empty() {
return Ok(());
}
let mut remapped_panes = HashMap::with_capacity(self.panes.len());
for (target, values) in &self.panes {
let next_target =
remapped_pane_index_target(target, session_name, window_index, index_map);
if remapped_panes
.insert(
next_target.clone(),
values
.clone()
.with_scope(OptionScopeSelector::Pane(next_target.clone())),
)
.is_some()
{
return Err(RmuxError::Server(format!(
"pane options already exist for {next_target}"
)));
}
}
self.panes = remapped_panes;
Ok(())
}
pub fn transfer_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
self.rekey_pane_overrides(&[(source.clone(), Some(target.clone()))])
.expect("single pane override transfer cannot collide");
}
pub fn swap_pane_overrides(&mut self, source: &PaneTarget, target: &PaneTarget) {
self.rekey_pane_overrides(&[
(source.clone(), Some(target.clone())),
(target.clone(), Some(source.clone())),
])
.expect("two-way pane override swap cannot collide");
}
pub fn rekey_pane_overrides(
&mut self,
mappings: &[(PaneTarget, Option<PaneTarget>)],
) -> Result<(), RmuxError> {
if mappings.is_empty() {
return Ok(());
}
let mut targets = HashSet::new();
for (_, target) in mappings {
if let Some(target) = target {
if !targets.insert(target.clone()) {
return Err(RmuxError::Server(format!(
"pane options remap has duplicate destination {target}"
)));
}
}
}
let mut removed = Vec::with_capacity(mappings.len());
for (source, _) in mappings {
if let Some(node) = self.panes.remove(source) {
removed.push((source.clone(), node));
}
}
for (_, target) in mappings {
if let Some(target) = target {
let _ = self.panes.remove(target);
}
}
for (source, node) in removed {
let Some((_, Some(target))) = mappings
.iter()
.find(|(mapped_source, _)| *mapped_source == source)
else {
continue;
};
if self
.panes
.insert(
target.clone(),
node.with_scope(OptionScopeSelector::Pane(target.clone())),
)
.is_some()
{
return Err(RmuxError::Server(format!(
"pane options already exist for {target}"
)));
}
}
Ok(())
}
pub fn remap_session_window_indices(
&mut self,
session_name: &SessionName,
index_map: &BTreeMap<u32, u32>,
) -> Result<(), RmuxError> {
let mut remapped_windows = HashMap::with_capacity(self.windows.len());
for (target, values) in &self.windows {
let next_target = remapped_window_target(target, session_name, index_map);
if remapped_windows
.insert(next_target.clone(), values.clone())
.is_some()
{
return Err(RmuxError::Server(format!(
"window options already exist for {next_target}"
)));
}
}
let mut remapped_panes = HashMap::with_capacity(self.panes.len());
for (target, values) in &self.panes {
let next_target = remapped_pane_target(target, session_name, index_map);
if remapped_panes
.insert(next_target.clone(), values.clone())
.is_some()
{
return Err(RmuxError::Server(format!(
"pane options already exist for {next_target}"
)));
}
}
self.windows = remapped_windows;
self.panes = remapped_panes;
Ok(())
}
fn set_query(
&mut self,
scope: OptionScopeSelector,
query: &OptionQuery,
value: Option<&str>,
mode: SetOptionMode,
only_if_unset: bool,
) -> Result<OptionMutationOutcome, RmuxError> {
let effective_before = self
.effective_value_for_scope(&scope, query)
.or_else(|| self.default_value_as_string(query));
let explicit_before = self.explicit_value_for_scope(&scope, query);
let default_entry = self.default_entry_for_scope(query, scope.clone());
let node = self.node_for_exact_scope_mut(&scope);
if only_if_unset && explicit_before.is_some() {
return Err(RmuxError::InvalidSetOption(format!(
"{} is already set",
query.canonical_name()
)));
}
if query.is_array()
&& mode == SetOptionMode::Append
&& query.index().is_none()
&& value.unwrap_or_default().is_empty()
{
return Ok(build_mutation_outcome(
query,
scope,
explicit_before.clone(),
explicit_before,
));
}
if query.is_array() {
let entry = node
.entries
.entry(query.canonical_name().to_owned())
.or_insert_with(|| {
if is_global_scope(&scope) {
default_entry.unwrap_or_else(|| {
OptionEntry::new_empty_array(
query.canonical_name(),
query.known_option(),
scope.clone(),
query.value_type(),
)
})
} else {
OptionEntry::new_empty_array(
query.canonical_name(),
query.known_option(),
scope.clone(),
query.value_type(),
)
}
});
apply_array_mutation(
entry,
query,
value.unwrap_or_default(),
mode,
explicit_before.as_deref(),
)?;
} else {
let current = match (query.value_type(), mode) {
(OptionValueType::String, SetOptionMode::Append) => {
if explicit_before.is_some() || is_global_scope(&scope) {
explicit_before.clone().or_else(|| effective_before.clone())
} else {
None
}
}
(OptionValueType::String, SetOptionMode::Replace) => None,
_ => effective_before.clone(),
};
let normalized = normalize_scalar_value(query, value, current.as_deref())?;
node.entries.insert(
query.canonical_name().to_owned(),
OptionEntry::new_scalar(query, scope.clone(), normalized),
);
}
let explicit_after = self.explicit_value_for_scope(&scope, query);
Ok(build_mutation_outcome(
query,
scope,
explicit_before,
explicit_after,
))
}
fn unset_query(
&mut self,
scope: OptionScopeSelector,
query: &OptionQuery,
only_if_unset: bool,
) -> Result<OptionMutationOutcome, RmuxError> {
let explicit_before = self.explicit_value_for_scope(&scope, query);
let default_entry = self.default_entry_for_scope(query, scope.clone());
let node = self.node_for_exact_scope_mut(&scope);
if only_if_unset && node.contains(query.canonical_name(), query.index()) {
return Err(RmuxError::InvalidSetOption(format!(
"{} is already set",
query.canonical_name()
)));
}
if query.is_array() && query.index().is_some() {
let remove_node = if let Some(entry) = node.entries.get_mut(query.canonical_name()) {
entry.remove_array_index(query.index().unwrap(), query.separator());
entry.is_empty()
} else {
false
};
if remove_node {
node.entries.remove(query.canonical_name());
}
} else if is_global_scope(&scope) {
if let Some(default_entry) = default_entry {
node.entries
.insert(query.canonical_name().to_owned(), default_entry);
} else {
node.entries.remove(query.canonical_name());
}
} else {
node.entries.remove(query.canonical_name());
}
let explicit_after = self.explicit_value_for_scope(&scope, query);
Ok(build_mutation_outcome(
query,
scope,
explicit_before,
explicit_after,
))
}
fn unset_window_pane_overrides(
&mut self,
scope: &OptionScopeSelector,
query: &OptionQuery,
) -> Vec<OptionMutationOutcome> {
let OptionScopeSelector::Window(target) = scope else {
return Vec::new();
};
let name = query.canonical_name();
let mut outcomes = Vec::new();
self.panes.retain(|pane_target, node| {
let matches_window = pane_target.session_name() == target.session_name()
&& pane_target.window_index() == target.window_index();
if matches_window {
if let Some(old_explicit) = node
.entries
.get(name)
.and_then(|entry| entry.value(query.index()))
.map(str::to_owned)
{
outcomes.push(build_mutation_outcome(
query,
OptionScopeSelector::Pane(pane_target.clone()),
Some(old_explicit),
None,
));
}
node.entries.remove(name);
}
!node.is_empty()
});
outcomes
}
}
fn remove_window_pane_options(
panes: &mut PaneOptions,
window: &WindowTarget,
) -> Vec<(PaneTarget, OptionNode)> {
let pane_targets = panes
.keys()
.filter(|pane_target| {
pane_target.session_name() == window.session_name()
&& pane_target.window_index() == window.window_index()
})
.cloned()
.collect::<Vec<_>>();
pane_targets
.into_iter()
.filter_map(|pane_target| panes.remove(&pane_target).map(|node| (pane_target, node)))
.collect()
}
fn rekey_pane_options(
panes: Vec<(PaneTarget, OptionNode)>,
source: &WindowTarget,
target: &WindowTarget,
) -> Vec<(PaneTarget, OptionNode)> {
panes
.into_iter()
.map(move |(pane_target, node)| {
let next_target = PaneTarget::with_window(
target.session_name().clone(),
target.window_index(),
pane_target.pane_index(),
);
let next_node = node.with_scope(OptionScopeSelector::Pane(next_target.clone()));
debug_assert_eq!(pane_target.session_name(), source.session_name());
debug_assert_eq!(pane_target.window_index(), source.window_index());
(next_target, next_node)
})
.collect()
}
fn remapped_window_target(
target: &WindowTarget,
session_name: &SessionName,
index_map: &BTreeMap<u32, u32>,
) -> WindowTarget {
if target.session_name() != session_name {
return target.clone();
}
index_map.get(&target.window_index()).copied().map_or_else(
|| target.clone(),
|window_index| WindowTarget::with_window(session_name.clone(), window_index),
)
}
fn remapped_pane_target(
target: &PaneTarget,
session_name: &SessionName,
index_map: &BTreeMap<u32, u32>,
) -> PaneTarget {
if target.session_name() != session_name {
return target.clone();
}
index_map.get(&target.window_index()).copied().map_or_else(
|| target.clone(),
|window_index| {
PaneTarget::with_window(session_name.clone(), window_index, target.pane_index())
},
)
}
fn remapped_pane_index_target(
target: &PaneTarget,
session_name: &SessionName,
window_index: u32,
index_map: &BTreeMap<u32, u32>,
) -> PaneTarget {
if target.session_name() != session_name || target.window_index() != window_index {
return target.clone();
}
index_map.get(&target.pane_index()).copied().map_or_else(
|| target.clone(),
|pane_index| PaneTarget::with_window(session_name.clone(), window_index, pane_index),
)
}
#[cfg(test)]
#[path = "options/tests.rs"]
mod tests;