rat_theme4/
pal_io.rs

1//!
2//! Allows load/store to an ini-style format.
3//! Serde for Palette is supported as well.
4//!
5
6use crate::error::LoadPaletteErr;
7use crate::palette;
8use crate::palette::{Colors, Palette};
9use ratatui::style::Color;
10use std::borrow::Cow;
11use std::io;
12
13/// Stora a Palette as a .pal file.
14pub fn store_palette(pal: &Palette, mut buf: impl io::Write) -> Result<(), io::Error> {
15    writeln!(buf, "[theme]")?;
16    writeln!(buf, "name={}", pal.theme_name)?;
17    writeln!(buf, "theme={}", pal.theme)?;
18    writeln!(buf)?;
19    writeln!(buf, "[palette]")?;
20    writeln!(buf, "name={}", pal.name)?;
21    writeln!(buf, "docs={}", pal.doc.replace('\n', "\\n"))?;
22    writeln!(buf, "generator={}", pal.generator)?;
23    writeln!(buf,)?;
24    writeln!(buf, "[color]")?;
25    for c in Colors::array() {
26        writeln!(
27            buf,
28            "{}={}, {}",
29            *c, pal.color[*c as usize][0], pal.color[*c as usize][3]
30        )?;
31    }
32    writeln!(buf,)?;
33    writeln!(buf, "[reference]")?;
34    for (r, i) in pal.aliased.as_ref() {
35        writeln!(buf, "{}={}", r, i)?;
36    }
37    Ok(())
38}
39
40/// Load a .pal file as a Palette.
41pub fn load_palette(mut r: impl io::Read) -> Result<Palette, io::Error> {
42    let mut buf = String::new();
43    r.read_to_string(&mut buf)?;
44
45    enum S {
46        Start,
47        Theme,
48        Palette,
49        Color,
50        Reference,
51        Fail(u8),
52    }
53
54    let mut pal = Palette::default();
55    let mut dark = 63u8;
56
57    let mut state = S::Start;
58    'm: for l in buf.lines() {
59        let l = l.trim();
60        match state {
61            S::Start => {
62                if l == "[theme]" {
63                    state = S::Theme;
64                } else if l == "[palette]" {
65                    state = S::Palette;
66                } else {
67                    state = S::Fail(1);
68                    break 'm;
69                }
70            }
71            S::Theme => {
72                if l == "[palette]" {
73                    state = S::Palette;
74                } else if l.is_empty() || l.starts_with("#") {
75                    // ok
76                } else if l.starts_with("name") {
77                    if let Some(s) = l.split('=').nth(1) {
78                        pal.theme_name = Cow::Owned(s.trim().to_string());
79                    }
80                } else if l.starts_with("theme") {
81                    if let Some(s) = l.split('=').nth(1) {
82                        pal.theme = Cow::Owned(s.trim().to_string());
83                    }
84                } else {
85                    state = S::Fail(2);
86                    break 'm;
87                }
88            }
89            S::Palette => {
90                if l == "[color]" {
91                    state = S::Color;
92                } else if l.is_empty() || l.starts_with("#") {
93                    // ok
94                } else if l.starts_with("name") {
95                    if let Some(s) = l.split('=').nth(1) {
96                        pal.name = Cow::Owned(s.trim().to_string());
97                    }
98                } else if l.starts_with("docs") {
99                    if let Some(s) = l.split('=').nth(1) {
100                        let doc = s.trim().replace("\\n", "\n");
101                        pal.doc = Cow::Owned(doc);
102                    }
103                } else if l.starts_with("generator") {
104                    if let Some(s) = l.split('=').nth(1) {
105                        pal.generator = Cow::Owned(s.trim().to_string());
106                        if s.starts_with("light-dark") {
107                            if let Some(s) = l.split(':').nth(1) {
108                                dark = s.trim().parse::<u8>().unwrap_or(63);
109                            }
110                        }
111                    }
112                } else if l.starts_with("dark") {
113                    if let Some(s) = l.split('=').nth(1) {
114                        if let Ok(v) = s.trim().parse::<u8>() {
115                            dark = v;
116                        } else {
117                            // skip
118                        }
119                    }
120                } else {
121                    state = S::Fail(3);
122                    break 'm;
123                }
124            }
125            S::Color => {
126                if l == "[reference]" {
127                    state = S::Reference;
128                } else if l.is_empty() || l.starts_with("#") {
129                    // ok
130                } else {
131                    let mut kv = l.split('=');
132                    let cn = if let Some(v) = kv.next() {
133                        let Ok(c) = v.trim().parse::<Colors>() else {
134                            state = S::Fail(4);
135                            break 'm;
136                        };
137                        c
138                    } else {
139                        state = S::Fail(5);
140                        break 'm;
141                    };
142                    let (c0, c3) = if let Some(v) = kv.next() {
143                        let mut vv = v.split(',');
144                        let c0 = if let Some(v) = vv.next() {
145                            let Ok(v) = v.trim().parse::<Color>() else {
146                                state = S::Fail(6);
147                                break 'm;
148                            };
149                            v
150                        } else {
151                            state = S::Fail(7);
152                            break 'm;
153                        };
154                        let c3 = if let Some(v) = vv.next() {
155                            let Ok(v) = v.trim().parse::<Color>() else {
156                                state = S::Fail(8);
157                                break 'm;
158                            };
159                            v
160                        } else {
161                            state = S::Fail(9);
162                            break 'm;
163                        };
164                        (c0, c3)
165                    } else {
166                        state = S::Fail(10);
167                        break 'm;
168                    };
169
170                    if cn == Colors::TextLight || cn == Colors::TextDark {
171                        pal.color[cn as usize] =
172                            Palette::interpolatec2(c0, c3, Color::default(), Color::default())
173                    } else {
174                        pal.color[cn as usize] = Palette::interpolatec(c0, c3, dark);
175                    }
176                }
177            }
178            S::Reference => {
179                let mut kv = l.split('=');
180                let rn = if let Some(v) = kv.next() {
181                    v
182                } else {
183                    state = S::Fail(11);
184                    break 'm;
185                };
186                let ci = if let Some(v) = kv.next() {
187                    if let Ok(ci) = v.parse::<palette::ColorIdx>() {
188                        ci
189                    } else {
190                        state = S::Fail(12);
191                        break 'm;
192                    }
193                } else {
194                    state = S::Fail(13);
195                    break 'm;
196                };
197                pal.add_aliased(rn, ci);
198            }
199            S::Fail(_) => {
200                unreachable!()
201            }
202        }
203    }
204
205    match state {
206        S::Fail(n) => Err(io::Error::other(LoadPaletteErr(n))),
207        S::Start => Err(io::Error::other(LoadPaletteErr(100))),
208        S::Theme => Err(io::Error::other(LoadPaletteErr(101))),
209        S::Palette => Err(io::Error::other(LoadPaletteErr(102))),
210        S::Color | S::Reference => Ok(pal),
211    }
212}