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