rmux_core/options/
registry.rs1use 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 #[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
228pub(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
240pub 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 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#[must_use]
301pub fn option_name_by_name(name: &str) -> Option<OptionName> {
302 resolve_option_name(name).ok()?.known_option()
303}
304
305#[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#[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}