1use anyhow::{Context, Result};
2use std::fs;
3use std::path::Path;
4
5#[derive(Debug, Clone)]
6pub struct DesktopEntryInput<'a> {
7 pub name: &'a str,
8 pub generic_name: Option<&'a str>,
9 pub comment: Option<&'a str>,
10 pub exec_path: &'a Path,
11 pub icon_name: &'a str,
12 pub categories: &'a [String],
13 pub terminal: bool,
14}
15
16pub fn write_desktop_entry(path: &Path, input: &DesktopEntryInput<'_>) -> Result<()> {
17 if let Some(parent) = path.parent() {
18 fs::create_dir_all(parent).with_context(|| format!("failed to create desktop dir: {}", parent.display()))?;
19 }
20
21 let categories = if input.categories.is_empty() {
22 "Utility;".to_string()
23 } else {
24 let mut joined = input.categories.join(";");
25 if !joined.ends_with(';') { joined.push(';'); }
26 joined
27 };
28
29 let mut text = String::new();
30 text.push_str("[Desktop Entry]\n");
31 text.push_str("Type=Application\n");
32 text.push_str(&format!("Name={}\n", escape_value(input.name)));
33 if let Some(generic) = input.generic_name {
34 text.push_str(&format!("GenericName={}\n", escape_value(generic)));
35 }
36 if let Some(comment) = input.comment {
37 text.push_str(&format!("Comment={}\n", escape_value(comment)));
38 }
39 text.push_str(&format!("Exec={} %U\n", quote_exec(input.exec_path)));
40 text.push_str(&format!("Icon={}\n", escape_value(input.icon_name)));
41 text.push_str(&format!("Terminal={}\n", if input.terminal { "true" } else { "false" }));
42 text.push_str(&format!("Categories={}\n", categories));
43 text.push_str("StartupNotify=true\n");
44
45 fs::write(path, text).with_context(|| format!("failed to write desktop entry: {}", path.display()))?;
46 Ok(())
47}
48
49fn escape_value(value: &str) -> String {
50 value.replace('\\', "\\\\").replace('\n', "\\n")
51}
52
53fn quote_exec(path: &Path) -> String {
54 let s = path.to_string_lossy();
55 if s.contains(' ') {
56 format!("\"{}\"", s.replace('"', "\\\""))
57 } else {
58 s.to_string()
59 }
60}