1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use rmux_proto::types::OptionScopeSelector;
5use rmux_proto::{OptionName, RmuxError};
6
7pub(crate) const SCOPE_SERVER: u8 = 0x1;
8pub(crate) const SCOPE_SESSION: u8 = 0x2;
9pub(crate) const SCOPE_WINDOW: u8 = 0x4;
10pub(crate) const SCOPE_PANE: u8 = 0x8;
11
12pub(crate) const SHOW_SERVER: u8 = 0x1;
13pub(crate) const SHOW_SESSION: u8 = 0x2;
14pub(crate) const SHOW_WINDOW: u8 = 0x4;
15pub(crate) const SHOW_PANE: u8 = 0x8;
16
17pub(crate) const EFFECT_NONE: OptionChangeMask = OptionChangeMask(0);
18pub(crate) const EFFECT_RENDER: OptionChangeMask = OptionChangeMask(0x1);
19pub(crate) const EFFECT_PANE_STYLE: OptionChangeMask = OptionChangeMask(0x2);
20pub(crate) const EFFECT_TRANSCRIPT_LIMIT: OptionChangeMask = OptionChangeMask(0x4);
21pub(crate) const EFFECT_STYLE_PARSE: OptionChangeMask = OptionChangeMask(0x8);
22pub(crate) const EFFECT_TERMINAL_FEATURES: OptionChangeMask = OptionChangeMask(0x10);
23pub(crate) const EFFECT_KEY_TABLE: OptionChangeMask = OptionChangeMask(0x20);
24pub(crate) const EFFECT_USER_KEYS: OptionChangeMask = OptionChangeMask(0x40);
25pub(crate) const EFFECT_CURSOR: OptionChangeMask = OptionChangeMask(0x80);
26pub(crate) const EFFECT_FILL_CHARACTER: OptionChangeMask = OptionChangeMask(0x100);
27pub(crate) const EFFECT_STATUS_TIMER: OptionChangeMask = OptionChangeMask(0x200);
28pub(crate) const EFFECT_ALERTS: OptionChangeMask = OptionChangeMask(0x400);
29pub(crate) const EFFECT_LAYOUT: OptionChangeMask = OptionChangeMask(0x800);
30pub(crate) const EFFECT_SCROLLBAR_STYLE: OptionChangeMask = OptionChangeMask(0x1000);
31pub(crate) const EFFECT_CODEPOINT_WIDTHS: OptionChangeMask = OptionChangeMask(0x2000);
32pub(crate) const EFFECT_INPUT_BUFFER_SIZE: OptionChangeMask = OptionChangeMask(0x4000);
33pub(crate) const EFFECT_PANE_COLOURS: OptionChangeMask = OptionChangeMask(0x8000);
34pub(crate) const EFFECT_AUTOMATIC_RENAME: OptionChangeMask = OptionChangeMask(0x10000);
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) enum GlobalRoot {
38 Server,
39 Session,
40 Window,
41}
42
43#[allow(dead_code)]
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum OptionValueType {
46 String,
47 Number { minimum: u32 },
48 Key,
49 Colour,
50 Flag,
51 Choice(&'static [&'static str]),
52 Command,
53}
54
55#[allow(dead_code)]
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum DefaultValue {
58 Scalar(&'static str),
59 Array(&'static [&'static str]),
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct OptionChangeMask(u32);
64
65impl OptionChangeMask {
66 pub const fn contains(self, other: Self) -> bool {
67 (self.0 & other.0) == other.0
68 }
69
70 pub const fn union(self, other: Self) -> Self {
71 Self(self.0 | other.0)
72 }
73
74 pub const fn is_empty(self) -> bool {
75 self.0 == 0
76 }
77
78 #[must_use]
80 pub const fn affects_alerts(self) -> bool {
81 self.contains(EFFECT_ALERTS)
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub(crate) struct OptionMetadata {
87 option: OptionName,
88 name: &'static str,
89 aliases: &'static [&'static str],
90 show_mask: u8,
91 scope_mask: u8,
92 global_root: GlobalRoot,
93 value_type: OptionValueType,
94 default: DefaultValue,
95 separator: &'static str,
96 is_array: bool,
97 effects: OptionChangeMask,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct OptionQuery {
102 name: String,
103 metadata: Option<&'static OptionMetadata>,
104 index: Option<u32>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum OptionLookupError {
110 InvalidName(String),
112 AmbiguousPrefix(String),
114 InvalidIndexSyntax(String),
116 UserOptionArrayIndex(String),
118}
119
120impl OptionLookupError {
121 #[must_use]
123 pub const fn is_quiet_set_option_lookup_error(&self) -> bool {
124 matches!(self, Self::InvalidName(_) | Self::AmbiguousPrefix(_))
125 }
126
127 #[must_use]
129 pub fn into_rmux_error(self) -> RmuxError {
130 match self {
131 Self::InvalidName(name) => RmuxError::Server(format!("invalid option: {name}")),
132 Self::AmbiguousPrefix(name) => RmuxError::Server(format!("ambiguous option: {name}")),
133 Self::InvalidIndexSyntax(name) => {
134 RmuxError::Server(format!("invalid option index syntax: {name}"))
135 }
136 Self::UserOptionArrayIndex(name) => RmuxError::Server(format!(
137 "user option does not support array indexes: {name}"
138 )),
139 }
140 }
141}
142
143#[path = "table.rs"]
144mod table;
145use table::OPTIONS;
146
147static OPTION_LOOKUP: OnceLock<OptionLookup> = OnceLock::new();
148
149struct OptionLookup {
150 by_option: HashMap<OptionName, &'static OptionMetadata>,
151 by_name: HashMap<&'static str, &'static OptionMetadata>,
152 by_alias: HashMap<&'static str, &'static OptionMetadata>,
153}
154
155impl OptionMetadata {
156 pub(crate) fn option(&self) -> OptionName {
157 self.option
158 }
159
160 pub(crate) const fn name(&self) -> &'static str {
161 self.name
162 }
163
164 pub(crate) const fn aliases(&self) -> &'static [&'static str] {
165 self.aliases
166 }
167
168 pub(crate) const fn scope_mask(&self) -> u8 {
169 self.scope_mask
170 }
171
172 pub(crate) const fn global_root(&self) -> GlobalRoot {
173 self.global_root
174 }
175
176 pub(crate) const fn value_type(&self) -> OptionValueType {
177 self.value_type
178 }
179
180 pub(crate) const fn default_value(&self) -> DefaultValue {
181 self.default
182 }
183
184 pub(crate) const fn separator(&self) -> &'static str {
185 self.separator
186 }
187
188 pub(crate) const fn is_array(&self) -> bool {
189 self.is_array
190 }
191
192 pub(crate) const fn effects(&self) -> OptionChangeMask {
193 self.effects
194 }
195
196 pub(crate) const fn visible_in(&self, scope: u8) -> bool {
197 (self.show_mask & scope) != 0
198 }
199
200 pub(crate) const fn supports_scope(&self, scope: &OptionScopeSelector) -> bool {
201 let mask = match scope {
202 OptionScopeSelector::ServerGlobal => SCOPE_SERVER,
203 OptionScopeSelector::SessionGlobal | OptionScopeSelector::Session(_) => SCOPE_SESSION,
204 OptionScopeSelector::WindowGlobal | OptionScopeSelector::Window(_) => SCOPE_WINDOW,
205 OptionScopeSelector::Pane(_) => SCOPE_PANE,
206 };
207 (self.scope_mask & mask) != 0
208 }
209}
210
211impl OptionQuery {
212 pub(crate) fn known(option: OptionName) -> Self {
213 let metadata = option_metadata(option);
214 Self {
215 name: metadata.name().to_owned(),
216 metadata: Some(metadata),
217 index: None,
218 }
219 }
220
221 pub(crate) fn metadata(&self) -> Option<&'static OptionMetadata> {
222 self.metadata
223 }
224
225 pub(crate) fn canonical_name(&self) -> &str {
226 &self.name
227 }
228
229 pub(crate) fn index(&self) -> Option<u32> {
230 self.index
231 }
232
233 pub(crate) fn known_option(&self) -> Option<OptionName> {
234 self.metadata.map(OptionMetadata::option)
235 }
236
237 pub fn is_user(&self) -> bool {
238 self.metadata.is_none()
239 }
240
241 #[must_use]
242 pub fn supports_scope(&self, scope: &OptionScopeSelector) -> bool {
243 self.metadata
244 .map(|metadata| metadata.supports_scope(scope))
245 .unwrap_or(true)
246 }
247
248 pub(crate) fn is_array(&self) -> bool {
249 self.metadata.is_some_and(OptionMetadata::is_array)
250 }
251
252 pub(crate) fn value_type(&self) -> OptionValueType {
253 self.metadata
254 .map(OptionMetadata::value_type)
255 .unwrap_or(OptionValueType::String)
256 }
257
258 pub(crate) fn separator(&self) -> &'static str {
259 self.metadata
260 .map(OptionMetadata::separator)
261 .unwrap_or_default()
262 }
263
264 pub(crate) fn default_value(&self) -> Option<DefaultValue> {
265 self.metadata.map(OptionMetadata::default_value)
266 }
267
268 pub(crate) fn effects(&self) -> OptionChangeMask {
269 self.metadata
270 .map(OptionMetadata::effects)
271 .unwrap_or(EFFECT_PANE_STYLE)
272 }
273}
274
275pub(crate) fn registry() -> &'static [OptionMetadata] {
277 OPTIONS
278}
279
280pub(crate) fn option_metadata(option: OptionName) -> &'static OptionMetadata {
281 option_lookup()
282 .by_option
283 .get(&option)
284 .copied()
285 .expect("every OptionName variant must have registry metadata")
286}
287
288pub fn resolve_option_name(name: &str) -> Result<OptionQuery, RmuxError> {
290 resolve_option_name_typed(name).map_err(OptionLookupError::into_rmux_error)
291}
292
293pub fn resolve_option_name_typed(name: &str) -> Result<OptionQuery, OptionLookupError> {
295 let (base_name, index) = split_array_index_typed(name)?;
296 let lookup = option_lookup();
297
298 if base_name.starts_with('@') {
299 if index.is_some() {
300 return Err(OptionLookupError::UserOptionArrayIndex(name.to_owned()));
301 }
302 return Ok(OptionQuery {
303 name: base_name.to_owned(),
304 metadata: None,
305 index: None,
306 });
307 }
308
309 if let Some(metadata) = lookup.by_name.get(base_name).copied() {
310 return Ok(OptionQuery {
311 name: metadata.name().to_owned(),
312 metadata: Some(metadata),
313 index,
314 });
315 }
316
317 if let Some(metadata) = lookup.by_alias.get(base_name).copied() {
318 return Ok(OptionQuery {
319 name: metadata.name().to_owned(),
320 metadata: Some(metadata),
321 index,
322 });
323 }
324
325 let mut matches: Vec<&OptionMetadata> = OPTIONS
327 .iter()
328 .filter(|metadata| {
329 metadata.name().starts_with(base_name)
330 || metadata
331 .aliases()
332 .iter()
333 .any(|alias| alias.starts_with(base_name))
334 })
335 .collect();
336 matches.dedup_by_key(|metadata| metadata.name());
337 match matches.as_slice() {
338 [metadata] => Ok(OptionQuery {
339 name: metadata.name().to_owned(),
340 metadata: Some(metadata),
341 index,
342 }),
343 [] => Err(OptionLookupError::InvalidName(base_name.to_owned())),
344 _ => Err(OptionLookupError::AmbiguousPrefix(base_name.to_owned())),
345 }
346}
347
348pub(crate) fn resolve_exact_option_name(name: &str) -> Result<OptionQuery, RmuxError> {
350 let (base_name, index) = split_array_index(name)?;
351 let lookup = option_lookup();
352
353 if base_name.starts_with('@') {
354 if index.is_some() {
355 return Err(RmuxError::Server(format!(
356 "user option does not support array indexes: {name}"
357 )));
358 }
359 return Ok(OptionQuery {
360 name: base_name.to_owned(),
361 metadata: None,
362 index: None,
363 });
364 }
365
366 if let Some(metadata) = lookup.by_name.get(base_name).copied() {
367 return Ok(OptionQuery {
368 name: metadata.name().to_owned(),
369 metadata: Some(metadata),
370 index,
371 });
372 }
373
374 if let Some(metadata) = lookup.by_alias.get(base_name).copied() {
375 return Ok(OptionQuery {
376 name: metadata.name().to_owned(),
377 metadata: Some(metadata),
378 index,
379 });
380 }
381
382 Err(RmuxError::Server(format!("invalid option: {base_name}")))
383}
384
385#[must_use]
387pub fn option_name_by_name(name: &str) -> Option<OptionName> {
388 resolve_option_name(name).ok()?.known_option()
389}
390
391#[must_use]
393pub fn option_affects_rendering(option: OptionName) -> bool {
394 let effects = option_metadata(option).effects();
395 effects.contains(EFFECT_RENDER)
396 || effects.contains(EFFECT_PANE_STYLE)
397 || effects.contains(EFFECT_STYLE_PARSE)
398 || effects.contains(EFFECT_CODEPOINT_WIDTHS)
399 || effects.contains(EFFECT_CURSOR)
400 || effects.contains(EFFECT_FILL_CHARACTER)
401 || effects.contains(EFFECT_STATUS_TIMER)
402 || effects.contains(EFFECT_LAYOUT)
403 || effects.contains(EFFECT_SCROLLBAR_STYLE)
404 || effects.contains(EFFECT_PANE_COLOURS)
405}
406
407#[must_use]
409pub fn option_affects_alerts(option: OptionName) -> bool {
410 option_metadata(option).effects().affects_alerts()
411}
412
413fn option_lookup() -> &'static OptionLookup {
414 OPTION_LOOKUP.get_or_init(|| {
415 let mut by_option = HashMap::with_capacity(OPTIONS.len());
416 let mut by_name = HashMap::with_capacity(OPTIONS.len());
417 let alias_count = OPTIONS
418 .iter()
419 .map(|metadata| metadata.aliases().len())
420 .sum();
421 let mut by_alias = HashMap::with_capacity(alias_count);
422
423 for metadata in OPTIONS {
424 by_option.insert(metadata.option(), metadata);
425 by_name.insert(metadata.name(), metadata);
426 for alias in metadata.aliases() {
427 by_alias.insert(*alias, metadata);
428 }
429 }
430
431 OptionLookup {
432 by_option,
433 by_name,
434 by_alias,
435 }
436 })
437}
438
439fn split_array_index(name: &str) -> Result<(&str, Option<u32>), RmuxError> {
440 split_array_index_typed(name).map_err(OptionLookupError::into_rmux_error)
441}
442
443fn split_array_index_typed(name: &str) -> Result<(&str, Option<u32>), OptionLookupError> {
444 let Some(start) = name.rfind('[') else {
445 if name.contains(']') {
446 return Err(OptionLookupError::InvalidIndexSyntax(name.to_owned()));
447 }
448 return Ok((name, None));
449 };
450
451 if !name.ends_with(']') {
452 return Err(OptionLookupError::InvalidIndexSyntax(name.to_owned()));
453 }
454
455 let base_name = &name[..start];
456 let index_text = &name[start + 1..name.len() - 1];
457 if base_name.is_empty() || index_text.is_empty() {
458 return Err(OptionLookupError::InvalidIndexSyntax(name.to_owned()));
459 }
460
461 let index = index_text
462 .parse::<u32>()
463 .map_err(|_| OptionLookupError::InvalidIndexSyntax(name.to_owned()))?;
464 Ok((base_name, Some(index)))
465}