Skip to main content

sequoia_dock/
persist.rs

1use crate::model::{DockModel, DockNode, FloatingPanel, PanelId, SplitAxis};
2
3const TAG: &str = "dock-v1";
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum DecodeError {
8    Expected { what: &'static str, pos: usize },
9
10    Number { pos: usize },
11
12    Trailing { pos: usize },
13}
14
15impl std::fmt::Display for DecodeError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            DecodeError::Expected { what, pos } => {
19                write!(f, "dock: oczekiwano {what} @{pos}")
20            }
21            DecodeError::Number { pos } => write!(f, "dock: wrong number @{pos}"),
22            DecodeError::Trailing { pos } => write!(f, "dock: nadmiar danych @{pos}"),
23        }
24    }
25}
26
27impl std::error::Error for DecodeError {}
28
29pub fn encode(model: &DockModel) -> String {
30    let mut s = String::from(TAG);
31    s.push_str(" root=");
32    match &model.root {
33        None => s.push('-'),
34        Some(n) => encode_node(n, &mut s),
35    }
36    s.push_str(" float=");
37    for (i, f) in model.floating.iter().enumerate() {
38        if i > 0 {
39            s.push(';');
40        }
41        encode_float(f, &mut s);
42    }
43    s
44}
45
46pub fn decode(text: &str) -> Result<DockModel, DecodeError> {
47    let rest = text.strip_prefix(TAG).ok_or(DecodeError::Expected {
48        what: "tag `dock-v1`",
49        pos: 0,
50    })?;
51    let rest = rest.strip_prefix(' ').ok_or(DecodeError::Expected {
52        what: "spacja po tagu",
53        pos: TAG.len(),
54    })?;
55
56    let base = TAG.len() + 1;
57    let split_at = rest.find(" float=").ok_or(DecodeError::Expected {
58        what: "` float=`",
59        pos: base,
60    })?;
61    let (root_part, float_part) = (&rest[..split_at], &rest[split_at + " float=".len()..]);
62    let float_base = base + split_at + " float=".len();
63    let root_src = root_part
64        .strip_prefix("root=")
65        .ok_or(DecodeError::Expected {
66            what: "`root=`",
67            pos: base,
68        })?;
69    let root = if root_src == "-" {
70        None
71    } else {
72        let mut p = Parser {
73            src: root_src.as_bytes(),
74            pos: 0,
75            base: base + "root=".len(),
76        };
77        let n = parse_node(&mut p)?;
78        p.expect_end()?;
79        Some(n)
80    };
81    let mut floating = Vec::new();
82    if !float_part.is_empty() {
83        let mut off = float_base;
84        for item in float_part.split(';') {
85            floating.push(parse_float(item, off)?);
86            off += item.len() + 1;
87        }
88    }
89    let mut model = DockModel { root, floating };
90    model.sanitize();
91    Ok(model)
92}
93
94fn split_once<'a>(s: &'a str, pat: &str) -> Option<(&'a str, &'a str)> {
95    let i = s.find(pat)?;
96    Some((&s[..i], &s[i + pat.len()..]))
97}
98
99fn encode_node(n: &DockNode, s: &mut String) {
100    use std::fmt::Write as _;
101    match n {
102        DockNode::Leaf(p) => {
103            let _ = write!(s, "L{}", p.get());
104        }
105        DockNode::Tabs { tabs, active } => {
106            let _ = write!(s, "T[{active}]{{");
107            for (i, t) in tabs.iter().enumerate() {
108                if i > 0 {
109                    s.push(',');
110                }
111                let _ = write!(s, "{}", t.get());
112            }
113            s.push('}');
114        }
115        DockNode::Split {
116            axis,
117            frac,
118            first,
119            second,
120        } => {
121            let a = match axis {
122                SplitAxis::Horizontal => 'H',
123                SplitAxis::Vertical => 'V',
124            };
125            let _ = write!(s, "S({a},{frac:.4},");
126            encode_node(first, s);
127            s.push(',');
128            encode_node(second, s);
129            s.push(')');
130        }
131    }
132}
133
134fn encode_float(f: &FloatingPanel, s: &mut String) {
135    use std::fmt::Write as _;
136    let _ = write!(s, "F{}@{},{},{},{}", f.panel.get(), f.x, f.y, f.w, f.h);
137    if let Some(w) = f.window {
138        let _ = write!(s, "#{w}");
139    }
140}
141
142struct Parser<'a> {
143    src: &'a [u8],
144    pos: usize,
145
146    base: usize,
147}
148
149impl Parser<'_> {
150    fn abs(&self) -> usize {
151        self.base + self.pos
152    }
153
154    fn err<T>(&self, what: &'static str) -> Result<T, DecodeError> {
155        Err(DecodeError::Expected {
156            what,
157            pos: self.abs(),
158        })
159    }
160
161    fn peek(&self) -> Option<u8> {
162        self.src.get(self.pos).copied()
163    }
164
165    fn eat(&mut self, b: u8) -> bool {
166        if self.peek() == Some(b) {
167            self.pos += 1;
168            true
169        } else {
170            false
171        }
172    }
173
174    fn expect(&mut self, b: u8, what: &'static str) -> Result<(), DecodeError> {
175        if self.eat(b) { Ok(()) } else { self.err(what) }
176    }
177
178    fn expect_end(&self) -> Result<(), DecodeError> {
179        if self.pos == self.src.len() {
180            Ok(())
181        } else {
182            Err(DecodeError::Trailing { pos: self.abs() })
183        }
184    }
185
186    fn number(&mut self) -> Result<String, DecodeError> {
187        let start = self.pos;
188        let mut seen = false;
189        while let Some(c) = self.peek() {
190            if c.is_ascii_alphanumeric() || c == b'.' || c == b'-' || c == b'+' {
191                self.pos += 1;
192                seen = true;
193            } else {
194                break;
195            }
196        }
197        if seen {
198            Ok(String::from_utf8_lossy(&self.src[start..self.pos]).into_owned())
199        } else {
200            self.err("liczby")
201        }
202    }
203
204    fn u64_num(&mut self) -> Result<u64, DecodeError> {
205        let t = self.number()?;
206        t.parse::<u64>()
207            .map_err(|_| DecodeError::Number { pos: self.abs() })
208    }
209
210    fn f32_num(&mut self) -> Result<f32, DecodeError> {
211        let t = self.number()?;
212        t.parse::<f32>()
213            .map_err(|_| DecodeError::Number { pos: self.abs() })
214    }
215}
216
217fn parse_node(p: &mut Parser<'_>) -> Result<DockNode, DecodeError> {
218    match p.peek() {
219        Some(b'L') => {
220            p.pos += 1;
221            Ok(DockNode::Leaf(PanelId::new(p.u64_num()?)))
222        }
223        Some(b'T') => {
224            p.pos += 1;
225            p.expect(b'[', "`[`")?;
226            let active: usize = p
227                .number()?
228                .parse()
229                .map_err(|_| DecodeError::Number { pos: p.abs() })?;
230            p.expect(b']', "`]`")?;
231            p.expect(b'{', "`{`")?;
232            let mut tabs = Vec::new();
233
234            tabs.push(PanelId::new(p.u64_num()?));
235            while p.eat(b',') {
236                tabs.push(PanelId::new(p.u64_num()?));
237            }
238            p.expect(b'}', "`}`")?;
239            if tabs.is_empty() || active >= tabs.len() {
240                return p.err("active poza zakresem");
241            }
242            Ok(DockNode::Tabs { tabs, active })
243        }
244        Some(b'S') => {
245            p.pos += 1;
246            p.expect(b'(', "`(`")?;
247            let axis = match p.peek() {
248                Some(b'H') => {
249                    p.pos += 1;
250                    SplitAxis::Horizontal
251                }
252                Some(b'V') => {
253                    p.pos += 1;
254                    SplitAxis::Vertical
255                }
256                _ => return p.err("H|V axis"),
257            };
258            p.expect(b',', "`,`")?;
259            let frac = p.f32_num()?;
260            if !frac.is_finite() {
261                return p.err("frac must be finished");
262            }
263            p.expect(b',', "`,`")?;
264            let first = parse_node(p)?;
265            p.expect(b',', "`,`")?;
266            let second = parse_node(p)?;
267            p.expect(b')', "`)`")?;
268            Ok(DockNode::Split {
269                axis,
270                frac: DockNode::sanitized_frac(frac),
271                first: Box::new(first),
272                second: Box::new(second),
273            })
274        }
275        _ => p.err("L|T|S node"),
276    }
277}
278
279fn parse_float(item: &str, base: usize) -> Result<FloatingPanel, DecodeError> {
280    let body = item.strip_prefix('F').ok_or(DecodeError::Expected {
281        what: "`F{id}@x,y,w,h`",
282        pos: base,
283    })?;
284    let (id_part, rest) = split_once(body, "@").ok_or(DecodeError::Expected {
285        what: "`@`",
286        pos: base + 1,
287    })?;
288    let id_base = base + 1;
289    let id: u64 = id_part
290        .parse()
291        .map_err(|_| DecodeError::Number { pos: id_base })?;
292    let (geom, win) = match split_once(rest, "#") {
293        Some((g, w)) => (g, Some(w)),
294        None => (rest, None),
295    };
296    let geom_base = id_base + id_part.len() + 1;
297    let nums: Vec<&str> = geom.split(',').collect();
298    if nums.len() != 4 {
299        return Err(DecodeError::Expected {
300            what: "`x,y,w,h`",
301            pos: geom_base,
302        });
303    }
304    let parse = |s: &str| {
305        s.parse::<f32>()
306            .map_err(|_| DecodeError::Number { pos: geom_base })
307    };
308    let (x, y, w, h) = (
309        parse(nums[0])?,
310        parse(nums[1])?,
311        parse(nums[2])?,
312        parse(nums[3])?,
313    );
314    let window = match win {
315        Some(w) => Some(w.parse::<u64>().map_err(|_| DecodeError::Number {
316            pos: geom_base + geom.len() + 1,
317        })?),
318        None => None,
319    };
320    Ok(FloatingPanel {
321        panel: PanelId::new(id),
322        x,
323        y,
324        w,
325        h,
326        window,
327    })
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::drop::DockDrop;
334    use crate::model::DockModel;
335    use crate::ops::DockOp;
336
337    fn sample() -> DockModel {
338        let mut m = DockModel::single(PanelId::new(1));
339        m.apply(DockOp::Insert {
340            panel: PanelId::new(2),
341            target: PanelId::new(1),
342            drop: DockDrop::Right,
343            frac: 0.3,
344            moving: false,
345        });
346        m.apply(DockOp::Insert {
347            panel: PanelId::new(3),
348            target: PanelId::new(2),
349            drop: DockDrop::Tab,
350            frac: 0.5,
351            moving: false,
352        });
353        m.apply(DockOp::Float {
354            panel: PanelId::new(3),
355            x: 40.0,
356            y: 40.0,
357            w: 320.0,
358            h: 240.0,
359        });
360
361        m.apply(DockOp::Insert {
362            panel: PanelId::new(4),
363            target: PanelId::new(2),
364            drop: DockDrop::Tab,
365            frac: 0.5,
366            moving: false,
367        });
368        m
369    }
370
371    #[test]
372    fn roundtrip() {
373        let m = sample();
374        let s = encode(&m);
375        let back = decode(&s).expect("roundtrip");
376        assert_eq!(m, back, "{s}");
377    }
378
379    #[test]
380    fn empty_roundtrip() {
381        let m = DockModel::new();
382        assert_eq!(decode(&encode(&m)).unwrap(), m);
383    }
384
385    #[test]
386    fn bad_inputs_error_not_panic() {
387        assert!(decode("xxx").is_err());
388
389        assert!(decode("dock-v1 root=L1 float=").is_ok());
390        assert!(decode("dock-v1 root=L1").is_err());
391        assert!(decode("dock-v1 root=S(H,0.5,L1) float=").is_err());
392        assert!(decode("dock-v1 root=L1 float=F1@1,2").is_err());
393        assert!(decode("dock-v1 root=T[5]{1,2} float=").is_err());
394        assert!(decode("dock-v1 root=T[0]{1,2 float=").is_err());
395    }
396
397    #[test]
398    fn tabs_inside_split_with_sibling_roundtrip() {
399        let text = "dock-v1 root=S(H,0.72,T[0]{2,3},L5) float=";
400        let back = decode(text).expect("tabs + siblings");
401
402        let again = decode(&encode(&back)).expect("re-encode");
403        assert_eq!(back, again);
404        match back.root {
405            Some(DockNode::Split { .. }) => {}
406            other => panic!("oczekiwano splitu, jest {other:?}"),
407        }
408    }
409
410    #[test]
411    fn window_float_roundtrip() {
412        let mut m = DockModel::single(PanelId::new(1));
413        m.apply(DockOp::Float {
414            panel: PanelId::new(1),
415            x: 0.0,
416            y: 0.0,
417            w: 400.0,
418            h: 300.0,
419        });
420        m.attach_window(PanelId::new(1), Some(7));
421        let back = decode(&encode(&m)).unwrap();
422        assert_eq!(back.floating[0].window, Some(7));
423    }
424}