Copy mime_app from cosmic-files
- Add optional language sorter parameter to MimeAppCache::reload() - Use mime_app::exec_term_command in desktop::spawn_desktop_exec() - Add function mime_app::exec_term_to_command() to get exec command using terminal with fallback - Use this in MimeApp::command() - Use this in desktop::spawn_desktop_exec()
This commit is contained in:
parent
0aed16b23c
commit
eed20610a7
4 changed files with 363 additions and 12 deletions
|
|
@ -43,6 +43,7 @@ desktop = [
|
|||
"dep:freedesktop-desktop-entry",
|
||||
"dep:mime",
|
||||
"dep:shlex",
|
||||
"dep:xdg",
|
||||
"tokio?/io-util",
|
||||
"tokio?/net",
|
||||
]
|
||||
|
|
@ -95,6 +96,7 @@ cosmic-config = { path = "cosmic-config" }
|
|||
cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
css-color = "0.2.5"
|
||||
derive_setters = "0.1.5"
|
||||
icu_collator = "1.5"
|
||||
image = { version = "0.25.5", default-features = false, features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
|
|
@ -103,6 +105,8 @@ lazy_static = "1.4.0"
|
|||
libc = { version = "0.2.155", optional = true }
|
||||
license = { version = "3.5.1", optional = true }
|
||||
mime = { version = "0.3.17", optional = true }
|
||||
mime_guess = "2"
|
||||
once_cell = "1.19"
|
||||
palette = "0.7.3"
|
||||
rfd = { version = "0.14.0", default-features = false, features = ["xdg-portal"], optional = true }
|
||||
rustix = { version = "0.38.34", features = [
|
||||
|
|
@ -117,11 +121,13 @@ tokio = { version = "1.24.2", optional = true }
|
|||
tracing = "0.1.41"
|
||||
unicode-segmentation = "1.6"
|
||||
url = "2.4.0"
|
||||
xdg = { version = "2.5.2", optional = true }
|
||||
zbus = { version = "4.2.1", default-features = false, optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" }
|
||||
freedesktop-desktop-entry = { version = "0.5.1", optional = true }
|
||||
freedesktop_entry_parser = "1.3"
|
||||
shlex = { version = "1.3.0", optional = true }
|
||||
|
||||
[dependencies.cosmic-theme]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
|
|||
#[cfg(not(windows))]
|
||||
use std::{borrow::Cow, ffi::OsStr};
|
||||
|
||||
use crate::mime_app::{exec_term_to_command, exec_to_command};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IconSource {
|
||||
Name(String),
|
||||
|
|
@ -258,21 +260,14 @@ pub async fn spawn_desktop_exec<S, I, K, V>(
|
|||
_ => return,
|
||||
};
|
||||
|
||||
let mut cmd = if terminal {
|
||||
let mut cmd = std::process::Command::new("cosmic-term");
|
||||
cmd.args(vec!["--", format!("{}", &executable).as_str()]);
|
||||
cmd
|
||||
let cmd = if terminal {
|
||||
exec_term_to_command(&executable, None)
|
||||
} else {
|
||||
let mut cmd = std::process::Command::new(&executable);
|
||||
for arg in exec {
|
||||
// TODO handle "%" args here if necessary?
|
||||
if !arg.starts_with('%') {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
cmd
|
||||
exec_to_command(&executable, None)
|
||||
};
|
||||
|
||||
let Some(mut cmd) = cmd else { return };
|
||||
|
||||
cmd.envs(env_vars);
|
||||
|
||||
// https://systemd.io/DESKTOP_ENVIRONMENTS
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ pub mod keyboard_nav;
|
|||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mod desktop;
|
||||
|
||||
pub mod mime_app;
|
||||
|
||||
#[cfg(all(feature = "process", not(windows)))]
|
||||
pub mod process;
|
||||
|
||||
|
|
|
|||
347
src/mime_app.rs
Normal file
347
src/mime_app.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
use crate::desktop;
|
||||
use crate::widget;
|
||||
use icu_collator::Collator;
|
||||
pub use mime_guess::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
use palette::cast::ArraysInto;
|
||||
use std::{
|
||||
cmp::Ordering, collections::HashMap, env, path::PathBuf, process, sync::Mutex, time::Instant,
|
||||
};
|
||||
|
||||
pub fn exec_to_command(exec: &str, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
let args_vec: Vec<String> = shlex::split(exec)?;
|
||||
let mut args = args_vec.iter();
|
||||
let mut command = process::Command::new(args.next()?);
|
||||
for arg in args {
|
||||
if arg.starts_with('%') {
|
||||
match arg.as_str() {
|
||||
"%f" | "%F" | "%u" | "%U" => {
|
||||
if let Some(path) = &path_opt {
|
||||
command.arg(path);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("unsupported Exec code {:?} in {:?}", arg, exec);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
command.arg(arg);
|
||||
}
|
||||
}
|
||||
Some(command)
|
||||
}
|
||||
|
||||
pub fn exec_term_to_command(exec: &str, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
let Some(mut term_cmd) = terminal()
|
||||
.and_then(|term| term.exec)
|
||||
.and_then(|exec| exec_to_command(&exec, None))
|
||||
else {
|
||||
tracing::warn!("No terminal was found to run {:?}", exec);
|
||||
return None;
|
||||
};
|
||||
|
||||
let exec_cmd = exec_to_command(exec, path_opt)?;
|
||||
term_cmd.arg("-e");
|
||||
term_cmd.arg(exec_cmd.get_program());
|
||||
term_cmd.args(exec_cmd.get_args());
|
||||
Some(term_cmd)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MimeApp {
|
||||
pub id: String,
|
||||
pub path: Option<PathBuf>,
|
||||
pub name: String,
|
||||
pub exec: Option<String>,
|
||||
pub icon: widget::icon::Handle,
|
||||
pub is_default: bool,
|
||||
pub terminal: bool,
|
||||
}
|
||||
|
||||
impl MimeApp {
|
||||
//TODO: support multiple files
|
||||
pub fn command(&self, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
if self.terminal {
|
||||
exec_term_to_command(self.exec.as_deref()?, path_opt)
|
||||
} else {
|
||||
exec_to_command(self.exec.as_deref()?, path_opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
impl From<&desktop::DesktopEntryData> for MimeApp {
|
||||
fn from(app: &desktop::DesktopEntryData) -> Self {
|
||||
Self {
|
||||
id: app.id.clone(),
|
||||
path: app.path.clone(),
|
||||
name: app.name.clone(),
|
||||
exec: app.exec.clone(),
|
||||
icon: match &app.icon {
|
||||
desktop::IconSource::Name(name) => widget::icon::from_name(name.as_str()).handle(),
|
||||
desktop::IconSource::Path(path) => widget::icon::from_path(path.clone()),
|
||||
},
|
||||
is_default: false,
|
||||
terminal: app.terminal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
|
||||
path_opt
|
||||
.as_ref()
|
||||
.and_then(|path| path.file_name())
|
||||
.map(|x| x == filename)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub trait LanguageSorter {
|
||||
fn language_sorter(&self) -> Option<Lazy<Collator>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MimeAppCache {
|
||||
cache: HashMap<Mime, Vec<MimeApp>>,
|
||||
terminals: Vec<MimeApp>,
|
||||
}
|
||||
|
||||
impl MimeAppCache {
|
||||
pub fn new() -> Self {
|
||||
let mut mime_app_cache = Self {
|
||||
cache: HashMap::new(),
|
||||
terminals: Vec::new(),
|
||||
};
|
||||
mime_app_cache.reload(None);
|
||||
mime_app_cache
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
pub fn reload(&mut self, language_sorter: Option<&Lazy<Collator>>) {}
|
||||
|
||||
// Only available when using desktop feature of libcosmic, which only works on Unix-likes
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn reload(&mut self, language_sorter: Option<&Lazy<Collator>>) {
|
||||
let start = Instant::now();
|
||||
|
||||
self.cache.clear();
|
||||
self.terminals.clear();
|
||||
|
||||
//TODO: get proper locale?
|
||||
let locale = None;
|
||||
|
||||
// Load desktop applications by supported mime types
|
||||
//TODO: hashmap for all apps by id?
|
||||
let all_apps = desktop::load_applications(locale, false);
|
||||
for app in all_apps.iter() {
|
||||
for mime in app.mime_types.iter() {
|
||||
let apps = self
|
||||
.cache
|
||||
.entry(mime.clone())
|
||||
.or_insert_with(|| Vec::with_capacity(1));
|
||||
if apps.iter().find(|x| x.id == app.id).is_none() {
|
||||
apps.push(MimeApp::from(app));
|
||||
}
|
||||
}
|
||||
for category in app.categories.iter() {
|
||||
if category == "TerminalEmulator" {
|
||||
self.terminals.push(MimeApp::from(app));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let desktops: Vec<String> = env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.split(':')
|
||||
.map(|x| x.to_ascii_lowercase())
|
||||
.collect();
|
||||
|
||||
// Load mimeapps.list files
|
||||
// https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html
|
||||
//TODO: ensure correct lookup order
|
||||
let mut mimeapps_paths = Vec::new();
|
||||
match xdg::BaseDirectories::new() {
|
||||
Ok(xdg_dirs) => {
|
||||
for path in xdg_dirs.find_data_files("applications/mimeapps.list") {
|
||||
mimeapps_paths.push(path);
|
||||
}
|
||||
for desktop in desktops.iter().rev() {
|
||||
for path in
|
||||
xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list"))
|
||||
{
|
||||
mimeapps_paths.push(path);
|
||||
}
|
||||
}
|
||||
for path in xdg_dirs.find_config_files("mimeapps.list") {
|
||||
mimeapps_paths.push(path);
|
||||
}
|
||||
for desktop in desktops.iter().rev() {
|
||||
for path in xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")) {
|
||||
mimeapps_paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to get xdg base directories: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: handle directory specific behavior
|
||||
for path in mimeapps_paths {
|
||||
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to parse {:?}: {}", path, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for attr in entry
|
||||
.section("Added Associations")
|
||||
.attrs()
|
||||
.chain(entry.section("Default Applications").attrs())
|
||||
{
|
||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||
if let Some(filenames) = attr.value {
|
||||
for filename in filenames.split_terminator(';') {
|
||||
tracing::trace!("add {}={}", mime, filename);
|
||||
let apps = self
|
||||
.cache
|
||||
.entry(mime.clone())
|
||||
.or_insert_with(|| Vec::with_capacity(1));
|
||||
if apps
|
||||
.iter()
|
||||
.find(|x| filename_eq(&x.path, filename))
|
||||
.is_none()
|
||||
{
|
||||
if let Some(app) =
|
||||
all_apps.iter().find(|x| filename_eq(&x.path, filename))
|
||||
{
|
||||
apps.push(MimeApp::from(app));
|
||||
} else {
|
||||
tracing::debug!("failed to add association for {:?}: application {:?} not found", mime, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for attr in entry.section("Removed Associations").attrs() {
|
||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||
if let Some(filenames) = attr.value {
|
||||
for filename in filenames.split_terminator(';') {
|
||||
tracing::trace!("remove {}={}", mime, filename);
|
||||
if let Some(apps) = self.cache.get_mut(&mime) {
|
||||
apps.retain(|x| !filename_eq(&x.path, filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for attr in entry.section("Default Applications").attrs() {
|
||||
if let Ok(mime) = attr.name.parse::<Mime>() {
|
||||
if let Some(filenames) = attr.value {
|
||||
for filename in filenames.split_terminator(';') {
|
||||
tracing::trace!("default {}={}", mime, filename);
|
||||
if let Some(apps) = self.cache.get_mut(&mime) {
|
||||
let mut found = false;
|
||||
for app in apps.iter_mut() {
|
||||
if filename_eq(&app.path, filename) {
|
||||
app.is_default = true;
|
||||
found = true;
|
||||
} else {
|
||||
app.is_default = false;
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break;
|
||||
} else {
|
||||
tracing::debug!("failed to set default for {:?}: application {:?} not found", mime, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort apps by name
|
||||
for apps in self.cache.values_mut() {
|
||||
apps.sort_by(|a, b| match (a.is_default, b.is_default) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => language_sorter
|
||||
.as_ref()
|
||||
.map_or(Ordering::Equal, |ls| ls.compare(&a.name, &b.name)),
|
||||
});
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
tracing::info!("loaded mime app cache in {:?}", elapsed);
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &Mime) -> Vec<MimeApp> {
|
||||
self.cache
|
||||
.get(&key)
|
||||
.map_or_else(|| Vec::new(), |x| x.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn terminal(&self) -> Option<MimeApp> {
|
||||
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
|
||||
|
||||
// Look for and return preferred terminals
|
||||
//TODO: fallback order beyond cosmic-term?
|
||||
for id in &["com.system76.CosmicTerm"] {
|
||||
for terminal in self.terminals.iter() {
|
||||
if &terminal.id == id {
|
||||
return Some(terminal.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return whatever was the first terminal found
|
||||
self.terminals.first().map(|x| x.clone())
|
||||
}
|
||||
}
|
||||
|
||||
static MIME_APP_CACHE: Lazy<Mutex<MimeAppCache>> = Lazy::new(|| Mutex::new(MimeAppCache::new()));
|
||||
|
||||
pub fn mime_apps(mime: &Mime) -> Vec<MimeApp> {
|
||||
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
|
||||
mime_app_cache.get(mime)
|
||||
}
|
||||
|
||||
pub fn terminal() -> Option<MimeApp> {
|
||||
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
|
||||
|
||||
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
|
||||
|
||||
// Look for and return preferred terminals
|
||||
//TODO: fallback order beyond cosmic-term?
|
||||
for id in &["com.system76.CosmicTerm"] {
|
||||
for terminal in mime_app_cache.terminals.iter() {
|
||||
if &terminal.id == id {
|
||||
return Some(terminal.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return whatever was the first terminal found
|
||||
mime_app_cache.terminals.first().map(|x| x.clone())
|
||||
}
|
||||
|
||||
pub fn reload(language_sorter: Option<&Lazy<Collator>>) {
|
||||
let mut mime_app_cache = MIME_APP_CACHE.lock().unwrap();
|
||||
|
||||
mime_app_cache.reload(language_sorter);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue