use std::path::{Path, PathBuf};
const ICON_PNG: &[u8] = include_bytes!("../assets/icon-256.png");
pub fn install(app_id: &str) {
let system = ["/usr/share/applications", "/usr/local/share/applications"];
if system
.iter()
.any(|d| Path::new(d).join(format!("{app_id}.desktop")).exists())
{
return;
}
let data = std::env::var_os("XDG_DATA_HOME")
.filter(|d| !d.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")));
let (Some(data), Ok(exe)) = (data, std::env::current_exe()) else {
return;
};
for (path, content) in files(&data, &exe, app_id) {
if let Err(error) = write_if_changed(&path, &content) {
eprintln!("bombadil: could not write {}: {error}", path.display());
}
}
}
fn files(data: &Path, exe: &Path, app_id: &str) -> [(PathBuf, Vec<u8>); 2] {
let icon = data.join(format!("icons/hicolor/256x256/apps/{app_id}.png"));
let entry = format!(
"[Desktop Entry]\nType=Application\nName=Bombadil\n\
GenericName=Virtual environment manager\n\
Comment=A desktop keeper for uv virtual environments\n\
Exec=\"{}\"\nIcon={}\nTerminal=false\nCategories=Development;IDE;\n\
Keywords=python;uv;virtualenv;venv;\nStartupWMClass={app_id}\n",
exe.display(),
icon.display(),
);
[
(icon, ICON_PNG.to_vec()),
(
data.join(format!("applications/{app_id}.desktop")),
entry.into_bytes(),
),
]
}
fn write_if_changed(path: &Path, content: &[u8]) -> std::io::Result<()> {
if std::fs::read(path).is_ok_and(|old| old == content) {
return Ok(());
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_moved_binary_rewrites_exec() {
let dir = tempfile::tempdir().unwrap();
let entry = dir.path().join("applications/bombadil.desktop");
for exe in ["/old/bombadil-gui", "/new/bombadil"] {
for (path, content) in files(dir.path(), Path::new(exe), "bombadil") {
write_if_changed(&path, &content).unwrap();
}
}
let text = std::fs::read_to_string(entry).unwrap();
assert!(text.contains("Exec=\"/new/bombadil\""), "{text}");
assert!(text.contains("StartupWMClass=bombadil"));
let icon = dir.path().join("icons/hicolor/256x256/apps/bombadil.png");
assert!(text.contains(&format!("Icon={}", icon.display())), "{text}");
assert_eq!(std::fs::read(icon).unwrap(), ICON_PNG);
}
}