Skip to main content

upstream_rs/models/common/
desktop_entry.rs

1use 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    /// Merge another DesktopEntry into self, preferring `other` when present
25    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    /// Set a parsed key/value pair, storing unknown keys in `extras`.
41    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    /// Sanitize fields that must always be overridden
90    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    /// Render to XDG .desktop format
101    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}