hyprshell_core_lib/
ini_owned.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 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 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); 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}