use anyhow::{Context, Result}; use directories::BaseDirs; use std::env; use std::fs; use std::path::PathBuf; const DESKTOP_TEMPLATE: &str = include_str!("../assets/emby-client.desktop"); /// A terminal emulator we know how to invoke. `args` are the tokens placed /// between the terminal binary and the command to run. struct TerminalSpec { bin: &'static str, args: &'static [&'static str], } const TERMINALS: &[TerminalSpec] = &[ TerminalSpec { bin: "kitty", args: &[] }, TerminalSpec { bin: "alacritty", args: &["-e"] }, TerminalSpec { bin: "wezterm", args: &["start", "--"] }, TerminalSpec { bin: "foot", args: &[] }, TerminalSpec { bin: "ghostty", args: &["-e"] }, TerminalSpec { bin: "gnome-terminal", args: &["--"] }, TerminalSpec { bin: "konsole", args: &["-e"] }, TerminalSpec { bin: "xfce4-terminal", args: &["-x"] }, TerminalSpec { bin: "tilix", args: &["-e"] }, TerminalSpec { bin: "x-terminal-emulator", args: &["-e"] }, TerminalSpec { bin: "xterm", args: &["-e"] }, ]; fn which(bin: &str) -> Option { let path = env::var_os("PATH")?; for dir in env::split_paths(&path) { let candidate = dir.join(bin); if candidate.is_file() { return Some(candidate); } } None } fn find_terminal() -> Option<(PathBuf, &'static [&'static str])> { // Honor $TERMINAL if it matches a spec we know. if let Ok(t) = env::var("TERMINAL") && let Some(spec) = TERMINALS.iter().find(|s| s.bin == t) && let Some(p) = which(spec.bin) { return Some((p, spec.args)); } for spec in TERMINALS { if let Some(p) = which(spec.bin) { return Some((p, spec.args)); } } None } /// Quote a token for a `.desktop` Exec line per the freedesktop spec. fn quote_desktop(s: &str) -> String { let needs_quote = s.is_empty() || s.chars().any(|c| { c.is_whitespace() || matches!( c, '"' | '\\' | '$' | '`' | '\'' | '|' | '&' | ';' | '<' | '>' | '(' | ')' | '#' | '~' | '*' | '?' ) }); if !needs_quote { return s.to_string(); } let mut out = String::with_capacity(s.len() + 2); out.push('"'); for c in s.chars() { if matches!(c, '"' | '\\' | '$' | '`') { out.push('\\'); } out.push(c); } out.push('"'); out } /// Write a `.desktop` entry for the current executable to /// `$XDG_DATA_HOME/applications/emby-client.desktop` (typically /// `~/.local/share/applications/`). Returns the path it was written to. pub fn install_desktop_entry() -> Result { let exe = env::current_exe().context("failed to locate current executable")?; let exe = exe.canonicalize().unwrap_or(exe); let exe_str = exe.to_string_lossy().into_owned(); let (exec_line, terminal_flag) = match find_terminal() { Some((term, args)) => { let mut parts: Vec = Vec::with_capacity(2 + args.len()); parts.push(quote_desktop(&term.to_string_lossy())); for a in args { parts.push(quote_desktop(a)); } parts.push(quote_desktop(&exe_str)); (parts.join(" "), "false") } None => (quote_desktop(&exe_str), "true"), }; let content = DESKTOP_TEMPLATE .replace("{EXEC}", &exec_line) .replace("{TERMINAL}", terminal_flag); let dirs = BaseDirs::new().context("could not determine user data directory")?; let dir = dirs.data_dir().join("applications"); fs::create_dir_all(&dir) .with_context(|| format!("failed to create {}", dir.display()))?; let target = dir.join("emby-client.desktop"); fs::write(&target, content) .with_context(|| format!("failed to write {}", target.display()))?; Ok(target) }