hyprshell_core_lib/
ini_owned.rs

1use std::collections::HashMap;
2use std::collections::hash_map::Entry;
3use std::fmt::Write;
4use std::path::Path;
5use tracing::{debug_span, warn};
6
7#[derive(Debug, Default)]
8pub struct OwnedSection {
9    entries: HashMap<Box<str>, Vec<Box<str>>>,
10}
11
12impl OwnedSection {
13    #[must_use]
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    #[must_use]
19    pub fn get_all(&self, key: &str) -> Option<Vec<Box<str>>> {
20        self.entries.get(key).cloned()
21    }
22
23    #[must_use]
24    pub fn get_first(&self, key: &str) -> Option<Box<str>> {
25        self.entries.get(key)?.first().cloned()
26    }
27
28    #[must_use]
29    pub fn get_first_as_path(&self, key: &str) -> Option<Box<Path>> {
30        self.get_first(key).as_deref().map(Path::new).map(Box::from)
31    }
32
33    #[must_use]
34    pub fn get_first_as_boolean(&self, key: &str) -> Option<bool> {
35        self.get_first(key).map(|s| &*s == "true")
36    }
37}
38
39impl OwnedSection {
40    pub fn insert_item(&mut self, mime: Box<str>, desktop_file: Box<str>) {
41        self.entries.entry(mime).or_default().push(desktop_file);
42    }
43    pub fn insert_item_at_front(&mut self, mime: Box<str>, desktop_file: Box<str>) {
44        self.entries
45            .entry(mime)
46            .or_default()
47            .insert(0, desktop_file);
48    }
49    pub fn insert_items(&mut self, mime: Box<str>, mut desktop_files: Vec<Box<str>>) {
50        self.entries
51            .entry(mime)
52            .or_default()
53            .append(&mut desktop_files);
54    }
55}
56
57#[derive(Debug, Default)]
58pub struct IniFileOwned {
59    sections: HashMap<Box<str>, OwnedSection>,
60}
61
62impl IniFileOwned {
63    #[allow(clippy::should_implement_trait)]
64    pub fn from_str(content: &str) -> Self {
65        let _span = debug_span!("from_str").entered();
66
67        let mut sections = HashMap::new();
68        let mut current_section = sections
69            .entry(Box::from(""))
70            .or_insert_with(OwnedSection::default);
71
72        for line in content.lines() {
73            let line = line.trim();
74            if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
75                continue;
76            }
77
78            if line.starts_with('[') && line.ends_with(']') {
79                let current_section_name = &line[1..line.len() - 1];
80                current_section = sections
81                    .entry(Box::from(current_section_name.trim()))
82                    .or_insert_with(OwnedSection::default);
83                continue;
84            }
85
86            if let Some((key, value)) = line.split_once('=') {
87                let key = key.trim();
88                let value = value.trim();
89
90                // Skip localized entries (containing [...])
91                if key.contains('[') {
92                    continue;
93                }
94                let values = value
95                    .split(';')
96                    .filter(|s| !s.is_empty())
97                    .map(|s| Box::from(s.trim()))
98                    .collect::<Vec<_>>();
99                current_section.insert_items(Box::from(key), values);
100            } else {
101                warn!("malformed line: {line}");
102            }
103        }
104
105        Self { sections }
106    }
107}
108
109impl IniFileOwned {
110    #[must_use]
111    pub fn get_section(&self, section_name: &str) -> Option<&OwnedSection> {
112        self.sections.get(section_name)
113    }
114
115    #[must_use]
116    pub const fn sections(&self) -> &HashMap<Box<str>, OwnedSection> {
117        &self.sections
118    }
119
120    #[must_use]
121    pub fn format(&self) -> String {
122        let mut str = String::with_capacity(self.into_iter().count() * 20); // 20 chars per line should be good
123        let mut sections = self.sections().iter().collect::<Vec<_>>();
124        sections.sort_by_key(|&(name, _)| name);
125        for (name, section) in sections {
126            if !name.is_empty() {
127                if str.is_empty() {
128                    let _ = str.write_str(&format!("[{name}]\n"));
129                } else {
130                    let _ = str.write_str(&format!("\n[{name}]\n"));
131                }
132            }
133            let mut section = section.into_iter().collect::<Vec<_>>();
134            section.sort_by_key(|(key, _)| *key);
135            for (key, values) in section {
136                let _ = str.write_str(&format!("{key}={}\n", values.join(";")));
137            }
138        }
139        str
140    }
141}
142
143impl IniFileOwned {
144    pub fn get_section_mut(&mut self, section_name: &str) -> Option<&mut OwnedSection> {
145        self.sections.get_mut(section_name)
146    }
147
148    pub fn section_entry(&mut self, section_name: Box<str>) -> Entry<'_, Box<str>, OwnedSection> {
149        self.sections.entry(section_name)
150    }
151    pub fn insert_section(&mut self, name: Box<str>, section: OwnedSection) {
152        self.sections.insert(name, section);
153    }
154}
155
156impl<'a> IniFileOwned {
157    #[allow(dead_code)]
158    fn iter(&'a self) -> Box<dyn Iterator<Item = <&'a Self as IntoIterator>::Item> + 'a> {
159        <&Self as IntoIterator>::into_iter(self)
160    }
161}
162
163impl<'a> IntoIterator for &'a IniFileOwned {
164    type Item = (&'a Box<str>, &'a Box<str>, &'a Vec<Box<str>>);
165    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;
166
167    fn into_iter(self) -> Self::IntoIter {
168        let iter = self.sections.iter().flat_map(|(section_name, section)| {
169            section
170                .into_iter()
171                .map(move |(key, values)| (section_name, key, values))
172        });
173        Box::new(iter)
174    }
175}
176
177impl<'a> OwnedSection {
178    #[allow(dead_code)]
179    fn iter(&'a self) -> Box<dyn Iterator<Item = <&'a Self as IntoIterator>::Item> + 'a> {
180        <&Self as IntoIterator>::into_iter(self)
181    }
182}
183
184impl<'a> IntoIterator for &'a OwnedSection {
185    type Item = (&'a Box<str>, &'a Vec<Box<str>>);
186    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;
187
188    fn into_iter(self) -> Self::IntoIter {
189        let iter = self.entries.iter();
190        Box::new(iter)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test_log::test]
199    #[test_log(default_log_filter = "trace")]
200    fn test_parse_ini() {
201        let content = r"[Section1]
202key1=value1
203key2=value2
204
205[Section2]
206foo=bar
207baz=qux
208
209; Comment
210# Another comment
211[Empty Section]
212
213[Section With Spaces]
214key with spaces=value with spaces; and more values
215";
216
217        let ini = IniFileOwned::from_str(content);
218
219        assert_eq!(
220            ini.get_section("Section1")
221                .expect("section missing")
222                .get_first("key1"),
223            Some("value1".into())
224        );
225        assert_eq!(
226            ini.get_section("Section2")
227                .expect("section missing")
228                .get_first("foo"),
229            Some("bar".into())
230        );
231
232        assert!(ini.get_section("Empty Section").is_some());
233        assert_ne!(
234            ini.get_section("Section With Spaces")
235                .expect("section missing")
236                .get_all("key with spaces"),
237            Some(vec!["value with spaces".into()])
238        );
239        assert_ne!(
240            ini.get_section("Section With Spaces")
241                .expect("section missing")
242                .get_all("key with spaces"),
243            Some(vec!["value with spaces".into()])
244        );
245        assert_eq!(
246            ini.get_section("Section With Spaces")
247                .expect("section missing")
248                .get_all("key with spaces"),
249            Some(vec!["value with spaces".into(), "and more values".into()])
250        );
251
252        assert!(ini.get_section("NonExistent").is_none());
253        assert_eq!(
254            ini.get_section("Section1")
255                .expect("section missing")
256                .get_first("nonexistent"),
257            None
258        );
259    }
260
261    #[test_log::test]
262    #[test_log(default_log_filter = "trace")]
263    fn test_empty_ini() {
264        let content = "";
265        let ini = IniFileOwned::from_str(content);
266        assert_eq!(ini.sections().len(), 1);
267    }
268
269    #[test_log::test]
270    #[test_log(default_log_filter = "trace")]
271    fn test_no_sections() {
272        let content = "key=value";
273        let ini = IniFileOwned::from_str(content);
274        assert_eq!(
275            ini.get_section("")
276                .expect("section missing")
277                .get_first("key"),
278            Some("value".into())
279        );
280    }
281
282    #[test_log::test]
283    #[test_log(default_log_filter = "trace")]
284    fn test_values_iterator() {
285        let content = r"
286    [Section1]
287    key1=value1
288    key2=value2;values3
289
290    [Section2]
291    foo=bar
292    ";
293        let ini = IniFileOwned::from_str(content);
294        let mut values: Vec<_> = ini.into_iter().collect();
295        values.sort_by_key(|&(section, key, _)| (section, key));
296        let mut iter = values.iter();
297        assert_eq!(
298            iter.next(),
299            Some(&(&"Section1".into(), &"key1".into(), &vec!["value1".into()]))
300        );
301        assert_eq!(
302            iter.next(),
303            Some(&(
304                &"Section1".into(),
305                &"key2".into(),
306                &vec!["value2".into(), "values3".into()]
307            ))
308        );
309        assert_eq!(
310            iter.next(),
311            Some(&(&"Section2".into(), &"foo".into(), &vec!["bar".into()]))
312        );
313        assert_eq!(values.len(), 3);
314    }
315
316    #[test_log::test]
317    #[test_log(default_log_filter = "trace")]
318    fn test_values_iterator_2() {
319        let content = r"
320    [Section1]
321    key1=value1
322    key2=value2
323
324    [Section2]
325    foo=bar
326    ";
327        let ini = IniFileOwned::from_str(content);
328        let mut count = 0;
329        for (section, name, value) in &ini {
330            assert!(!section.is_empty(), "Item should not be empty");
331            assert!(!name.is_empty(), "Item should not be empty");
332            assert!(!value.is_empty(), "Item should not be empty");
333            count += 1;
334        }
335        assert_eq!(count, 3, "There should be 3 items in the iterator");
336    }
337
338    #[test_log::test]
339    #[test_log(default_log_filter = "trace")]
340    fn test_format_empty() {
341        let content = "test=test";
342        let ini = IniFileOwned::from_str(content);
343        assert_eq!(ini.format(), "test=test\n");
344    }
345
346    #[test_log::test]
347    #[test_log(default_log_filter = "trace")]
348    fn test_format_multiple_sections() {
349        let content = r"[B]
350key1=value1
351key2=value2;value3
352
353[A]
354foo=bar
355";
356        let content2 = r"[A]
357foo=bar
358
359[B]
360key1=value1
361key2=value2;value3
362";
363        let ini = IniFileOwned::from_str(content);
364        assert_eq!(ini.format(), content2);
365    }
366}