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
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Emby Client
|
||||||
|
GenericName=Emby TUI Client
|
||||||
|
Comment=TUI Emby client with libmpv playback
|
||||||
|
Exec={EXEC}
|
||||||
|
Terminal={TERMINAL}
|
||||||
|
Categories=AudioVideo;Player;
|
||||||
|
Keywords=emby;media;player;tui;
|
||||||
@@ -21,6 +21,10 @@ pub struct CliArgs {
|
|||||||
/// Path to config file. Defaults: ./config.toml then $XDG_CONFIG_HOME/emby-client/config.toml
|
/// Path to config file. Defaults: ./config.toml then $XDG_CONFIG_HOME/emby-client/config.toml
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub config: Option<PathBuf>,
|
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)]
|
#[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 app;
|
||||||
mod config;
|
mod config;
|
||||||
mod emby;
|
mod emby;
|
||||||
|
mod install;
|
||||||
mod login;
|
mod login;
|
||||||
mod player;
|
mod player;
|
||||||
mod ui;
|
mod ui;
|
||||||
@@ -11,6 +12,13 @@ use clap::Parser;
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = config::CliArgs::parse();
|
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, save_target) = config::load(&args)?;
|
||||||
|
|
||||||
let cfg = match cfg {
|
let cfg = match cfg {
|
||||||
|
|||||||
Reference in new issue
Block a user