hyprshell_core_lib/
ini.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4#[derive(Debug, Default)]
5pub struct IniFile<'a> {
6    sections: HashMap<&'a str, Section<'a>>,
7}
8
9#[derive(Debug, Default)]
10pub struct Section<'a> {
11    entries: HashMap<&'a str, &'a str>,
12}
13
14impl<'a> Section<'a> {
15    pub fn insert(&mut self, key: &'a str, value: &'a str) {
16        self.entries.insert(key, value);
17    }
18
19    pub fn get(&self, key: &str) -> Option<&'a str> {
20        self.entries.get(key).copied()
21    }
22
23    pub fn get_boxed(&self, key: &str) -> Option<Box<str>> {
24        self.get(key).map(Box::from)
25    }
26
27    pub fn get_path_boxed(&self, key: &str) -> Option<Box<Path>> {
28        self.get(key).map(Path::new).map(Box::from)
29    }
30
31    pub fn get_boolean(&self, key: &str) -> Option<bool> {
32        self.get(key).map(|s| s == "true")
33    }
34
35    pub fn values(&'a self) -> impl Iterator<Item = &'a str> {
36        self.entries.values().copied()
37    }
38}
39
40impl<'a> IniFile<'a> {
41    pub fn parse(content: &'a str) -> Self {
42        let mut sections = HashMap::new();
43        let mut current_section = sections.entry("").or_insert_with(Section::default);
44
45        for line in content.lines() {
46            let line = line.trim();
47            if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
48                continue;
49            }
50
51            if line.starts_with('[') && line.ends_with(']') {
52                let current_section_name = &line[1..line.len() - 1];
53                current_section = sections
54                    .entry(current_section_name.trim())
55                    .or_insert_with(Section::default);
56                continue;
57            }
58
59            if let Some((key, value)) = line.split_once('=') {
60                let key = key.trim();
61                let value = value.trim();
62
63                // Skip localized entries (containing [...])
64                if key.contains('[') {
65                    continue;
66                }
67                current_section.insert(key, value);
68            }
69        }
70
71        Self { sections }
72    }
73
74    pub fn get_section(&self, section: &str) -> Option<&Section> {
75        self.sections.get(section)
76    }
77
78    pub fn get_value(&self, section: &str, key: &str) -> Option<&'a str> {
79        self.sections.get(section).and_then(|s| s.get(key))
80    }
81
82    pub fn sections(&self) -> &HashMap<&'a str, Section> {
83        &self.sections
84    }
85
86    pub fn values(&'a self) -> impl Iterator<Item = &'a str> + 'a {
87        self.sections.values().flat_map(|section| section.values())
88    }
89}
90
91impl<'a> IntoIterator for &'a IniFile<'a> {
92    type Item = &'a str;
93    type IntoIter = Box<dyn Iterator<Item = &'a str> + 'a>;
94
95    fn into_iter(self) -> Self::IntoIter {
96        Box::new(self.values())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_parse_ini() {
106        let content = r#"[Section1]
107key1=value1
108key2=value2
109
110[Section2]
111foo=bar
112baz=qux
113
114; Comment
115# Another comment
116[Empty Section]
117
118[Section With Spaces]
119key with spaces=value with spaces
120"#;
121
122        let ini = IniFile::parse(content);
123
124        // Test section content
125        assert_eq!(ini.get_value("Section1", "key1"), Some("value1"));
126        assert_eq!(ini.get_value("Section2", "foo"), Some("bar"));
127
128        // Test section existence
129        assert!(ini.get_section("Empty Section").is_some());
130
131        // Test spaces in keys and values
132        assert_eq!(
133            ini.get_value("Section With Spaces", "key with spaces"),
134            Some("value with spaces")
135        );
136
137        // Test non-existent sections and keys
138        assert_eq!(ini.get_value("NonExistent", "key"), None);
139        assert_eq!(ini.get_value("Section1", "nonexistent"), None);
140    }
141
142    #[test]
143    fn test_empty_ini() {
144        let content = "";
145        let ini = IniFile::parse(content);
146        assert_eq!(ini.sections().len(), 1);
147    }
148
149    #[test]
150    fn test_no_sections() {
151        let content = "key=value";
152        let ini = IniFile::parse(content);
153        assert_eq!(ini.get_value("", "key"), Some("value"));
154    }
155
156    #[test]
157    fn test_values_iterator() {
158        let content = r#"
159    [Section1]
160    key1=value1
161    key2=value2
162
163    [Section2]
164    foo=bar
165    "#;
166        let ini = IniFile::parse(content);
167        let values: Vec<_> = ini.values().collect();
168        assert!(values.contains(&"value1"));
169        assert!(values.contains(&"value2"));
170        assert!(values.contains(&"bar"));
171        assert_eq!(values.len(), 3);
172    }
173
174    #[test]
175    fn test_values_iterator_2() {
176        let content = r#"
177    [Section1]
178    key1=value1
179    key2=value2
180
181    [Section2]
182    foo=bar
183    "#;
184        let ini = IniFile::parse(content);
185        let mut count = 0;
186        for item in &ini {
187            assert!(!item.is_empty(), "Item should not be empty");
188            count += 1;
189        }
190        assert_eq!(count, 3, "There should be 3 items in the iterator");
191    }
192}