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};
33use storage::{OptionEntry, OptionNode};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ShowOptionsMode {
38 Resolved,
40 Explicit,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct OptionNotification {
47 pub name: String,
49 pub scope: OptionScopeSelector,
51 pub effects: OptionChangeMask,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct OptionMutationOutcome {
58 pub name: String,
60 pub known_option: Option<OptionName>,
62 pub notifications: Vec<OptionNotification>,
64}
65
66type SessionOptions = HashMap<SessionName, OptionNode>;
67type WindowOptions = HashMap<WindowTarget, OptionNode>;
68type PaneOptions = HashMap<PaneTarget, OptionNode>;
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct OptionStore {
73 server_global: OptionNode,
74 session_global: OptionNode,
75 window_global: OptionNode,
76 sessions: SessionOptions,
77 windows: WindowOptions,
78 panes: PaneOptions,
79}
80
81impl OptionStore {
82 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 #[must_use]
90 pub fn is_empty(&self) -> bool {
91 self.server_global.is_empty()
92 && self.session_global.is_empty()
93 && self.window_global.is_empty()
94 && self.sessions.values().all(OptionNode::is_empty)
95 && self.windows.values().all(OptionNode::is_empty)
96 && self.panes.values().all(OptionNode::is_empty)
97 }
98
99 pub fn set(
101 &mut self,
102 scope: ScopeSelector,
103 option: OptionName,
104 value: String,
105 mode: SetOptionMode,
106 ) -> Result<OptionMutationOutcome, RmuxError> {
107 let explicit_scope = legacy_scope_for_option(option, &scope);
108 self.set_by_name(
109 explicit_scope,
110 option_metadata(option).name(),
111 Some(value),
112 mode,
113 false,
114 false,
115 false,
116 )
117 }
118
119 #[allow(clippy::too_many_arguments)]
121 pub fn set_by_name(
122 &mut self,
123 scope: OptionScopeSelector,
124 name: &str,
125 value: Option<String>,
126 mode: SetOptionMode,
127 only_if_unset: bool,
128 unset: bool,
129 unset_pane_overrides: bool,
130 ) -> Result<OptionMutationOutcome, RmuxError> {
131 if unset_pane_overrides && !matches!(scope, OptionScopeSelector::Window(_)) {
132 return Err(RmuxError::InvalidSetOption(
133 "unset pane overrides only supports window scope".to_owned(),
134 ));
135 }
136
137 let query = validate_option_name_mutation(name, &scope, mode, value.as_deref(), unset)?;
138
139 if unset_pane_overrides {
140 self.unset_window_pane_overrides(&scope, query.canonical_name());
141 }
142
143 if unset {
144 self.unset_query(scope, &query, only_if_unset)
145 } else {
146 self.set_query(scope, &query, value.as_deref(), mode, only_if_unset)
147 }
148 }
149
150 pub fn remove_session(
152 &mut self,
153 session_name: &SessionName,
154 ) -> Option<HashMap<OptionName, String>> {
155 self.windows
156 .retain(|target, _| target.session_name() != session_name);
157 self.panes
158 .retain(|target, _| target.session_name() != session_name);
159 self.sessions
160 .remove(session_name)
161 .map(|node| node.into_known_values())
162 }
163
164 pub fn rename_session(
166 &mut self,
167 session_name: &SessionName,
168 new_name: SessionName,
169 ) -> Result<(), RmuxError> {
170 let mut renamed_sessions = HashMap::with_capacity(self.sessions.len());
171 for (name, values) in &self.sessions {
172 let next_name = if name == session_name {
173 new_name.clone()
174 } else {
175 name.clone()
176 };
177 if renamed_sessions
178 .insert(next_name.clone(), values.clone())
179 .is_some()
180 {
181 return Err(RmuxError::Server(format!(
182 "session options already exist for session {next_name}"
183 )));
184 }
185 }
186
187 let mut renamed_windows = HashMap::with_capacity(self.windows.len());
188 for (target, values) in &self.windows {
189 let next_target = if target.session_name() == session_name {
190 WindowTarget::with_window(new_name.clone(), target.window_index())
191 } else {
192 target.clone()
193 };
194 if renamed_windows
195 .insert(next_target.clone(), values.clone())
196 .is_some()
197 {
198 return Err(RmuxError::Server(format!(
199 "window options already exist for {next_target}"
200 )));
201 }
202 }
203
204 let mut renamed_panes = HashMap::with_capacity(self.panes.len());
205 for (target, values) in &self.panes {
206 let next_target = if target.session_name() == session_name {
207 PaneTarget::with_window(
208 new_name.clone(),
209 target.window_index(),
210 target.pane_index(),
211 )
212 } else {
213 target.clone()
214 };
215 if renamed_panes
216 .insert(next_target.clone(), values.clone())
217 .is_some()
218 {
219 return Err(RmuxError::Server(format!(
220 "pane options already exist for {next_target}"
221 )));
222 }
223 }
224
225 self.sessions = renamed_sessions;
226 self.windows = renamed_windows;
227 self.panes = renamed_panes;
228 Ok(())
229 }
230
231 pub fn remove_window(&mut self, target: &WindowTarget) -> Option<HashMap<OptionName, String>> {
233 self.panes.retain(|pane_target, _| {
234 pane_target.session_name() != target.session_name()
235 || pane_target.window_index() != target.window_index()
236 });
237 self.windows
238 .remove(target)
239 .map(OptionNode::into_known_values)
240 }
241
242 pub fn remove_pane(&mut self, target: &PaneTarget) -> Option<HashMap<OptionName, String>> {
244 self.panes.remove(target).map(OptionNode::into_known_values)
245 }
246
247 fn set_query(
248 &mut self,
249 scope: OptionScopeSelector,
250 query: &OptionQuery,
251 value: Option<&str>,
252 mode: SetOptionMode,
253 only_if_unset: bool,
254 ) -> Result<OptionMutationOutcome, RmuxError> {
255 let effective_before = self
256 .effective_value_for_scope(&scope, query)
257 .or_else(|| self.default_value_as_string(query));
258 let explicit_before = self.explicit_value_for_scope(&scope, query);
259 let default_entry = self.default_entry_for_scope(query, scope.clone());
260 let node = self.node_for_exact_scope_mut(&scope);
261 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
262 return Err(RmuxError::InvalidSetOption(format!(
263 "{} is already set",
264 query.canonical_name()
265 )));
266 }
267
268 if query.is_array()
269 && mode == SetOptionMode::Append
270 && query.index().is_none()
271 && value.unwrap_or_default().is_empty()
272 {
273 return Ok(build_mutation_outcome(query, scope));
274 }
275
276 if query.is_array() {
277 let entry = node
278 .entries
279 .entry(query.canonical_name().to_owned())
280 .or_insert_with(|| {
281 if is_global_scope(&scope) {
282 default_entry.unwrap_or_else(|| {
283 OptionEntry::new_empty_array(
284 query.canonical_name(),
285 query.known_option(),
286 scope.clone(),
287 query.value_type(),
288 )
289 })
290 } else {
291 OptionEntry::new_empty_array(
292 query.canonical_name(),
293 query.known_option(),
294 scope.clone(),
295 query.value_type(),
296 )
297 }
298 });
299 apply_array_mutation(
300 entry,
301 query,
302 value.unwrap_or_default(),
303 mode,
304 explicit_before.as_deref(),
305 )?;
306 } else {
307 let current = match (query.value_type(), mode) {
308 (OptionValueType::String, SetOptionMode::Append) => {
309 if explicit_before.is_some() || is_global_scope(&scope) {
310 explicit_before.clone().or_else(|| effective_before.clone())
311 } else {
312 None
313 }
314 }
315 (OptionValueType::String, SetOptionMode::Replace) => None,
316 _ => effective_before.clone(),
317 };
318 let normalized = normalize_scalar_value(query, value, current.as_deref())?;
319 node.entries.insert(
320 query.canonical_name().to_owned(),
321 OptionEntry::new_scalar(query, scope.clone(), normalized),
322 );
323 }
324
325 Ok(build_mutation_outcome(query, scope))
326 }
327
328 fn unset_query(
329 &mut self,
330 scope: OptionScopeSelector,
331 query: &OptionQuery,
332 only_if_unset: bool,
333 ) -> Result<OptionMutationOutcome, RmuxError> {
334 let default_entry = self.default_entry_for_scope(query, scope.clone());
335 let node = self.node_for_exact_scope_mut(&scope);
336 if only_if_unset && node.contains(query.canonical_name(), query.index()) {
337 return Err(RmuxError::InvalidSetOption(format!(
338 "{} is already set",
339 query.canonical_name()
340 )));
341 }
342
343 if query.is_array() && query.index().is_some() {
344 let remove_node = if let Some(entry) = node.entries.get_mut(query.canonical_name()) {
345 entry.remove_array_index(query.index().unwrap(), query.separator());
346 entry.is_empty()
347 } else {
348 false
349 };
350 if remove_node {
351 node.entries.remove(query.canonical_name());
352 }
353 } else if is_global_scope(&scope) {
354 if let Some(default_entry) = default_entry {
355 node.entries
356 .insert(query.canonical_name().to_owned(), default_entry);
357 } else {
358 node.entries.remove(query.canonical_name());
359 }
360 } else {
361 node.entries.remove(query.canonical_name());
362 }
363
364 Ok(build_mutation_outcome(query, scope))
365 }
366
367 fn unset_window_pane_overrides(&mut self, scope: &OptionScopeSelector, name: &str) {
368 let OptionScopeSelector::Window(target) = scope else {
369 return;
370 };
371 self.panes.retain(|pane_target, node| {
372 let matches_window = pane_target.session_name() == target.session_name()
373 && pane_target.window_index() == target.window_index();
374 if matches_window {
375 node.entries.remove(name);
376 }
377 !node.is_empty()
378 });
379 }
380}
381
382#[cfg(test)]
383#[path = "options/tests.rs"]
384mod tests;