Skip to main content

rmux_core/options/
access.rs

1use std::collections::HashMap;
2
3use rmux_proto::types::OptionScopeSelector;
4use rmux_proto::{OptionName, PaneTarget, SessionName, WindowTarget};
5
6use super::mutation::{
7    default_array_items, default_scalar_text, normalize_scalar_value, split_array_assignment,
8};
9use super::registry::{
10    option_metadata, registry, resolve_exact_option_name, resolve_option_name, DefaultValue,
11    GlobalRoot,
12};
13use super::render::format_rendered_option_value;
14use super::scope::{
15    push_known_global_roots, scope_allows_pane, scope_allows_session, scope_allows_window,
16    ResolveContext,
17};
18use super::storage::{OptionEntry, OptionNode};
19use super::{OptionQuery, OptionStore};
20
21impl OptionStore {
22    /// Returns the exact explicit global value for the given known option.
23    #[must_use]
24    pub fn global_value(&self, option: OptionName) -> Option<&str> {
25        let query = OptionQuery::known(option);
26        self.node_for_global_root(option_metadata(option).global_root())
27            .and_then(|node| node.value(query.canonical_name(), None))
28    }
29
30    /// Returns the exact session-local value for the given known option.
31    #[must_use]
32    pub fn session_value(&self, session_name: &SessionName, option: OptionName) -> Option<&str> {
33        let query = OptionQuery::known(option);
34        self.sessions
35            .get(session_name)
36            .and_then(|node| node.value(query.canonical_name(), None))
37    }
38
39    /// Returns the exact window-local value for the given known option.
40    #[must_use]
41    pub fn window_value(&self, target: &WindowTarget, option: OptionName) -> Option<&str> {
42        let query = OptionQuery::known(option);
43        self.windows
44            .get(target)
45            .and_then(|node| node.value(query.canonical_name(), None))
46    }
47
48    /// Returns the exact pane-local value for the given known option.
49    #[must_use]
50    pub fn pane_value(&self, target: &PaneTarget, option: OptionName) -> Option<&str> {
51        let query = OptionQuery::known(option);
52        self.panes
53            .get(target)
54            .and_then(|node| node.value(query.canonical_name(), None))
55    }
56
57    /// Resolves the effective value using the tmux-style parent chain for the known option.
58    #[must_use]
59    pub fn resolve<'a>(
60        &'a self,
61        session_name: Option<&'a SessionName>,
62        option: OptionName,
63    ) -> Option<&'a str> {
64        let query = OptionQuery::known(option);
65        let resolved = match session_name {
66            Some(session_name) => self
67                .resolve_query(&ResolveContext::Session(session_name), &query)
68                .map(OptionEntry::rendered),
69            None => self
70                .resolve_query(&ResolveContext::Server, &query)
71                .map(OptionEntry::rendered),
72        };
73        if let Some(resolved) = resolved {
74            Some(resolved)
75        } else {
76            self.default_rendered_value(&query)
77        }
78    }
79
80    /// Resolves the effective window value using the tmux-style parent chain.
81    #[must_use]
82    pub fn resolve_for_window<'a>(
83        &'a self,
84        session_name: &'a SessionName,
85        window_index: u32,
86        option: OptionName,
87    ) -> Option<&'a str> {
88        let query = OptionQuery::known(option);
89        let resolved = self
90            .resolve_query(&ResolveContext::Window(session_name, window_index), &query)
91            .map(OptionEntry::rendered);
92        if let Some(resolved) = resolved {
93            Some(resolved)
94        } else {
95            self.default_rendered_value(&query)
96        }
97    }
98
99    /// Resolves the effective pane value using the tmux-style parent chain.
100    #[must_use]
101    pub fn resolve_for_pane<'a>(
102        &'a self,
103        session_name: &'a SessionName,
104        window_index: u32,
105        pane_index: u32,
106        option: OptionName,
107    ) -> Option<&'a str> {
108        let query = OptionQuery::known(option);
109        let resolved = self
110            .resolve_query(
111                &ResolveContext::Pane(session_name, window_index, pane_index),
112                &query,
113            )
114            .map(OptionEntry::rendered);
115        if let Some(resolved) = resolved {
116            Some(resolved)
117        } else {
118            self.default_rendered_value(&query)
119        }
120    }
121
122    /// Resolves the effective array values for the known option.
123    #[must_use]
124    pub fn resolve_array_values(
125        &self,
126        session_name: Option<&SessionName>,
127        option: OptionName,
128    ) -> Vec<String> {
129        let query = OptionQuery::known(option);
130        if !query.is_array() {
131            return Vec::new();
132        }
133
134        let resolved = match session_name {
135            Some(session_name) => self
136                .resolve_query(&ResolveContext::Session(session_name), &query)
137                .map(OptionEntry::array_values),
138            None => self
139                .resolve_query(&ResolveContext::Server, &query)
140                .map(OptionEntry::array_values),
141        };
142        if let Some(resolved) = resolved {
143            return resolved;
144        }
145
146        match query.default_value() {
147            Some(DefaultValue::Scalar(value)) => split_array_assignment(value, query.separator()),
148            Some(DefaultValue::Array(values)) => {
149                values.iter().map(|value| (*value).to_owned()).collect()
150            }
151            None => Vec::new(),
152        }
153    }
154
155    /// Resolves a string-keyed option in a session context.
156    #[must_use]
157    pub fn resolve_name(&self, session_name: Option<&SessionName>, name: &str) -> Option<String> {
158        let query = resolve_option_name(name).ok()?;
159        match session_name {
160            // tmux format.c::format_find checks server options first, then the
161            // context-specific tree, then any wider parents for that context.
162            Some(session_name) => self
163                .resolve_name_from_nodes(
164                    &query,
165                    [
166                        Some(&self.server_global),
167                        self.sessions.get(session_name),
168                        Some(&self.session_global),
169                    ],
170                )
171                .or_else(|| self.default_value_as_string(&query)),
172            None => self
173                .resolve_name_from_nodes(&query, [Some(&self.server_global)])
174                .or_else(|| self.default_value_as_string(&query)),
175        }
176    }
177
178    /// Resolves a string-keyed option for tmux format evaluation in a session context.
179    #[must_use]
180    pub fn resolve_name_for_format(
181        &self,
182        session_name: Option<&SessionName>,
183        name: &str,
184    ) -> Option<String> {
185        let query = resolve_exact_option_name(name).ok()?;
186        let value = match session_name {
187            Some(session_name) => self
188                .resolve_name_from_nodes(
189                    &query,
190                    [
191                        Some(&self.server_global),
192                        self.sessions.get(session_name),
193                        Some(&self.session_global),
194                    ],
195                )
196                .or_else(|| self.default_value_as_string(&query)),
197            None => self
198                .resolve_name_from_nodes(
199                    &query,
200                    [
201                        Some(&self.server_global),
202                        Some(&self.session_global),
203                        Some(&self.window_global),
204                    ],
205                )
206                .or_else(|| self.default_value_as_string(&query)),
207        }?;
208        Some(format_rendered_option_value(&query, value))
209    }
210
211    /// Resolves a string-keyed option in a window context.
212    #[must_use]
213    pub fn resolve_name_for_window(
214        &self,
215        session_name: &SessionName,
216        window_index: u32,
217        name: &str,
218    ) -> Option<String> {
219        let query = resolve_option_name(name).ok()?;
220        let target = WindowTarget::with_window(session_name.clone(), window_index);
221        self.resolve_name_from_nodes(
222            &query,
223            [
224                Some(&self.server_global),
225                self.windows.get(&target),
226                Some(&self.window_global),
227                self.sessions.get(session_name),
228                Some(&self.session_global),
229            ],
230        )
231        .or_else(|| self.default_value_as_string(&query))
232    }
233
234    /// Resolves a string-keyed option for tmux format evaluation in a window context.
235    #[must_use]
236    pub fn resolve_name_for_window_format(
237        &self,
238        session_name: &SessionName,
239        window_index: u32,
240        name: &str,
241    ) -> Option<String> {
242        let query = resolve_exact_option_name(name).ok()?;
243        let target = WindowTarget::with_window(session_name.clone(), window_index);
244        let value = self
245            .resolve_name_from_nodes(
246                &query,
247                [
248                    Some(&self.server_global),
249                    self.windows.get(&target),
250                    Some(&self.window_global),
251                    self.sessions.get(session_name),
252                    Some(&self.session_global),
253                ],
254            )
255            .or_else(|| self.default_value_as_string(&query))?;
256        Some(format_rendered_option_value(&query, value))
257    }
258
259    /// Resolves a string-keyed option in a pane context.
260    #[must_use]
261    pub fn resolve_name_for_pane(
262        &self,
263        session_name: &SessionName,
264        window_index: u32,
265        pane_index: u32,
266        name: &str,
267    ) -> Option<String> {
268        let query = resolve_option_name(name).ok()?;
269        let pane_target = PaneTarget::with_window(session_name.clone(), window_index, pane_index);
270        let window_target = WindowTarget::with_window(session_name.clone(), window_index);
271        self.resolve_name_from_nodes(
272            &query,
273            [
274                Some(&self.server_global),
275                self.panes.get(&pane_target),
276                self.windows.get(&window_target),
277                Some(&self.window_global),
278                self.sessions.get(session_name),
279                Some(&self.session_global),
280            ],
281        )
282        .or_else(|| self.default_value_as_string(&query))
283    }
284
285    /// Resolves a string-keyed option for tmux format evaluation in a pane context.
286    #[must_use]
287    pub fn resolve_name_for_pane_format(
288        &self,
289        session_name: &SessionName,
290        window_index: u32,
291        pane_index: u32,
292        name: &str,
293    ) -> Option<String> {
294        let query = resolve_exact_option_name(name).ok()?;
295        let pane_target = PaneTarget::with_window(session_name.clone(), window_index, pane_index);
296        let window_target = WindowTarget::with_window(session_name.clone(), window_index);
297        let value = self
298            .resolve_name_from_nodes(
299                &query,
300                [
301                    Some(&self.server_global),
302                    self.panes.get(&pane_target),
303                    self.windows.get(&window_target),
304                    Some(&self.window_global),
305                    self.sessions.get(session_name),
306                    Some(&self.session_global),
307                ],
308            )
309            .or_else(|| self.default_value_as_string(&query))?;
310        Some(format_rendered_option_value(&query, value))
311    }
312
313    /// Returns the option snapshot that future panes in a session should inherit.
314    #[must_use]
315    pub fn resolved(&self, session_name: &SessionName) -> HashMap<OptionName, String> {
316        registry()
317            .iter()
318            .map(|metadata| {
319                let query = OptionQuery::known(metadata.option());
320                let value = self
321                    .resolve(Some(session_name), metadata.option())
322                    .map(str::to_owned)
323                    .or_else(|| self.default_value_as_string(&query))
324                    .unwrap_or_default();
325                (metadata.option(), value)
326            })
327            .collect()
328    }
329
330    /// Returns the option snapshot for a pane after the full inheritance chain is applied.
331    #[must_use]
332    pub fn resolved_for_pane(
333        &self,
334        session_name: &SessionName,
335        window_index: u32,
336        pane_index: u32,
337    ) -> HashMap<OptionName, String> {
338        registry()
339            .iter()
340            .map(|metadata| {
341                let query = OptionQuery::known(metadata.option());
342                let value = self
343                    .resolve_for_pane(session_name, window_index, pane_index, metadata.option())
344                    .map(str::to_owned)
345                    .or_else(|| self.default_value_as_string(&query))
346                    .unwrap_or_default();
347                (metadata.option(), value)
348            })
349            .collect()
350    }
351
352    pub(super) fn node_for_exact_scope_mut(
353        &mut self,
354        scope: &OptionScopeSelector,
355    ) -> &mut OptionNode {
356        match scope {
357            OptionScopeSelector::ServerGlobal => &mut self.server_global,
358            OptionScopeSelector::SessionGlobal => &mut self.session_global,
359            OptionScopeSelector::WindowGlobal => &mut self.window_global,
360            OptionScopeSelector::Session(session_name) => {
361                self.sessions.entry(session_name.clone()).or_default()
362            }
363            OptionScopeSelector::Window(target) => self.windows.entry(target.clone()).or_default(),
364            OptionScopeSelector::Pane(target) => self.panes.entry(target.clone()).or_default(),
365        }
366    }
367
368    pub(super) fn node_for_exact_scope(&self, scope: &OptionScopeSelector) -> Option<&OptionNode> {
369        match scope {
370            OptionScopeSelector::ServerGlobal => Some(&self.server_global),
371            OptionScopeSelector::SessionGlobal => Some(&self.session_global),
372            OptionScopeSelector::WindowGlobal => Some(&self.window_global),
373            OptionScopeSelector::Session(session_name) => self.sessions.get(session_name),
374            OptionScopeSelector::Window(target) => self.windows.get(target),
375            OptionScopeSelector::Pane(target) => self.panes.get(target),
376        }
377    }
378
379    pub(super) fn resolve_query<'a>(
380        &'a self,
381        context: &ResolveContext<'a>,
382        query: &OptionQuery,
383    ) -> Option<&'a OptionEntry> {
384        for node in self.chain_for_context(context, query) {
385            if let Some(entry) = node.entry(query.canonical_name()) {
386                if query.index().is_none() || entry.value(query.index()).is_some() {
387                    return Some(entry);
388                }
389            }
390        }
391        None
392    }
393
394    pub(super) fn effective_value_for_scope(
395        &self,
396        scope: &OptionScopeSelector,
397        query: &OptionQuery,
398    ) -> Option<String> {
399        match scope {
400            OptionScopeSelector::ServerGlobal => self
401                .resolve_query(&ResolveContext::Server, query)
402                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
403            OptionScopeSelector::SessionGlobal => self
404                .resolve_query(&ResolveContext::SessionGlobal, query)
405                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
406            OptionScopeSelector::WindowGlobal => self
407                .resolve_query(&ResolveContext::WindowGlobal, query)
408                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
409            OptionScopeSelector::Session(session_name) => self
410                .resolve_query(&ResolveContext::Session(session_name), query)
411                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
412            OptionScopeSelector::Window(target) => self
413                .resolve_query(
414                    &ResolveContext::Window(target.session_name(), target.window_index()),
415                    query,
416                )
417                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
418            OptionScopeSelector::Pane(target) => self
419                .resolve_query(
420                    &ResolveContext::Pane(
421                        target.session_name(),
422                        target.window_index(),
423                        target.pane_index(),
424                    ),
425                    query,
426                )
427                .and_then(|entry| entry.value(query.index()).map(str::to_owned)),
428        }
429    }
430
431    pub(super) fn explicit_value_for_scope(
432        &self,
433        scope: &OptionScopeSelector,
434        query: &OptionQuery,
435    ) -> Option<String> {
436        self.node_for_exact_scope(scope)
437            .and_then(|node| node.value(query.canonical_name(), query.index()))
438            .map(str::to_owned)
439    }
440
441    pub(super) fn default_entry_for_scope(
442        &self,
443        query: &OptionQuery,
444        scope: OptionScopeSelector,
445    ) -> Option<OptionEntry> {
446        let default = query.default_value()?;
447
448        if query.is_array() {
449            let items = default_array_items(query, default).ok()?;
450            Some(OptionEntry::new_array(query, scope, items))
451        } else {
452            let value =
453                normalize_scalar_value(query, Some(default_scalar_text(default)), None).ok()?;
454            Some(OptionEntry::new_scalar(query, scope, value))
455        }
456    }
457
458    pub(super) fn default_value_as_string(&self, query: &OptionQuery) -> Option<String> {
459        let default = query.default_value()?;
460        match default {
461            DefaultValue::Scalar(value) => Some(value.to_owned()),
462            DefaultValue::Array(values) => Some(values.join(query.separator())),
463        }
464    }
465
466    fn node_for_global_root(&self, root: GlobalRoot) -> Option<&OptionNode> {
467        match root {
468            GlobalRoot::Server => Some(&self.server_global),
469            GlobalRoot::Session => Some(&self.session_global),
470            GlobalRoot::Window => Some(&self.window_global),
471        }
472    }
473
474    fn chain_for_context<'a>(
475        &'a self,
476        context: &ResolveContext<'a>,
477        query: &OptionQuery,
478    ) -> Vec<&'a OptionNode> {
479        match context {
480            ResolveContext::Server => vec![&self.server_global],
481            // tmux global roots are independent; known options resolve to their
482            // own global root only. User options cross all roots.
483            ResolveContext::SessionGlobal => {
484                if query.is_user() {
485                    vec![&self.session_global, &self.server_global]
486                } else {
487                    vec![&self.session_global]
488                }
489            }
490            ResolveContext::WindowGlobal => {
491                if query.is_user() {
492                    vec![
493                        &self.window_global,
494                        &self.session_global,
495                        &self.server_global,
496                    ]
497                } else {
498                    vec![&self.window_global]
499                }
500            }
501            ResolveContext::Session(session_name) => {
502                let mut chain = Vec::new();
503                if scope_allows_session(query) {
504                    if let Some(node) = self.sessions.get(*session_name) {
505                        chain.push(node);
506                    }
507                }
508                push_known_global_roots(&mut chain, self, query);
509                chain
510            }
511            ResolveContext::Window(session_name, window_index) => {
512                let mut chain = Vec::new();
513                if scope_allows_window(query) {
514                    let target = WindowTarget::with_window((*session_name).clone(), *window_index);
515                    if let Some(node) = self.windows.get(&target) {
516                        chain.push(node);
517                    }
518                }
519                if scope_allows_session(query) {
520                    if let Some(node) = self.sessions.get(*session_name) {
521                        chain.push(node);
522                    }
523                }
524                push_known_global_roots(&mut chain, self, query);
525                chain
526            }
527            ResolveContext::Pane(session_name, window_index, pane_index) => {
528                let mut chain = Vec::new();
529                if scope_allows_pane(query) {
530                    let pane_target = PaneTarget::with_window(
531                        (*session_name).clone(),
532                        *window_index,
533                        *pane_index,
534                    );
535                    if let Some(node) = self.panes.get(&pane_target) {
536                        chain.push(node);
537                    }
538                }
539                if scope_allows_window(query) {
540                    let window_target =
541                        WindowTarget::with_window((*session_name).clone(), *window_index);
542                    if let Some(node) = self.windows.get(&window_target) {
543                        chain.push(node);
544                    }
545                }
546                if scope_allows_session(query) {
547                    if let Some(node) = self.sessions.get(*session_name) {
548                        chain.push(node);
549                    }
550                }
551                push_known_global_roots(&mut chain, self, query);
552                chain
553            }
554        }
555    }
556
557    fn default_rendered_value(&self, query: &OptionQuery) -> Option<&'static str> {
558        if query.index().is_some() {
559            return None;
560        }
561        match query.default_value()? {
562            DefaultValue::Scalar(value) => Some(value),
563            // Array defaults with DefaultValue::Array require a computed join;
564            // the static-str callers fall back to default_value_as_string for
565            // those options. Currently all array options use Scalar defaults.
566            DefaultValue::Array(_) => None,
567        }
568    }
569
570    fn resolve_name_from_nodes<'a, I>(&self, query: &OptionQuery, nodes: I) -> Option<String>
571    where
572        I: IntoIterator<Item = Option<&'a OptionNode>>,
573    {
574        for node in nodes.into_iter().flatten() {
575            if let Some(entry) = node.entry(query.canonical_name()) {
576                if let Some(value) = entry.value(query.index()) {
577                    return Some(value.to_owned());
578                }
579            }
580        }
581        None
582    }
583}