use crate::config::atomic_write;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const DESKTOP_FILE: &str = "dev.lios.Bar.desktop";
pub(crate) fn install() -> Result<String, String> {
let executable = env::current_exe()
.map_err(|error| format!("could not determine current executable: {error}"))?
.canonicalize()
.map_err(|error| format!("could not resolve current executable: {error}"))?;
let applications = applications_dir()?;
let path = applications.join(DESKTOP_FILE);
let contents = desktop_entry(&executable);
atomic_write(&path, contents.as_bytes(), 0o644)?;
refresh(&applications);
Ok(format!(
"installed LiosBar desktop launcher at {}",
path.display()
))
}
pub(crate) fn uninstall() -> Result<String, String> {
let applications = applications_dir()?;
let path = applications.join(DESKTOP_FILE);
match fs::remove_file(&path) {
Ok(()) => {
refresh(&applications);
Ok(format!(
"removed LiosBar desktop launcher at {}",
path.display()
))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(format!(
"LiosBar desktop launcher is not installed at {}",
path.display()
)),
Err(error) => Err(format!("could not remove '{}': {error}", path.display())),
}
}
fn applications_dir() -> Result<PathBuf, String> {
if let Some(root) = env::var_os("XDG_DATA_HOME").filter(|value| !value.is_empty()) {
return Ok(PathBuf::from(root).join("applications"));
}
let home = env::var_os("HOME")
.filter(|value| !value.is_empty())
.ok_or_else(|| "set HOME or XDG_DATA_HOME to install the launcher".to_string())?;
Ok(PathBuf::from(home).join(".local/share/applications"))
}
fn desktop_entry(executable: &Path) -> String {
let executable = desktop_exec(executable);
format!(
"[Desktop Entry]\nType=Application\nName=LiosBar\nGenericName=Terminal Workspace\nComment=Navigate live terminal sessions from a sidebar\nExec={executable}\nTryExec={executable}\nIcon=utilities-terminal\nTerminal=false\nCategories=System;TerminalEmulator;Utility;\nKeywords=shell;terminal;session;sidebar;workspace;\nStartupNotify=true\nStartupWMClass=dev.lios.Bar\n"
)
}
fn desktop_exec(path: &Path) -> String {
let text = path.to_string_lossy();
if text
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '+'))
{
return text.replace('%', "%%");
}
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 refresh(applications: &Path) {
let _ = Command::new("update-desktop-database")
.arg(applications)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn launcher_has_terminal_categories_and_safe_literal_exec() {
let entry = desktop_entry(Path::new("/opt/Lios Bar/lios-bar%stable"));
assert!(entry.contains("Name=LiosBar"));
assert!(entry.contains("Terminal=false"));
assert!(entry.contains("Exec=\"/opt/Lios Bar/lios-bar%%stable\""));
assert!(!entry.contains("\nExec=/bin/sh"));
}
}