Skip to main content

rmux_core/input/
params.rs

1//! Parameter splitting and access matching tmux `input_split` / `input_get`.
2
3use super::PARAM_LIST_MAX;
4
5/// Parameter type discriminant.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ParamType {
8    /// No value present (empty between semicolons).
9    Missing,
10    /// Numeric value.
11    Number(i32),
12    /// Colon-containing string (for ISO SGR forms like `38:2:r:g:b`).
13    Str(String),
14}
15
16/// A single parsed parameter.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct InputParam {
19    /// The parameter type and value.
20    pub ptype: ParamType,
21}
22
23impl InputParam {
24    fn missing() -> Self {
25        Self {
26            ptype: ParamType::Missing,
27        }
28    }
29}
30
31/// Parsed parameter list.
32pub(crate) struct ParamList {
33    params: [Option<InputParam>; PARAM_LIST_MAX],
34    len: u32,
35}
36
37impl ParamList {
38    pub(crate) fn new() -> Self {
39        Self {
40            params: std::array::from_fn(|_| None),
41            len: 0,
42        }
43    }
44
45    pub(crate) fn len(&self) -> u32 {
46        self.len
47    }
48
49    pub(crate) fn clear(&mut self) {
50        for p in &mut self.params {
51            *p = None;
52        }
53        self.len = 0;
54    }
55
56    /// Split a raw parameter buffer (semicolon-delimited) into the param list.
57    /// Returns `true` on success, `false` on parse error.
58    pub(crate) fn split(&mut self, buf: &[u8], len: usize) -> bool {
59        self.clear();
60
61        if len == 0 {
62            return true;
63        }
64
65        let s = match std::str::from_utf8(&buf[..len]) {
66            Ok(s) => s,
67            Err(_) => return false,
68        };
69
70        for part in s.split(';') {
71            if self.len as usize >= PARAM_LIST_MAX {
72                return false;
73            }
74            let param = if part.is_empty() {
75                InputParam::missing()
76            } else if part.contains(':') {
77                InputParam {
78                    ptype: ParamType::Str(part.to_owned()),
79                }
80            } else {
81                match part.parse::<i32>() {
82                    Ok(n) if n >= 0 => InputParam {
83                        ptype: ParamType::Number(n),
84                    },
85                    _ => return false,
86                }
87            };
88            self.params[self.len as usize] = Some(param);
89            self.len += 1;
90        }
91
92        true
93    }
94
95    /// Get parameter at `index` with clamping semantics matching tmux `input_get`.
96    ///
97    /// - If `index` is out of range: returns `defval`.
98    /// - If parameter is Missing: returns `defval`.
99    /// - If parameter is Str: returns `-1`.
100    /// - If parameter is Number: returns `max(value, minval)`.
101    pub(crate) fn get(&self, index: u32, minval: i32, defval: i32) -> i32 {
102        if index >= self.len {
103            return defval;
104        }
105        match &self.params[index as usize] {
106            None => defval,
107            Some(p) => match &p.ptype {
108                ParamType::Missing => defval,
109                ParamType::Str(_) => -1,
110                ParamType::Number(n) => {
111                    if *n < minval {
112                        minval
113                    } else {
114                        *n
115                    }
116                }
117            },
118        }
119    }
120
121    /// Returns the param type at the given index, if any.
122    pub(crate) fn param_at(&self, index: u32) -> Option<&InputParam> {
123        if index >= self.len {
124            return None;
125        }
126        self.params[index as usize].as_ref()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn split_empty() {
136        let mut pl = ParamList::new();
137        assert!(pl.split(b"", 0));
138        assert_eq!(pl.len(), 0);
139        assert_eq!(pl.get(0, 0, 42), 42);
140    }
141
142    #[test]
143    fn split_single_number() {
144        let mut pl = ParamList::new();
145        assert!(pl.split(b"5", 1));
146        assert_eq!(pl.len(), 1);
147        assert_eq!(pl.get(0, 0, 0), 5);
148    }
149
150    #[test]
151    fn split_multiple_with_missing() {
152        let mut pl = ParamList::new();
153        let buf = b";3;;7";
154        assert!(pl.split(buf, buf.len()));
155        assert_eq!(pl.len(), 4);
156        assert_eq!(pl.get(0, 0, 99), 99); // missing
157        assert_eq!(pl.get(1, 0, 0), 3);
158        assert_eq!(pl.get(2, 0, 99), 99); // missing
159        assert_eq!(pl.get(3, 0, 0), 7);
160    }
161
162    #[test]
163    fn split_colon_string() {
164        let mut pl = ParamList::new();
165        let buf = b"38:2:255:0:128";
166        assert!(pl.split(buf, buf.len()));
167        assert_eq!(pl.len(), 1);
168        assert_eq!(pl.get(0, 0, 0), -1); // string returns -1
169    }
170
171    #[test]
172    fn get_clamps_to_minval() {
173        let mut pl = ParamList::new();
174        assert!(pl.split(b"0", 1));
175        assert_eq!(pl.get(0, 1, 1), 1);
176    }
177
178    #[test]
179    fn get_out_of_range_returns_defval() {
180        let mut pl = ParamList::new();
181        assert!(pl.split(b"5", 1));
182        assert_eq!(pl.get(5, 0, 42), 42);
183    }
184}