Detects an installed terminal emulator (kitty, alacritty, wezterm, foot, ghostty, gnome-terminal, konsole, xfce4-terminal, tilix, x-terminal-emulator, xterm) and bakes an explicit launch command into the entry, since Terminal=true is unreliable on modern desktops. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
118 lines
3.9 KiB
Rust
118 lines
3.9 KiB
Rust
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<String> = 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)
|
|
}
|