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]>
This commit is contained in:
1 parent
79f3a2e13b
commit
a9495389e8
4 files changed
+138
No files matched your search
@@ -21,6 +21,10 @@ pub struct CliArgs {
|
||||
/// Path to config file. Defaults: ./config.toml then $XDG_CONFIG_HOME/emby-client/config.toml
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Install a desktop entry for the current executable and exit
|
||||
#[arg(long)]
|
||||
pub install_desktop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod emby;
|
||||
mod install;
|
||||
mod login;
|
||||
mod player;
|
||||
mod ui;
|
||||
@@ -11,6 +12,13 @@ use clap::Parser;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = config::CliArgs::parse();
|
||||
|
||||
if args.install_desktop {
|
||||
let path = install::install_desktop_entry()?;
|
||||
println!("Installed desktop entry: {}", path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (cfg, save_target) = config::load(&args)?;
|
||||
|
||||
let cfg = match cfg {
|
||||
|
||||
Reference in new issue
Block a user