upstream_rs/models/common/
desktop_entry.rs1use std::{collections::BTreeMap, path::Path};
2
3use crate::models::upstream::Package;
4
5#[derive(Debug, Default)]
6pub struct DesktopEntry {
7 pub name: Option<String>,
8 pub comment: Option<String>,
9 pub exec: Option<String>,
10 pub icon: Option<String>,
11 pub categories: Option<String>,
12 pub terminal: bool,
13 pub extras: BTreeMap<String, String>,
14}
15
16impl DesktopEntry {
17 pub fn new(name: &str) -> DesktopEntry {
18 DesktopEntry {
19 name: Some(name.to_string()),
20 ..DesktopEntry::default()
21 }
22 }
23
24 pub fn merge(self, other: DesktopEntry) -> DesktopEntry {
26 let mut extras = self.extras;
27 extras.extend(other.extras);
28
29 DesktopEntry {
30 name: other.name.or(self.name),
31 comment: other.comment.or(self.comment),
32 exec: other.exec.or(self.exec),
33 icon: other.icon.or(self.icon),
34 categories: other.categories.or(self.categories),
35 terminal: other.terminal || self.terminal,
36 extras,
37 }
38 }
39
40 pub fn set_field(&mut self, key: &str, value: String) {
42 match key {
43 "Name" => self.name = Some(value),
44 "Comment" => self.comment = Some(value),
45 "Exec" => self.exec = Some(value),
46 "Icon" => self.icon = Some(value),
47 "Categories" => self.categories = Some(value),
48 "Terminal" => self.terminal = value.eq_ignore_ascii_case("true"),
49 _ => {
50 self.extras.insert(key.to_string(), value);
51 }
52 }
53 }
54
55 pub fn from_package(package: &Package) -> DesktopEntry {
56 let mut entry = DesktopEntry::new(&package.name);
57 entry.exec = package
58 .exec_path
59 .as_ref()
60 .map(|path| path.display().to_string());
61 entry.icon = Some(
62 package
63 .icon_path
64 .as_deref()
65 .map(|path| path.display().to_string())
66 .unwrap_or_default(),
67 );
68 entry
69 }
70
71 pub fn ensure_name(mut self, fallback: &str) -> DesktopEntry {
72 if self.name.is_some() {
73 return self;
74 }
75
76 if let Some(localized_name) = self
77 .extras
78 .iter()
79 .find_map(|(key, value)| key.starts_with("Name[").then_some(value.as_str()))
80 {
81 self.name = Some(localized_name.to_string());
82 return self;
83 }
84
85 self.name = Some(fallback.to_string());
86 self
87 }
88
89 pub fn sanitize(mut self, exec: &Path, icon: Option<&Path>) -> DesktopEntry {
91 self.exec = Some(exec.display().to_string());
92 self.icon = Some(
93 icon.map(|path| path.display().to_string())
94 .unwrap_or_default(),
95 );
96 self.terminal = false;
97 self
98 }
99
100 pub fn to_desktop_file(&self) -> String {
102 let mut out = String::from("[Desktop Entry]\nType=Application\nVersion=1.0\n");
103
104 if let Some(name) = &self.name {
105 out.push_str(&format!("Name={}\n", name));
106 }
107
108 if let Some(exec) = &self.exec {
109 out.push_str(&format!("Exec={}\n", exec));
110 }
111
112 if let Some(icon) = &self.icon {
113 out.push_str(&format!("Icon={}\n", icon));
114 }
115
116 if let Some(comment) = &self.comment {
117 out.push_str(&format!("Comment={}\n", comment));
118 }
119
120 out.push_str(&format!(
121 "Categories={}\n",
122 self.categories.as_deref().unwrap_or("Application;")
123 ));
124
125 out.push_str(&format!("Terminal={}\n", self.terminal));
126
127 for (key, value) in &self.extras {
128 if matches!(
129 key.as_str(),
130 "Type"
131 | "Version"
132 | "Name"
133 | "Exec"
134 | "Icon"
135 | "Comment"
136 | "Categories"
137 | "Terminal"
138 ) {
139 continue;
140 }
141 out.push_str(&format!("{key}={value}\n"));
142 }
143
144 out
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::DesktopEntry;
151 use crate::models::common::enums::{Channel, Filetype, Provider};
152 use crate::models::upstream::Package;
153 use std::path::PathBuf;
154
155 #[test]
156 fn from_package_maps_name_exec_and_icon_paths() {
157 let mut package = Package::with_defaults(
158 "tool".to_string(),
159 "owner/tool".to_string(),
160 Filetype::Binary,
161 None,
162 None,
163 Channel::Stable,
164 Provider::Github,
165 None,
166 );
167 package.exec_path = Some(PathBuf::from("/tmp/tool"));
168 package.icon_path = Some(PathBuf::from("/tmp/tool.png"));
169
170 let entry = DesktopEntry::from_package(&package);
171 assert_eq!(entry.name.as_deref(), Some("tool"));
172 assert_eq!(entry.exec.as_deref(), Some("/tmp/tool"));
173 assert_eq!(entry.icon.as_deref(), Some("/tmp/tool.png"));
174 }
175
176 #[test]
177 fn from_package_uses_empty_icon_when_icon_path_missing() {
178 let package = Package::with_defaults(
179 "tool".to_string(),
180 "owner/tool".to_string(),
181 Filetype::Binary,
182 None,
183 None,
184 Channel::Stable,
185 Provider::Github,
186 None,
187 );
188
189 let entry = DesktopEntry::from_package(&package);
190 assert_eq!(entry.name.as_deref(), Some("tool"));
191 assert_eq!(entry.exec, None);
192 assert_eq!(entry.icon.as_deref(), Some(""));
193 }
194}