use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const DESKTOP_FILE_NAME: &str = "dev.lios.Terminal.desktop";
pub fn install_desktop_entry() -> Result<String, String> {
let executable = env::current_exe()
.map_err(|error| format!("failed to determine current executable: {error}"))?
.canonicalize()
.map_err(|error| format!("failed to resolve current executable path: {error}"))?;
let applications_dir = applications_dir()?;
let desktop_file = applications_dir.join(DESKTOP_FILE_NAME);
fs::create_dir_all(&applications_dir).map_err(|error| {
format!(
"failed to create applications directory '{}': {error}",
applications_dir.display()
)
})?;
fs::write(&desktop_file, desktop_entry(&executable)).map_err(|error| {
format!(
"failed to write desktop launcher '{}': {error}",
desktop_file.display()
)
})?;
Ok(format!(
"installed desktop launcher\n file: {}\n exec: {}\n",
desktop_file.display(),
executable.display()
))
}
pub fn uninstall_desktop_entry() -> Result<String, String> {
let desktop_file = applications_dir()?.join(DESKTOP_FILE_NAME);
if !desktop_file.exists() {
return Ok(format!(
"desktop launcher is not installed at {}\n",
desktop_file.display()
));
}
fs::remove_file(&desktop_file).map_err(|error| {
format!(
"failed to remove desktop launcher '{}': {error}",
desktop_file.display()
)
})?;
Ok(format!(
"removed desktop launcher\n file: {}\n",
desktop_file.display()
))
}
fn desktop_entry(executable: &Path) -> String {
format!(
"[Desktop Entry]\nType=Application\nName=Lios\nGenericName=Terminal\nComment=GTK/VTE terminal emulator\nExec={}\nTryExec={}\nIcon=utilities-terminal\nTerminal=false\nCategories=System;TerminalEmulator;Utility;\nKeywords=shell;prompt;command;terminal;\nStartupNotify=true\nStartupWMClass=dev.lios.Terminal\n",
desktop_exec_path(executable),
desktop_exec_path(executable),
)
}
fn applications_dir() -> Result<PathBuf, String> {
if let Ok(data_home) = env::var("XDG_DATA_HOME") {
if !data_home.is_empty() {
return Ok(PathBuf::from(data_home).join("applications"));
}
}
let home = env::var("HOME").map_err(|_| {
"unable to determine applications directory; set HOME or XDG_DATA_HOME".to_string()
})?;
if home.is_empty() {
return Err("unable to determine applications directory; HOME is empty".to_string());
}
Ok(PathBuf::from(home).join(".local/share/applications"))
}
fn desktop_exec_path(path: &Path) -> String {
let text = path.to_string_lossy();
if text
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '+'))
{
escape_percent(&text)
} else {
let escaped = text.chars().fold(String::new(), |mut output, ch| {
match ch {
'%' => output.push_str("%%"),
'\\' | '"' | '$' | '`' => {
output.push('\\');
output.push(ch);
}
_ => output.push(ch),
}
output
});
format!("\"{escaped}\"")
}
}
fn escape_percent(text: &str) -> String {
text.replace('%', "%%")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn desktop_exec_path_leaves_simple_paths_unquoted() {
assert_eq!(
desktop_exec_path(Path::new("/usr/bin/lios")),
"/usr/bin/lios"
);
}
#[test]
fn desktop_exec_path_quotes_paths_with_spaces() {
assert_eq!(
desktop_exec_path(Path::new("/tmp/Lios Build/lios")),
"\"/tmp/Lios Build/lios\""
);
}
#[test]
fn desktop_exec_path_escapes_field_codes() {
assert_eq!(
desktop_exec_path(Path::new("/tmp/%/lios")),
"\"/tmp/%%/lios\""
);
}
}