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_core::style::Color;
10use std::borrow::Cow;
11use std::{array, 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    if pal.generator.starts_with("light-dark") {
26        for c in Colors::array() {
27            writeln!(
28                buf,
29                "{}={}, {}",
30                *c, pal.color[*c as usize][0], pal.color[*c as usize][3]
31            )?;
32        }
33    } else if pal.generator.starts_with("color-1") {
34        for c in Colors::array() {
35            writeln!(buf, "{}={}", *c, pal.color[*c as usize][0])?;
36        }
37    } else if pal.generator.starts_with("color-2") {
38        for c in Colors::array() {
39            writeln!(
40                buf,
41                "{}={}, {}",
42                *c, pal.color[*c as usize][0], pal.color[*c as usize][4]
43            )?;
44        }
45    } else if pal.generator.starts_with("color-4") {
46        for c in Colors::array() {
47            writeln!(
48                buf,
49                "{}={}, {}, {}, {}",
50                *c,
51                pal.color[*c as usize][0],
52                pal.color[*c as usize][1],
53                pal.color[*c as usize][2],
54                pal.color[*c as usize][3]
55            )?;
56        }
57    } else if pal.generator.starts_with("color-4-dark") {
58        for c in Colors::array() {
59            writeln!(
60                buf,
61                "{}={}, {}, {}, {}",
62                *c,
63                pal.color[*c as usize][0],
64                pal.color[*c as usize][1],
65                pal.color[*c as usize][2],
66                pal.color[*c as usize][3]
67            )?;
68        }
69    } else if pal.generator.starts_with("color-8") {
70        for c in Colors::array() {
71            writeln!(
72                buf,
73                "{}={}, {}, {}, {}, {}, {}, {}, {}",
74                *c,
75                pal.color[*c as usize][0],
76                pal.color[*c as usize][1],
77                pal.color[*c as usize][2],
78                pal.color[*c as usize][3],
79                pal.color[*c as usize][4],
80                pal.color[*c as usize][5],
81                pal.color[*c as usize][6],
82                pal.color[*c as usize][7],
83            )?;
84        }
85    } else {
86        return Err(io::Error::other(LoadPaletteErr(format!(
87            "Invalid generator format {:?}",
88            pal.generator
89        ))));
90    }
91    writeln!(buf,)?;
92    writeln!(buf, "[reference]")?;
93    for (r, i) in pal.aliased.as_ref() {
94        writeln!(buf, "{}={}", r, i)?;
95    }
96    Ok(())
97}
98
99/// Load a .pal file as a Palette.
100pub fn load_palette(mut r: impl io::Read) -> Result<Palette, io::Error> {
101    let mut buf = String::new();
102    r.read_to_string(&mut buf)?;
103
104    enum S {
105        Start,
106        Theme,
107        Palette,
108        Color,
109        Reference,
110        Fail(String),
111    }
112
113    let mut pal = Palette::default();
114    let mut dark = 63u8;
115
116    let mut state = S::Start;
117    'm: for l in buf.lines() {
118        let l = l.trim();
119        match state {
120            S::Start => {
121                if l == "[theme]" {
122                    state = S::Theme;
123                } else if l == "[palette]" {
124                    state = S::Palette;
125                } else {
126                    state = S::Fail("No a valid pal-file".to_string());
127                    break 'm;
128                }
129            }
130            S::Theme => {
131                if l == "[palette]" {
132                    state = S::Palette;
133                } else if l.is_empty() || l.starts_with("#") {
134                    // ok
135                } else if l.starts_with("name") {
136                    if let Some(s) = l.split('=').nth(1) {
137                        pal.theme_name = Cow::Owned(s.trim().to_string());
138                    }
139                } else if l.starts_with("theme") {
140                    if let Some(s) = l.split('=').nth(1) {
141                        pal.theme = Cow::Owned(s.trim().to_string());
142                    }
143                } else {
144                    state = S::Fail(format!("Invalid theme property {:?}", l));
145                    break 'm;
146                }
147            }
148            S::Palette => {
149                if l == "[color]" {
150                    state = S::Color;
151                } else if l.is_empty() || l.starts_with("#") {
152                    // ok
153                } else if l.starts_with("name") {
154                    if let Some(s) = l.split('=').nth(1) {
155                        pal.name = Cow::Owned(s.trim().to_string());
156                    }
157                } else if l.starts_with("docs") {
158                    if let Some(s) = l.split('=').nth(1) {
159                        let doc = s.trim().replace("\\n", "\n");
160                        pal.doc = Cow::Owned(doc);
161                    }
162                } else if l.starts_with("generator") {
163                    if let Some(s) = l.split('=').nth(1) {
164                        pal.generator = Cow::Owned(s.trim().to_string());
165                        if s.starts_with("light-dark") {
166                            if let Some(s) = l.split(':').nth(1) {
167                                dark = s.trim().parse::<u8>().unwrap_or(63);
168                            }
169                        } else if s.starts_with("color-1") {
170                        } else if s.starts_with("color-2") {
171                        } else if s.starts_with("color-4") {
172                        } else if s.starts_with("color-4-dark") {
173                            if let Some(s) = l.split(':').nth(1) {
174                                dark = s.trim().parse::<u8>().unwrap_or(63);
175                            }
176                        } else if s.starts_with("color-8") {
177                        } else {
178                            state = S::Fail(format!("Unknown generator format {:?}", s));
179                            break 'm;
180                        }
181                    }
182                } else if l.starts_with("dark") {
183                    if let Some(s) = l.split('=').nth(1) {
184                        if let Ok(v) = s.trim().parse::<u8>() {
185                            dark = v;
186                        } else {
187                            // skip
188                        }
189                    }
190                } else {
191                    state = S::Fail(format!("Invalid palette property {:?}", l));
192                    break 'm;
193                }
194            }
195            S::Color => {
196                if l == "[reference]" {
197                    state = S::Reference;
198                } else if l.is_empty() || l.starts_with("#") {
199                    // ok
200                } else {
201                    let mut kv = l.split('=');
202                    let cn = if let Some(v) = kv.next() {
203                        let Ok(c) = v.trim().parse::<Colors>() else {
204                            state = S::Fail(format!("Invalid property format {:?}", l));
205                            break 'm;
206                        };
207                        c
208                    } else {
209                        state = S::Fail(format!("Invalid property format {:?}", l));
210                        break 'm;
211                    };
212                    if let Some(v) = kv.next() {
213                        if pal.generator.starts_with("light-dark") {
214                            let color = split_comma::<2>(v)?;
215                            if cn == Colors::TextLight || cn == Colors::TextDark {
216                                pal.color[cn as usize] = Palette::interpolatec2(
217                                    color[0],
218                                    color[1],
219                                    Color::default(),
220                                    Color::default(),
221                                )
222                            } else {
223                                pal.color[cn as usize] =
224                                    Palette::interpolatec(color[0], color[1], dark);
225                            }
226                        } else if pal.generator.starts_with("color-1") {
227                            let color = split_comma::<1>(v)?;
228                            pal.color[cn as usize] = array::from_fn(|_| color[0]);
229                        } else if pal.generator.starts_with("color-2") {
230                            let color = split_comma::<2>(v)?;
231                            pal.color[cn as usize][0..=3]
232                                .copy_from_slice(&array::from_fn::<_, 4, _>(|_| color[0]));
233                            pal.color[cn as usize][4..=7]
234                                .copy_from_slice(&array::from_fn::<_, 4, _>(|_| color[0]));
235                        } else if pal.generator.starts_with("color-4") {
236                            let color = split_comma::<4>(v)?;
237                            pal.color[cn as usize][0..=3].copy_from_slice(&color);
238                            pal.color[cn as usize][4..=7].copy_from_slice(&color);
239                        } else if pal.generator.starts_with("color-4-dark") {
240                            let color = split_comma::<4>(v)?;
241                            pal.color[cn as usize][0..=3].copy_from_slice(&color);
242                            pal.color[cn as usize][4] =
243                                Palette::scale_color_to(pal.color[cn as usize][0], dark);
244                            pal.color[cn as usize][5] =
245                                Palette::scale_color_to(pal.color[cn as usize][1], dark);
246                            pal.color[cn as usize][6] =
247                                Palette::scale_color_to(pal.color[cn as usize][2], dark);
248                            pal.color[cn as usize][7] =
249                                Palette::scale_color_to(pal.color[cn as usize][3], dark);
250                        } else if pal.generator.starts_with("color-8") {
251                            let color = split_comma::<8>(v)?;
252                            pal.color[cn as usize] = color;
253                        }
254                    } else {
255                        state = S::Fail(format!("Invalid property format {:?}", l));
256                        break 'm;
257                    };
258                }
259            }
260            S::Reference => {
261                let mut kv = l.split('=');
262                let rn = if let Some(v) = kv.next() {
263                    v
264                } else {
265                    state = S::Fail(format!("Invalid property format {:?}", l));
266                    break 'm;
267                };
268                let ci = if let Some(v) = kv.next() {
269                    if let Ok(ci) = v.parse::<palette::ColorIdx>() {
270                        ci
271                    } else {
272                        state = S::Fail(format!("Invalid color reference {:?}", l));
273                        break 'm;
274                    }
275                } else {
276                    state = S::Fail(format!("Invalid property format {:?}", l));
277                    break 'm;
278                };
279                pal.add_aliased(rn, ci);
280            }
281            S::Fail(_) => {
282                unreachable!()
283            }
284        }
285    }
286
287    match state {
288        S::Fail(n) => Err(io::Error::other(LoadPaletteErr(n))),
289        S::Start => Err(io::Error::other(LoadPaletteErr(
290            "Missing [theme]. Invalid format or truncated.".to_string(),
291        ))),
292        S::Theme => Err(io::Error::other(LoadPaletteErr(
293            "Missing [palette]. Invalid format or truncated.".to_string(),
294        ))),
295        S::Palette => Err(io::Error::other(LoadPaletteErr(
296            "Missing [reference]. Invalid format or truncated.".to_string(),
297        ))),
298        S::Color | S::Reference => Ok(pal),
299    }
300}
301
302fn split_comma<const N: usize>(s: &str) -> Result<[Color; N], io::Error> {
303    let mut r: [Color; N] = array::from_fn(|_| Color::default());
304    let mut vv = s.split(',');
305    for i in 0..N {
306        r[i] = if let Some(v) = vv.next() {
307            let Ok(v) = v.trim().parse::<Color>() else {
308                return Err(io::Error::other(LoadPaletteErr(
309                    format!("Invalid color[{}] {:?}", i, s).to_string(),
310                )));
311            };
312            v
313        } else {
314            return Err(io::Error::other(LoadPaletteErr(
315                format!("Invalid color[{}] {:?}", i, s).to_string(),
316            )));
317        }
318    }
319
320    if let Some(v) = vv.next()
321        && !v.trim().is_empty()
322    {
323        return Err(io::Error::other(LoadPaletteErr(
324            format!("Too many colors (max {}) {:?}", N, s).to_string(),
325        )));
326    }
327
328    Ok(r)
329}