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_log::test]
219 #[test_log(default_log_filter = "trace")]
220 fn test_parse_ini() {
221 let content = r"[Section1]
222key1=value1
223key2=value2
224
225[Section2]
226foo=bar
227baz=qux
228
229; Comment
230# Another comment
231[Empty Section]
232
233[Section With Spaces]
234key with spaces=value with spaces; and more values
235";
236
237 let ini = IniFile::from_str(content);
238
239 assert_eq!(
240 ini.get_section("Section1")
241 .expect("section missing")
242 .get_first("key1"),
243 Some("value1")
244 );
245 assert_eq!(
246 ini.get_section("Section2")
247 .expect("section missing")
248 .get_first("foo"),
249 Some("bar")
250 );
251
252 assert!(ini.get_section("Empty Section").is_some());
253 assert_ne!(
254 ini.get_section("Section With Spaces")
255 .expect("section missing")
256 .get_all("key with spaces"),
257 Some(&vec!["value with spaces"])
258 );
259 assert_ne!(
260 ini.get_section("Section With Spaces")
261 .expect("section missing")
262 .get_all("key with spaces"),
263 Some(&vec!["value with spaces"])
264 );
265 assert_eq!(
266 ini.get_section("Section With Spaces")
267 .expect("section missing")
268 .get_all("key with spaces"),
269 Some(&vec!["value with spaces", "and more values"])
270 );
271
272 assert!(ini.get_section("NonExistent").is_none());
273 assert_eq!(
274 ini.get_section("Section1")
275 .expect("section missing")
276 .get_first("nonexistent"),
277 None
278 );
279 }
280
281 #[test_log::test]
282 #[test_log(default_log_filter = "trace")]
283 fn test_empty_ini() {
284 let content = "";
285 let ini = IniFile::from_str(content);
286 assert_eq!(ini.sections().len(), 1);
287 }
288
289 #[test_log::test]
290 #[test_log(default_log_filter = "trace")]
291 fn test_no_sections() {
292 let content = "key=value";
293 let ini = IniFile::from_str(content);
294 assert_eq!(
295 ini.get_section("")
296 .expect("section missing")
297 .get_first("key"),
298 Some("value")
299 );
300 }
301
302 #[test_log::test]
303 #[test_log(default_log_filter = "trace")]
304 fn test_values_iterator() {
305 let content = r"
306 [Section1]
307 key1=value1
308 key2=value2;values3
309
310 [Section2]
311 foo=bar
312 ";
313 let ini = IniFile::from_str(content);
314 let mut values: Vec<_> = ini.into_iter().collect();
315 values.sort_by_key(|&(section, key, _)| (section, key));
316 let mut iter = values.iter();
317 assert_eq!(iter.next(), Some(&("Section1", "key1", &vec!["value1"])));
318 assert_eq!(
319 iter.next(),
320 Some(&("Section1", "key2", &vec!["value2", "values3"]))
321 );
322 assert_eq!(iter.next(), Some(&("Section2", "foo", &vec!["bar"])));
323 assert_eq!(values.len(), 3);
324 }
325
326 #[test_log::test]
327 #[test_log(default_log_filter = "trace")]
328 fn test_values_iterator_2() {
329 let content = r"
330 [Section1]
331 key1=value1
332 key2=value2
333
334 [Section2]
335 foo=bar
336 ";
337 let ini = IniFile::from_str(content);
338 let mut count = 0;
339 for (section, name, value) in &ini {
340 assert!(!section.is_empty(), "Item should not be empty");
341 assert!(!name.is_empty(), "Item should not be empty");
342 assert!(!value.is_empty(), "Item should not be empty");
343 count += 1;
344 }
345 assert_eq!(count, 3, "There should be 3 items in the iterator");
346 }
347
348 #[test_log::test]
349 #[test_log(default_log_filter = "trace")]
350 fn test_format_empty() {
351 let content = "test=test";
352 let ini = IniFile::from_str(content);
353 assert_eq!(ini.format(), "test=test\n");
354 }
355
356 #[test_log::test]
357 #[test_log(default_log_filter = "trace")]
358 fn test_format_multiple_sections() {
359 let content = r"[B]
360key1=value1
361key2=value2;value3
362
363[A]
364foo=bar
365";
366 let content2 = r"[A]
367foo=bar
368
369[B]
370key1=value1
371key2=value2;value3
372";
373 let ini = IniFile::from_str(content);
374 assert_eq!(ini.format(), content2);
375 }
376}