freedesktop_desktop_entry/
generic_entry.rs1use 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#[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 #[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}