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