Skip to main content

rmux_core/options/
registry.rs

1use rmux_proto::types::OptionScopeSelector;
2use rmux_proto::{OptionName, RmuxError};
3
4pub(crate) const SCOPE_SERVER: u8 = 0x1;
5pub(crate) const SCOPE_SESSION: u8 = 0x2;
6pub(crate) const SCOPE_WINDOW: u8 = 0x4;
7pub(crate) const SCOPE_PANE: u8 = 0x8;
8
9pub(crate) const SHOW_SERVER: u8 = 0x1;
10pub(crate) const SHOW_SESSION: u8 = 0x2;
11pub(crate) const SHOW_WINDOW: u8 = 0x4;
12pub(crate) const SHOW_PANE: u8 = 0x8;
13
14pub(crate) const EFFECT_NONE: OptionChangeMask = OptionChangeMask(0);
15pub(crate) const EFFECT_RENDER: OptionChangeMask = OptionChangeMask(0x1);
16pub(crate) const EFFECT_PANE_STYLE: OptionChangeMask = OptionChangeMask(0x2);
17pub(crate) const EFFECT_TRANSCRIPT_LIMIT: OptionChangeMask = OptionChangeMask(0x4);
18pub(crate) const EFFECT_STYLE_PARSE: OptionChangeMask = OptionChangeMask(0x8);
19pub(crate) const EFFECT_TERMINAL_FEATURES: OptionChangeMask = OptionChangeMask(0x10);
20pub(crate) const EFFECT_KEY_TABLE: OptionChangeMask = OptionChangeMask(0x20);
21pub(crate) const EFFECT_USER_KEYS: OptionChangeMask = OptionChangeMask(0x40);
22pub(crate) const EFFECT_CURSOR: OptionChangeMask = OptionChangeMask(0x80);
23pub(crate) const EFFECT_FILL_CHARACTER: OptionChangeMask = OptionChangeMask(0x100);
24pub(crate) const EFFECT_STATUS_TIMER: OptionChangeMask = OptionChangeMask(0x200);
25pub(crate) const EFFECT_ALERTS: OptionChangeMask = OptionChangeMask(0x400);
26pub(crate) const EFFECT_LAYOUT: OptionChangeMask = OptionChangeMask(0x800);
27pub(crate) const EFFECT_SCROLLBAR_STYLE: OptionChangeMask = OptionChangeMask(0x1000);
28pub(crate) const EFFECT_CODEPOINT_WIDTHS: OptionChangeMask = OptionChangeMask(0x2000);
29pub(crate) const EFFECT_INPUT_BUFFER_SIZE: OptionChangeMask = OptionChangeMask(0x4000);
30pub(crate) const EFFECT_PANE_COLOURS: OptionChangeMask = OptionChangeMask(0x8000);
31pub(crate) const EFFECT_AUTOMATIC_RENAME: OptionChangeMask = OptionChangeMask(0x10000);
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub(crate) enum GlobalRoot {
35    Server,
36    Session,
37    Window,
38}
39
40#[allow(dead_code)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub(crate) enum OptionValueType {
43    String,
44    Number { minimum: u32 },
45    Key,
46    Colour,
47    Flag,
48    Choice(&'static [&'static str]),
49    Command,
50}
51
52#[allow(dead_code)]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub(crate) enum DefaultValue {
55    Scalar(&'static str),
56    Array(&'static [&'static str]),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct OptionChangeMask(u32);
61
62impl OptionChangeMask {
63    pub const fn contains(self, other: Self) -> bool {
64        (self.0 & other.0) == other.0
65    }
66
67    pub const fn union(self, other: Self) -> Self {
68        Self(self.0 | other.0)
69    }
70
71    pub const fn is_empty(self) -> bool {
72        self.0 == 0
73    }
74
75    /// Returns whether the mask includes alert-timer side effects.
76    #[must_use]
77    pub const fn affects_alerts(self) -> bool {
78        self.contains(EFFECT_ALERTS)
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub(crate) struct OptionMetadata {
84    option: OptionName,
85    name: &'static str,
86    aliases: &'static [&'static str],
87    show_mask: u8,
88    scope_mask: u8,
89    global_root: GlobalRoot,
90    value_type: OptionValueType,
91    default: DefaultValue,
92    separator: &'static str,
93    is_array: bool,
94    effects: OptionChangeMask,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct OptionQuery {
99    name: String,
100    metadata: Option<&'static OptionMetadata>,
101    index: Option<u32>,
102}
103
104#[path = "table.rs"]
105mod table;
106use table::OPTIONS;
107
108impl OptionMetadata {
109    pub(crate) fn option(&self) -> OptionName {
110        self.option
111    }
112
113    pub(crate) const fn name(&self) -> &'static str {
114        self.name
115    }
116
117    pub(crate) const fn aliases(&self) -> &'static [&'static str] {
118        self.aliases
119    }
120
121    pub(crate) const fn scope_mask(&self) -> u8 {
122        self.scope_mask
123    }
124
125    pub(crate) const fn global_root(&self) -> GlobalRoot {
126        self.global_root
127    }
128
129    pub(crate) const fn value_type(&self) -> OptionValueType {
130        self.value_type
131    }
132
133    pub(crate) const fn default_value(&self) -> DefaultValue {
134        self.default
135    }
136
137    pub(crate) const fn separator(&self) -> &'static str {
138        self.separator
139    }
140
141    pub(crate) const fn is_array(&self) -> bool {
142        self.is_array
143    }
144
145    pub(crate) const fn effects(&self) -> OptionChangeMask {
146        self.effects
147    }
148
149    pub(crate) const fn visible_in(&self, scope: u8) -> bool {
150        (self.show_mask & scope) != 0
151    }
152
153    pub(crate) const fn supports_scope(&self, scope: &OptionScopeSelector) -> bool {
154        let mask = match scope {
155            OptionScopeSelector::ServerGlobal => SCOPE_SERVER,
156            OptionScopeSelector::SessionGlobal | OptionScopeSelector::Session(_) => SCOPE_SESSION,
157            OptionScopeSelector::WindowGlobal | OptionScopeSelector::Window(_) => SCOPE_WINDOW,
158            OptionScopeSelector::Pane(_) => SCOPE_PANE,
159        };
160        (self.scope_mask & mask) != 0
161    }
162}
163
164impl OptionQuery {
165    pub(crate) fn known(option: OptionName) -> Self {
166        let metadata = option_metadata(option);
167        Self {
168            name: metadata.name().to_owned(),
169            metadata: Some(metadata),
170            index: None,
171        }
172    }
173
174    pub(crate) fn metadata(&self) -> Option<&'static OptionMetadata> {
175        self.metadata
176    }
177
178    pub(crate) fn canonical_name(&self) -> &str {
179        &self.name
180    }
181
182    pub(crate) fn index(&self) -> Option<u32> {
183        self.index
184    }
185
186    pub(crate) fn known_option(&self) -> Option<OptionName> {
187        self.metadata.map(OptionMetadata::option)
188    }
189
190    pub fn is_user(&self) -> bool {
191        self.metadata.is_none()
192    }
193
194    #[must_use]
195    pub fn supports_scope(&self, scope: &OptionScopeSelector) -> bool {
196        self.metadata
197            .map(|metadata| metadata.supports_scope(scope))
198            .unwrap_or(true)
199    }
200
201    pub(crate) fn is_array(&self) -> bool {
202        self.metadata.is_some_and(OptionMetadata::is_array)
203    }
204
205    pub(crate) fn value_type(&self) -> OptionValueType {
206        self.metadata
207            .map(OptionMetadata::value_type)
208            .unwrap_or(OptionValueType::String)
209    }
210
211    pub(crate) fn separator(&self) -> &'static str {
212        self.metadata
213            .map(OptionMetadata::separator)
214            .unwrap_or_default()
215    }
216
217    pub(crate) fn default_value(&self) -> Option<DefaultValue> {
218        self.metadata.map(OptionMetadata::default_value)
219    }
220
221    pub(crate) fn effects(&self) -> OptionChangeMask {
222        self.metadata
223            .map(OptionMetadata::effects)
224            .unwrap_or(EFFECT_PANE_STYLE)
225    }
226}
227
228/// Returns the known option registry.
229pub(crate) fn registry() -> &'static [OptionMetadata] {
230    OPTIONS
231}
232
233pub(crate) fn option_metadata(option: OptionName) -> &'static OptionMetadata {
234    OPTIONS
235        .iter()
236        .find(|metadata| metadata.option == option)
237        .expect("every OptionName variant must have registry metadata")
238}
239
240/// Resolves a known option name using tmux-style aliasing and prefix matching.
241pub fn resolve_option_name(name: &str) -> Result<OptionQuery, RmuxError> {
242    let (base_name, index) = split_array_index(name)?;
243
244    if base_name.starts_with('@') {
245        if index.is_some() {
246            return Err(RmuxError::Server(format!(
247                "user option does not support array indexes: {name}"
248            )));
249        }
250        return Ok(OptionQuery {
251            name: base_name.to_owned(),
252            metadata: None,
253            index: None,
254        });
255    }
256
257    if let Some(metadata) = OPTIONS.iter().find(|metadata| metadata.name == base_name) {
258        return Ok(OptionQuery {
259            name: metadata.name().to_owned(),
260            metadata: Some(metadata),
261            index,
262        });
263    }
264
265    if let Some(metadata) = OPTIONS
266        .iter()
267        .find(|metadata| metadata.aliases().contains(&base_name))
268    {
269        return Ok(OptionQuery {
270            name: metadata.name().to_owned(),
271            metadata: Some(metadata),
272            index,
273        });
274    }
275
276    // Prefix matching: check canonical names and aliases for unambiguous prefix.
277    let mut matches: Vec<&OptionMetadata> = OPTIONS
278        .iter()
279        .filter(|metadata| {
280            metadata.name().starts_with(base_name)
281                || metadata
282                    .aliases()
283                    .iter()
284                    .any(|alias| alias.starts_with(base_name))
285        })
286        .collect();
287    matches.dedup_by_key(|metadata| metadata.name());
288    match matches.as_slice() {
289        [metadata] => Ok(OptionQuery {
290            name: metadata.name().to_owned(),
291            metadata: Some(metadata),
292            index,
293        }),
294        [] => Err(RmuxError::Server(format!("unknown option: {base_name}"))),
295        _ => Err(RmuxError::Server(format!("ambiguous option: {base_name}"))),
296    }
297}
298
299/// Resolves a known option by name when the caller only accepts registered options.
300#[must_use]
301pub fn option_name_by_name(name: &str) -> Option<OptionName> {
302    resolve_option_name(name).ok()?.known_option()
303}
304
305/// Returns whether a known option change may affect attached rendering.
306#[must_use]
307pub fn option_affects_rendering(option: OptionName) -> bool {
308    let effects = option_metadata(option).effects();
309    effects.contains(EFFECT_RENDER)
310        || effects.contains(EFFECT_PANE_STYLE)
311        || effects.contains(EFFECT_STYLE_PARSE)
312        || effects.contains(EFFECT_CODEPOINT_WIDTHS)
313        || effects.contains(EFFECT_CURSOR)
314        || effects.contains(EFFECT_FILL_CHARACTER)
315        || effects.contains(EFFECT_STATUS_TIMER)
316        || effects.contains(EFFECT_LAYOUT)
317        || effects.contains(EFFECT_SCROLLBAR_STYLE)
318        || effects.contains(EFFECT_PANE_COLOURS)
319}
320
321/// Returns whether a known option change may affect alert runtime state.
322#[must_use]
323pub fn option_affects_alerts(option: OptionName) -> bool {
324    option_metadata(option).effects().affects_alerts()
325}
326
327fn split_array_index(name: &str) -> Result<(&str, Option<u32>), RmuxError> {
328    let Some(start) = name.rfind('[') else {
329        if name.contains(']') {
330            return Err(RmuxError::Server(format!(
331                "invalid option index syntax: {name}"
332            )));
333        }
334        return Ok((name, None));
335    };
336
337    if !name.ends_with(']') {
338        return Err(RmuxError::Server(format!(
339            "invalid option index syntax: {name}"
340        )));
341    }
342
343    let base_name = &name[..start];
344    let index_text = &name[start + 1..name.len() - 1];
345    if base_name.is_empty() || index_text.is_empty() {
346        return Err(RmuxError::Server(format!(
347            "invalid option index syntax: {name}"
348        )));
349    }
350
351    let index = index_text
352        .parse::<u32>()
353        .map_err(|_| RmuxError::Server(format!("invalid option index syntax: {name}")))?;
354    Ok((base_name, Some(index)))
355}