hyprshell-core-lib 4.9.5

A modern GTK4-based window switcher and application launcher for Hyprland
Documentation
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fmt::Write;
use std::path::Path;
use tracing::{debug_span, warn};

#[derive(Debug, Default)]
pub struct OwnedSection {
    entries: HashMap<Box<str>, Vec<Box<str>>>,
}

impl OwnedSection {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn get_all(&self, key: &str) -> Option<Vec<Box<str>>> {
        self.entries.get(key).cloned()
    }

    #[must_use]
    pub fn get_first(&self, key: &str) -> Option<Box<str>> {
        self.entries.get(key)?.first().cloned()
    }

    #[must_use]
    pub fn get_first_as_path(&self, key: &str) -> Option<Box<Path>> {
        self.get_first(key).as_deref().map(Path::new).map(Box::from)
    }

    #[must_use]
    pub fn get_first_as_boolean(&self, key: &str) -> Option<bool> {
        self.get_first(key).map(|s| &*s == "true")
    }
}

impl OwnedSection {
    pub fn insert_item(&mut self, mime: Box<str>, desktop_file: Box<str>) {
        self.entries.entry(mime).or_default().push(desktop_file);
    }
    pub fn insert_item_at_front(&mut self, mime: Box<str>, desktop_file: Box<str>) {
        self.entries
            .entry(mime)
            .or_default()
            .insert(0, desktop_file);
    }
    pub fn insert_items(&mut self, mime: Box<str>, mut desktop_files: Vec<Box<str>>) {
        self.entries
            .entry(mime)
            .or_default()
            .append(&mut desktop_files);
    }
}

#[derive(Debug, Default)]
pub struct IniFileOwned {
    sections: HashMap<Box<str>, OwnedSection>,
}

impl IniFileOwned {
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(content: &str) -> Self {
        let _span = debug_span!("from_str").entered();

        let mut sections = HashMap::new();
        let mut current_section = sections
            .entry(Box::from(""))
            .or_insert_with(OwnedSection::default);

        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
                continue;
            }

            if line.starts_with('[') && line.ends_with(']') {
                let current_section_name = &line[1..line.len() - 1];
                current_section = sections
                    .entry(Box::from(current_section_name.trim()))
                    .or_insert_with(OwnedSection::default);
                continue;
            }

            if let Some((key, value)) = line.split_once('=') {
                let key = key.trim();
                let value = value.trim();

                // Skip localized entries (containing [...])
                if key.contains('[') {
                    continue;
                }
                let values = value
                    .split(';')
                    .filter(|s| !s.is_empty())
                    .map(|s| Box::from(s.trim()))
                    .collect::<Vec<_>>();
                current_section.insert_items(Box::from(key), values);
            } else {
                warn!("malformed line: {line}");
            }
        }

        Self { sections }
    }
}

impl IniFileOwned {
    #[must_use]
    pub fn get_section(&self, section_name: &str) -> Option<&OwnedSection> {
        self.sections.get(section_name)
    }

    #[must_use]
    pub const fn sections(&self) -> &HashMap<Box<str>, OwnedSection> {
        &self.sections
    }

    #[must_use]
    pub fn format(&self) -> String {
        let mut str = String::with_capacity(self.into_iter().count() * 20); // 20 chars per line should be good
        let mut sections = self.sections().iter().collect::<Vec<_>>();
        sections.sort_by_key(|&(name, _)| name);
        for (name, section) in sections {
            if !name.is_empty() {
                if str.is_empty() {
                    let _ = str.write_str(&format!("[{name}]\n"));
                } else {
                    let _ = str.write_str(&format!("\n[{name}]\n"));
                }
            }
            let mut section = section.into_iter().collect::<Vec<_>>();
            section.sort_by_key(|(key, _)| *key);
            for (key, values) in section {
                let _ = str.write_str(&format!("{key}={}\n", values.join(";")));
            }
        }
        str
    }
}

impl IniFileOwned {
    pub fn get_section_mut(&mut self, section_name: &str) -> Option<&mut OwnedSection> {
        self.sections.get_mut(section_name)
    }

    pub fn section_entry(&mut self, section_name: Box<str>) -> Entry<'_, Box<str>, OwnedSection> {
        self.sections.entry(section_name)
    }
    pub fn insert_section(&mut self, name: Box<str>, section: OwnedSection) {
        self.sections.insert(name, section);
    }
}

impl<'a> IniFileOwned {
    #[allow(dead_code)]
    fn iter(&'a self) -> Box<dyn Iterator<Item = <&'a Self as IntoIterator>::Item> + 'a> {
        <&Self as IntoIterator>::into_iter(self)
    }
}

