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    /// Returns the exact explicit value for a tmux-style option name at a scope.
442    pub fn explicit_value_by_name(
443        &self,
444        scope: &OptionScopeSelector,
445        name: &str,
446    ) -> Result<(String, Option<String>), rmux_proto::RmuxError> {
447        let query = resolve_option_name(name)?;
448        Ok((
449            query.canonical_name().to_owned(),
450            self.explicit_value_for_scope(scope, &query),
451        ))
452    }
453
454    /// Returns all explicit option entries stored exactly at a scope.
455    pub fn explicit_entries_for_scope(&self, scope: &OptionScopeSelector) -> Vec<(String, String)> {
456        self.node_for_exact_scope(scope)
457            .map(|node| {
458                node.entries
459                    .values()
460                    .map(|entry| (entry.name.clone(), entry.rendered().to_owned()))
461                    .collect()
462            })
463            .unwrap_or_default()
464    }
465
466    pub(super) fn default_entry_for_scope(
467        &self,
468        query: &OptionQuery,
469        scope: OptionScopeSelector,
470    ) -> Option<OptionEntry> {
471        let default = query.default_value()?;
472
473        if query.is_array() {
474            let items = default_array_items(query, default).ok()?;
475            Some(OptionEntry::new_array(query, scope, items))
476        } else {
477            let value =
478                normalize_scalar_value(query, Some(default_scalar_text(default)), None).ok()?;
479            Some(OptionEntry::new_scalar(query, scope, value))
480        }
481    }
482
483    pub(super) fn default_value_as_string(&self, query: &OptionQuery) -> Option<String> {
484        let default = query.default_value()?;
485        match default {
486            DefaultValue::Scalar(value) => Some(value.to_owned()),
487            DefaultValue::Array(values) => Some(values.join(query.separator())),
488        }
489    }
490
491    fn node_for_global_root(&self, root: GlobalRoot) -> Option<&OptionNode> {
492        match root {
493            GlobalRoot::Server => Some(&self.server_global),
494            GlobalRoot::Session => Some(&self.session_global),
495            GlobalRoot::Window => Some(&self.window_global),
496        }
497    }
498
499    fn chain_for_context<'a>(
500        &'a self,
501        context: &ResolveContext<'a>,
502        query: &OptionQuery,
503    ) -> Vec<&'a OptionNode> {
504        match context {
505            ResolveContext::Server => vec![&self.server_global],
506            // tmux global roots are independent; known options resolve to their
507            // own global root only. User options cross all roots.
508            ResolveContext::SessionGlobal => {
509                if query.is_user() {
510                    vec![&self.session_global, &self.server_global]
511                } else {
512                    vec![&self.session_global]
513                }
514            }
515            ResolveContext::WindowGlobal => {
516                if query.is_user() {
517                    vec![
518                        &self.window_global,
519                        &self.session_global,
520                        &self.server_global,
521                    ]
522                } else {
523                    vec![&self.window_global]
524                }
525            }
526            ResolveContext::Session(session_name) => {
527                let mut chain = Vec::new();
528                if let Some(node) = self.sessions.get(*session_name) {
529                    if scope_allows_session(query)
530                        || node.contains(query.canonical_name(), query.index())
531                    {
532                        chain.push(node);
533                    }
534                }
535                push_known_global_roots(&mut chain, self, query);
536                chain
537            }
538            ResolveContext::Window(session_name, window_index) => {
539                let mut chain = Vec::new();
540                let target = WindowTarget::with_window((*session_name).clone(), *window_index);
541                if let Some(node) = self.windows.get(&target) {
542                    if scope_allows_window(query)
543                        || node.contains(query.canonical_name(), query.index())
544                    {
545                        chain.push(node);
546                    }
547                }
548                if let Some(node) = self.sessions.get(*session_name) {
549                    if scope_allows_session(query)
550                        || node.contains(query.canonical_name(), query.index())
551                    {
552                        chain.push(node);
553                    }
554                }
555                push_known_global_roots(&mut chain, self, query);
556                chain
557            }
558            ResolveContext::Pane(session_name, window_index, pane_index) => {
559                let mut chain = Vec::new();
560                let pane_target =
561                    PaneTarget::with_window((*session_name).clone(), *window_index, *pane_index);
562                if let Some(node) = self.panes.get(&pane_target) {
563                    if scope_allows_pane(query)
564                        || node.contains(query.canonical_name(), query.index())
565                    {
566                        chain.push(node);
567                    }
568                }
569                let window_target =
570                    WindowTarget::with_window((*session_name).clone(), *window_index);
571                if let Some(node) = self.windows.get(&window_target) {
572                    if scope_allows_window(query)
573                        || node.contains(query.canonical_name(), query.index())
574                    {
575                        chain.push(node);
576                    }
577                }
578                if let Some(node) = self.sessions.get(*session_name) {
579                    if scope_allows_session(query)
580                        || node.contains(query.canonical_name(), query.index())
581                    {
582                        chain.push(node);
583                    }
584                }
585                push_known_global_roots(&mut chain, self, query);
586                chain
587            }
588        }
589    }
590
591    fn default_rendered_value(&self, query: &OptionQuery) -> Option<&'static str> {
592        if query.index().is_some() {
593            return None;
594        }
595        match query.default_value()? {
596            DefaultValue::Scalar(value) => Some(value),
597            // Array defaults with DefaultValue::Array require a computed join;
598            // the static-str callers fall back to default_value_as_string for
599            // those options. Currently all array options use Scalar defaults.
600            DefaultValue::Array(_) => None,
601        }
602    }
603
604    fn resolve_name_from_nodes<'a, I>(&self, query: &OptionQuery, nodes: I) -> Option<String>
605    where
606        I: IntoIterator<Item = Option<&'a OptionNode>>,
607    {
608        for node in nodes.into_iter().flatten() {
609            if let Some(entry) = node.entry(query.canonical_name()) {
610                if let Some(value) = entry.value(query.index()) {
611                    return Some(value.to_owned());
612                }
613            }
614        }
615        None
616    }
617}