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