freedesktop_desktop_entry/
generic_entry.rs

1use std::{
2    collections::BTreeMap,
3    fmt::{self, Display, Formatter},
4    fs,
5    path::PathBuf,
6};
7
8use crate::{
9    decoder::{format_value, parse_line, Line},
10    DecodeError,
11};
12
13#[derive(Debug, Clone, Default)]
14pub struct Group(pub BTreeMap<Key, Value>);
15pub type Key = String;
16pub type Value = String;
17
18impl Group {
19    #[inline]
20    pub fn entry(&self, key: &str) -> Option<&str> {
21        self.0.get(key).map(|key| key.as_ref())
22    }
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct Groups(pub BTreeMap<GroupName, Group>);
27pub type GroupName = String;
28
29impl Groups {
30    #[inline]
31    pub fn group(&self, key: &str) -> Option<&Group> {
32        self.0.get(key)
33    }
34}
35
36/// Parse files based on the desktop entry spec. Any duplicate groups or keys
37/// will be overridden by the last parsed value and any entries without a group will be ignored.
38#[derive(Debug, Clone)]
39pub struct GenericEntry {
40    pub path: PathBuf,
41    pub groups: Groups,
42}
43
44impl GenericEntry {
45    pub fn from_str(path: impl Into<PathBuf>, input: &str) -> Result<GenericEntry, DecodeError> {
46        #[inline(never)]
47        fn inner<'a>(path: PathBuf, input: &'a str) -> Result<GenericEntry, DecodeError> {
48            let path: PathBuf = path.into();
49
50            let mut groups = Groups::default();
51            let mut active_group: Option<(&str, Group)> = None;
52
53            for line in input.lines() {
54                match parse_line(line)? {
55                    Line::Group(key) => {
56                        if let Some((prev_key, prev_group)) =
57                            active_group.replace((key, Group::default()))
58                        {
59                            groups.0.insert(prev_key.to_string(), prev_group);
60                        }
61                    }
62                    Line::Entry(key, value) => {
63                        if let Some((_, group)) = active_group.as_mut() {
64                            group.0.insert(key.to_string(), format_value(value)?);
65                        }
66                    }
67                    _ => (),
68                }
69            }
70
71            if let Some((prev_key, prev_group)) = active_group.take() {
72                groups.0.insert(prev_key.to_string(), prev_group);
73            }
74
75            Ok(GenericEntry { groups, path })
76        }
77
78        inner(path.into(), input)
79    }
80
81    /// Return an owned [`GenericEntry`]
82    #[inline]
83    pub fn from_path(path: impl Into<PathBuf>) -> Result<GenericEntry, DecodeError> {
84        let path: PathBuf = path.into();
85        let input = fs::read_to_string(&path)?;
86        Self::from_str(path, &input)
87    }
88
89    #[inline]
90    pub fn group(&self, key: &str) -> Option<&Group> {
91        self.groups.group(key)
92    }
93}
94
95impl Display for GenericEntry {
96    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
97        for (group_name, group) in &self.groups.0 {
98            let _ = writeln!(formatter, "[{}]", group_name);
99
100            for (key, value) in &group.0 {
101                let _ = writeln!(formatter, "{}={}", key, value);
102            }
103            writeln!(formatter)?;
104        }
105
106        Ok(())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    const GENERIC_ENTRY_PATH: &str = "tests_entries/generic.entry";
115
116    #[test]
117    fn can_load_file() {
118        let path = PathBuf::from(GENERIC_ENTRY_PATH);
119
120        let entry = GenericEntry::from_path(&path).expect("failed to parse file");
121        assert_eq!(entry.groups.0.len(), 1);
122        assert_eq!(entry.path, path)
123    }
124
125    #[test]
126    fn can_get_entries() {
127        let path = PathBuf::from(GENERIC_ENTRY_PATH);
128
129        let entry = GenericEntry::from_path(&path).expect("failed to parse file");
130
131        let group = entry.group("Thumbnailer Entry").unwrap();
132
133        assert_eq!(
134            group.entry("Exec"),
135            Some("cosmic-player --thumbnail %o --size %s %u")
136        );
137        assert_eq!(group.entry("TryExec"), Some("cosmic-player"));
138        assert_eq!(
139            group.entry("MimeType"),
140            Some("application/mxf;application/ram")
141        );
142    }
143}