impl<'a> IntoIterator for &'a IniFileOwned {
    type Item = (&'a Box<str>, &'a Box<str>, &'a Vec<Box<str>>);
    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;

    fn into_iter(self) -> Self::IntoIter {
        let iter = self.sections.iter().flat_map(|(section_name, section)| {
            section
                .into_iter()
                .map(move |(key, values)| (section_name, key, values))
        });
        Box::new(iter)
    }
}

impl<'a> OwnedSection {
    #[allow(dead_code)]
    fn iter(&'a self) -> Box<dyn Iterator<Item = <&'a Self as IntoIterator>::Item> + 'a> {
        <&Self as IntoIterator>::into_iter(self)
    }
}

impl<'a> IntoIterator for &'a OwnedSection {
    type Item = (&'a Box<str>, &'a Vec<Box<str>>);
    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;

    fn into_iter(self) -> Self::IntoIter {
        let iter = self.entries.iter();
        Box::new(iter)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_parse_ini() {
        let content = r"[Section1]
key1=value1
key2=value2

[Section2]
foo=bar
baz=qux

; Comment
# Another comment
[Empty Section]

[Section With Spaces]
key with spaces=value with spaces; and more values
";

        let ini = IniFileOwned::from_str(content);

        assert_eq!(
            ini.get_section("Section1")
                .expect("section missing")
                .get_first("key1"),
            Some("value1".into())
        );
        assert_eq!(
            ini.get_section("Section2")
                .expect("section missing")
                .get_first("foo"),
            Some("bar".into())
        );

        assert!(ini.get_section("Empty Section").is_some());
        assert_ne!(
            ini.get_section("Section With Spaces")
                .expect("section missing")
                .get_all("key with spaces"),
            Some(vec!["value with spaces".into()])
        );
        assert_ne!(
            ini.get_section("Section With Spaces")
                .expect("section missing")
                .get_all("key with spaces"),
            Some(vec!["value with spaces".into()])
        );
        assert_eq!(
            ini.get_section("Section With Spaces")
                .expect("section missing")
                .get_all("key with spaces"),
            Some(vec!["value with spaces".into(), "and more values".into()])
        );

        assert!(ini.get_section("NonExistent").is_none());
        assert_eq!(
            ini.get_section("Section1")
                .expect("section missing")
                .get_first("nonexistent"),
            None
        );
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_empty_ini() {
        let content = "";
        let ini = IniFileOwned::from_str(content);
        assert_eq!(ini.sections().len(), 1);
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_no_sections() {
        let content = "key=value";
        let ini = IniFileOwned::from_str(content);
        assert_eq!(
            ini.get_section("")
                .expect("section missing")
                .get_first("key"),
            Some("value".into())
        );
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_values_iterator() {
        let content = r"
    [Section1]
    key1=value1
    key2=value2;values3

    [Section2]
    foo=bar
    ";
        let ini = IniFileOwned::from_str(content);
        let mut values: Vec<_> = ini.into_iter().collect();
        values.sort_by_key(|&(section, key, _)| (section, key));
        let mut iter = values.iter();
        assert_eq!(
            iter.next(),
            Some(&(&"Section1".into(), &"key1".into(), &vec!["value1".into()]))
        );
        assert_eq!(
            iter.next(),
            Some(&(
                &"Section1".into(),
                &"key2".into(),
                &vec!["value2".into(), "values3".into()]
            ))
        );
        assert_eq!(
            iter.next(),
            Some(&(&"Section2".into(), &"foo".into(), &vec!["bar".into()]))
        );
        assert_eq!(values.len(), 3);
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_values_iterator_2() {
        let content = r"
    [Section1]
    key1=value1
    key2=value2

    [Section2]
    foo=bar
    ";
        let ini = IniFileOwned::from_str(content);
        let mut count = 0;
        for (section, name, value) in &ini {
            assert!(!section.is_empty(), "Item should not be empty");
            assert!(!name.is_empty(), "Item should not be empty");
            assert!(!value.is_empty(), "Item should not be empty");
            count += 1;
        }
        assert_eq!(count, 3, "There should be 3 items in the iterator");
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_format_empty() {
        let content = "test=test";
        let ini = IniFileOwned::from_str(content);
        assert_eq!(ini.format(), "test=test\n");
    }

    #[test_log::test]
    #[test_log(default_log_filter = "trace")]
    fn test_format_multiple_sections() {
        let content = r"[B]
key1=value1
key2=value2;value3

[A]
foo=bar
";
        let content2 = r"[A]
foo=bar

[B]
key1=value1
key2=value2;value3
";
        let ini = IniFileOwned::from_str(content);
        assert_eq!(ini.format(), content2);
    }
}