1use std::collections::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 remove_pane(&mut self, target: &PaneTarget) -> Option<HashMap<OptionName, String>> {
247 self.panes.remove(target).map(OptionNode::into_known_values)
248 }
249
250 fn set_query(
251 &mut self,
252 scope: OptionScopeSelector,
253 query: &OptionQuery,
254 value: Option<&str>,
255 mode: SetOptionMode,
256 only_if_unset: bool,
257 ) -> Result<OptionMutationOutcome, RmuxError> {
258 let effective_before = self
259 .effective_value_for_scope(&scope, query)
260 .or_else(|| self.default_value_as_string(query));
261 let explicit_before = self.explicit_value_for_scope(&scope, query);
262 let default_entry = self.default_entry_for_scope(query, scope.clone());
263 let node = self.node_for_exact_scope_mut(&scope);
264 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
265 return Err(RmuxError::InvalidSetOption(format!(
266 "{} is already set",
267 query.canonical_name()
268 )));
269 }
270
271 if query.is_array()
272 && mode == SetOptionMode::Append
273 && query.index().is_none()
274 && value.unwrap_or_default().is_empty()
275 {
276 return Ok(build_mutation_outcome(query, scope));
277 }
278
279 if query.is_array() {
280 let entry = node
281 .entries
282 .entry(query.canonical_name().to_owned())
283 .or_insert_with(|| {
284 if is_global_scope(&scope) {
285 default_entry.unwrap_or_else(|| {
286 OptionEntry::new_empty_array(
287 query.canonical_name(),
288 query.known_option(),
289 scope.clone(),
290 query.value_type(),
291 )
292 })
293 } else {
294 OptionEntry::new_empty_array(
295 query.canonical_name(),
296 query.known_option(),
297 scope.clone(),
298 query.value_type(),
299 )
300 }
301 });
302 apply_array_mutation(
303 entry,
304 query,
305 value.unwrap_or_default(),
306 mode,
307 explicit_before.as_deref(),
308 )?;
309 } else {
310 let current = match (query.value_type(), mode) {
311 (OptionValueType::String, SetOptionMode::Append) => {
312 if explicit_before.is_some() || is_global_scope(&scope) {
313 explicit_before.clone().or_else(|| effective_before.clone())
314 } else {
315 None
316 }
317 }
318 (OptionValueType::String, SetOptionMode::Replace) => None,
319 _ => effective_before.clone(),
320 };
321 let normalized = normalize_scalar_value(query, value, current.as_deref())?;
322 node.entries.insert(
323 query.canonical_name().to_owned(),
324 OptionEntry::new_scalar(query, scope.clone(), normalized),
325 );
326 }
327
328 Ok(build_mutation_outcome(query, scope))
329 }
330
331 fn unset_query(
332 &mut self,
333 scope: OptionScopeSelector,
334 query: &OptionQuery,
335 only_if_unset: bool,
336 ) -> Result<OptionMutationOutcome, RmuxError> {
337 let default_entry = self.default_entry_for_scope(query, scope.clone());
338 let node = self.node_for_exact_scope_mut(&scope);
339 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
340 return Err(RmuxError::InvalidSetOption(format!(
341 "{} is already set",
342 query.canonical_name()
343 )));
344 }
345
346 if query.is_array() && query.index().is_some() {
347 let remove_node = if let Some(entry) = node.entries.get_mut(query.canonical_name()) {
348 entry.remove_array_index(query.index().unwrap(), query.separator());
349 entry.is_empty()
350 } else {
351 false
352 };
353 if remove_node {
354 node.entries.remove(query.canonical_name());
355 }
356 } else if is_global_scope(&scope) {
357 if let Some(default_entry) = default_entry {
358 node.entries
359 .insert(query.canonical_name().to_owned(), default_entry);
360 } else {
361 node.entries.remove(query.canonical_name());
362 }
363 } else {
364 node.entries.remove(query.canonical_name());
365 }
366
367 Ok(build_mutation_outcome(query, scope))
368 }
369
370 fn unset_window_pane_overrides(&mut self, scope: &OptionScopeSelector, name: &str) {
371 let OptionScopeSelector::Window(target) = scope else {
372 return;
373 };
374 self.panes.retain(|pane_target, node| {
375 let matches_window = pane_target.session_name() == target.session_name()
376 && pane_target.window_index() == target.window_index();
377 if matches_window {
378 node.entries.remove(name);
379 }
380 !node.is_empty()
381 });
382 }
383}
384
385#[cfg(test)]
386#[path = "options/tests.rs"]
387mod tests;