Files
emby-tui-client/src/install.rs
T
janisslsmandClaude Opus 4.7 a9495389e8 Add --install-desktop to install a desktop entry
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]>
2026-05-16 22:12:16 +03:00

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)
}