Merge pull request #891 from joshuamegnauth54/feat-multiple-uris-single-app
feat: Open multiple files with one/multiple apps
This commit is contained in:
commit
3f03f81ccc
3 changed files with 271 additions and 137 deletions
256
src/app.rs
256
src/app.rs
|
|
@ -66,21 +66,21 @@ use wayland_client::{protocol::wl_output::WlOutput, Proxy};
|
||||||
use crate::{
|
use crate::{
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||||
config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig, TypeToSearch},
|
config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig, TypeToSearch},
|
||||||
dialog::{DialogKind, DialogMessage},
|
dialog::{Dialog, DialogKind, DialogMessage, DialogResult},
|
||||||
fl, home_dir,
|
fl, home_dir,
|
||||||
key_bind::key_binds,
|
key_bind::key_binds,
|
||||||
localize::LANGUAGE_SORTER,
|
localize::LANGUAGE_SORTER,
|
||||||
menu, mime_app, mime_icon,
|
menu,
|
||||||
|
mime_app::{self, MimeApp, MimeAppCache},
|
||||||
|
mime_icon,
|
||||||
mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
|
mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
|
||||||
operation::{Controller, Operation, OperationSelection, ReplaceResult},
|
operation::{
|
||||||
|
Controller, Operation, OperationError, OperationErrorType, OperationSelection,
|
||||||
|
ReplaceResult,
|
||||||
|
},
|
||||||
spawn_detached::spawn_detached,
|
spawn_detached::spawn_detached,
|
||||||
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
||||||
};
|
};
|
||||||
use crate::{
|
|
||||||
dialog::Dialog,
|
|
||||||
operation::{OperationError, OperationErrorType},
|
|
||||||
};
|
|
||||||
use crate::{dialog::DialogResult, mime_app::MimeApp};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
|
|
@ -471,7 +471,7 @@ pub enum DialogPage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
mime: mime_guess::Mime,
|
mime: mime_guess::Mime,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
store_opt: Option<mime_app::MimeApp>,
|
store_opt: Option<MimeApp>,
|
||||||
},
|
},
|
||||||
RenameItem {
|
RenameItem {
|
||||||
from: PathBuf,
|
from: PathBuf,
|
||||||
|
|
@ -544,7 +544,7 @@ pub struct App {
|
||||||
dialog_text_input: widget::Id,
|
dialog_text_input: widget::Id,
|
||||||
key_binds: HashMap<KeyBind, Action>,
|
key_binds: HashMap<KeyBind, Action>,
|
||||||
margin: HashMap<window::Id, (f32, f32, f32, f32)>,
|
margin: HashMap<window::Id, (f32, f32, f32, f32)>,
|
||||||
mime_app_cache: mime_app::MimeAppCache,
|
mime_app_cache: MimeAppCache,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
mounter_items: HashMap<MounterKey, MounterItems>,
|
mounter_items: HashMap<MounterKey, MounterItems>,
|
||||||
network_drive_connecting: Option<(MounterKey, String)>,
|
network_drive_connecting: Option<(MounterKey, String)>,
|
||||||
|
|
@ -576,22 +576,95 @@ pub struct App {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn open_file(&mut self, path: &PathBuf) {
|
fn open_file(&mut self, paths: &[impl AsRef<Path>]) {
|
||||||
let mime = mime_icon::mime_for_path(path);
|
// Associate all paths to its MIME type
|
||||||
|
// This allows handling paths as groups if possible, such as launching a single video
|
||||||
|
// player that is passed every path.
|
||||||
|
let mut groups: HashMap<Mime, Vec<PathBuf>> = HashMap::new();
|
||||||
|
for (mime, path) in paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| (mime_icon::mime_for_path(path), path.as_ref().to_owned()))
|
||||||
|
{
|
||||||
|
groups.entry(mime).or_default().push(path);
|
||||||
|
}
|
||||||
|
|
||||||
if mime == "application/x-desktop" {
|
'outer: for (mime, paths) in groups {
|
||||||
// Try opening desktop application
|
log::debug!("Attempting to launch app\n\tfor: {mime}\n\twith: {paths:?}");
|
||||||
match freedesktop_entry_parser::parse_entry(path) {
|
|
||||||
Ok(entry) => match entry.section("Desktop Entry").attr("Exec") {
|
// First launch apps that can be launched directly
|
||||||
Some(exec) => match mime_app::exec_to_command(exec, None) {
|
if mime == "application/x-desktop" {
|
||||||
Some(mut command) => match spawn_detached(&mut command) {
|
// Try opening desktop application
|
||||||
Ok(()) => {
|
App::launch_desktop_entries(&paths);
|
||||||
return;
|
continue;
|
||||||
|
} else if mime == "application/x-executable" || mime == "application/vnd.appimage" {
|
||||||
|
// Try opening executable
|
||||||
|
for path in paths {
|
||||||
|
let mut command = std::process::Command::new(&path);
|
||||||
|
match spawn_detached(&mut command) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => match err.kind() {
|
||||||
|
io::ErrorKind::PermissionDenied => {
|
||||||
|
// If permission is denied, try marking as executable, then running
|
||||||
|
self.dialog_pages
|
||||||
|
.push_back(DialogPage::SetExecutableAndLaunch {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Err(err) => {
|
_ => {
|
||||||
log::warn!("failed to execute {:?}: {}", path, err);
|
log::warn!("failed to execute {:?}: {}", path, err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try mime apps, which should be faster than xdg-open
|
||||||
|
if self.launch_from_mime_cache(&mime, &paths) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through subclasses if available
|
||||||
|
if let Some(mime_sub_classes) = mime_icon::parent_mime_types(&mime) {
|
||||||
|
for sub_class in mime_sub_classes {
|
||||||
|
if self.launch_from_mime_cache(&sub_class, &paths) {
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to using open crate
|
||||||
|
for path in paths {
|
||||||
|
match open::that_detached(&path) {
|
||||||
|
Ok(()) => {
|
||||||
|
let _ = recently_used_xbel::update_recently_used(
|
||||||
|
&path,
|
||||||
|
App::APP_ID.to_string(),
|
||||||
|
"cosmic-files".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to open {:?}: {}", path, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
|
||||||
|
for path in paths.iter().map(AsRef::as_ref) {
|
||||||
|
match freedesktop_entry_parser::parse_entry(path) {
|
||||||
|
Ok(entry) => match entry.section("Desktop Entry").attr("Exec") {
|
||||||
|
Some(exec) => match mime_app::exec_to_command(exec, &[] as &[&str; 0]) {
|
||||||
|
Some(commands) => {
|
||||||
|
for mut command in commands {
|
||||||
|
if let Err(err) = spawn_detached(&mut command) {
|
||||||
|
log::warn!("failed to execute {:?}: {}", path, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
log::warn!("failed to parse {:?}: invalid Desktop Entry/Exec", path);
|
log::warn!("failed to parse {:?}: invalid Desktop Entry/Exec", path);
|
||||||
}
|
}
|
||||||
|
|
@ -604,87 +677,48 @@ impl App {
|
||||||
log::warn!("failed to parse {:?}: {}", path, err);
|
log::warn!("failed to parse {:?}: {}", path, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if mime == "application/x-executable" || mime == "application/vnd.appimage" {
|
|
||||||
// Try opening executable
|
|
||||||
let mut command = std::process::Command::new(path);
|
|
||||||
match spawn_detached(&mut command) {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) => match err.kind() {
|
|
||||||
io::ErrorKind::PermissionDenied => {
|
|
||||||
// If permission is denied, try marking as executable, then running
|
|
||||||
self.dialog_pages
|
|
||||||
.push_back(DialogPage::SetExecutableAndLaunch {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::warn!("failed to execute {:?}: {}", path, err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try mime apps, which should be faster than xdg-open
|
fn launch_from_mime_cache<P>(&self, mime: &Mime, paths: &[P]) -> bool
|
||||||
for app in self.mime_app_cache.get(&mime) {
|
where
|
||||||
let Some(mut command) = app.command(Some(path.clone().into())) else {
|
P: std::fmt::Debug + AsRef<Path> + AsRef<std::ffi::OsStr>,
|
||||||
|
{
|
||||||
|
for app in self.mime_app_cache.get(mime) {
|
||||||
|
let Some(commands) = app.command(paths) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
match spawn_detached(&mut command) {
|
let len = commands.len();
|
||||||
Ok(()) => {
|
|
||||||
let _ = recently_used_xbel::update_recently_used(
|
for (i, mut command) in commands.into_iter().enumerate() {
|
||||||
path,
|
if let Err(err) = spawn_detached(&mut command) {
|
||||||
App::APP_ID.to_string(),
|
// More than one command: The app doesn't support lists of paths so each command
|
||||||
"cosmic-files".to_string(),
|
// is associated with one instance
|
||||||
None,
|
//
|
||||||
);
|
// One command: Attempted to launch one app with multiple paths
|
||||||
return;
|
let path = if len > 1 {
|
||||||
}
|
format!("{:?}", paths.get(i))
|
||||||
Err(err) => {
|
} else {
|
||||||
|
format!("{paths:?}")
|
||||||
|
};
|
||||||
log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err);
|
log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// loop through subclasses if available
|
for path in paths {
|
||||||
if let Some(mime_sub_classes) = mime_icon::parent_mime_types(&mime) {
|
|
||||||
for sub_class in mime_sub_classes {
|
|
||||||
for app in self.mime_app_cache.get(&sub_class) {
|
|
||||||
let Some(mut command) = app.command(Some(path.clone().into())) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match spawn_detached(&mut command) {
|
|
||||||
Ok(()) => {
|
|
||||||
let _ = recently_used_xbel::update_recently_used(
|
|
||||||
path,
|
|
||||||
App::APP_ID.to_string(),
|
|
||||||
"cosmic-files".to_string(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to using open crate
|
|
||||||
match open::that_detached(path) {
|
|
||||||
Ok(()) => {
|
|
||||||
let _ = recently_used_xbel::update_recently_used(
|
let _ = recently_used_xbel::update_recently_used(
|
||||||
path,
|
&path.into(),
|
||||||
App::APP_ID.to_string(),
|
App::APP_ID.to_string(),
|
||||||
"cosmic-files".to_string(),
|
"cosmic-files".to_string(),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to open {:?}: {}", path, err);
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No app matched for mimes and paths
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|
@ -1769,7 +1803,7 @@ impl Application for App {
|
||||||
dialog_text_input: widget::Id::unique(),
|
dialog_text_input: widget::Id::unique(),
|
||||||
key_binds,
|
key_binds,
|
||||||
margin: HashMap::new(),
|
margin: HashMap::new(),
|
||||||
mime_app_cache: mime_app::MimeAppCache::new(),
|
mime_app_cache: MimeAppCache::new(),
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
mounter_items: HashMap::new(),
|
mounter_items: HashMap::new(),
|
||||||
network_drive_connecting: None,
|
network_drive_connecting: None,
|
||||||
|
|
@ -2307,7 +2341,9 @@ impl Application for App {
|
||||||
let all_apps = self.get_programs_for_mime(&mime);
|
let all_apps = self.get_programs_for_mime(&mime);
|
||||||
|
|
||||||
if let Some(app) = all_apps.get(selected) {
|
if let Some(app) = all_apps.get(selected) {
|
||||||
if let Some(mut command) = app.command(Some(path.clone().into())) {
|
if let Some(mut command) =
|
||||||
|
app.command(&[&path]).and_then(|v| v.into_iter().next())
|
||||||
|
{
|
||||||
match spawn_detached(&mut command) {
|
match spawn_detached(&mut command) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = recently_used_xbel::update_recently_used(
|
let _ = recently_used_xbel::update_recently_used(
|
||||||
|
|
@ -2739,18 +2775,18 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Some(mut command) = terminal.command(None) {
|
if let Some(mut command) = terminal
|
||||||
|
.command::<&str>(&[])
|
||||||
|
.and_then(|v| v.into_iter().next())
|
||||||
|
{
|
||||||
command.current_dir(&path);
|
command.current_dir(&path);
|
||||||
match spawn_detached(&mut command) {
|
if let Err(err) = spawn_detached(&mut command) {
|
||||||
Ok(()) => {}
|
log::warn!(
|
||||||
Err(err) => {
|
"failed to open {:?} with terminal {:?}: {}",
|
||||||
log::warn!(
|
path,
|
||||||
"failed to open {:?} with terminal {:?}: {}",
|
terminal.id,
|
||||||
path,
|
err
|
||||||
terminal.id,
|
)
|
||||||
err
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("failed to get command for {:?}", terminal.id);
|
log::warn!("failed to get command for {:?}", terminal.id);
|
||||||
|
|
@ -2800,12 +2836,12 @@ impl Application for App {
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let url = format!("mime:///{mime}");
|
let url = format!("mime:///{mime}");
|
||||||
if let Some(mut command) = app.command(Some(url.clone().into())) {
|
// TODO: Support multiple URLs
|
||||||
match spawn_detached(&mut command) {
|
if let Some(mut command) =
|
||||||
Ok(()) => {}
|
app.command(&[&url]).and_then(|v| v.into_iter().next())
|
||||||
Err(err) => {
|
{
|
||||||
log::warn!("failed to open {:?} with {:?}: {}", url, app.id, err)
|
if let Err(err) = spawn_detached(&mut command) {
|
||||||
}
|
log::warn!("failed to open {:?} with {:?}: {}", url, app.id, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -3329,7 +3365,7 @@ impl Application for App {
|
||||||
cosmic::action::app(Message::TabMessage(Some(entity), x))
|
cosmic::action::app(Message::TabMessage(Some(entity), x))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
tab::Command::OpenFile(path) => self.open_file(&path),
|
tab::Command::OpenFile(paths) => self.open_file(&paths),
|
||||||
tab::Command::OpenInNewTab(path) => {
|
tab::Command::OpenInNewTab(path) => {
|
||||||
commands.push(self.open_tab(Location::Path(path.clone()), false, None));
|
commands.push(self.open_tab(Location::Path(path.clone()), false, None));
|
||||||
}
|
}
|
||||||
|
|
@ -3682,9 +3718,9 @@ impl Application for App {
|
||||||
.nav_model
|
.nav_model
|
||||||
.data::<Location>(entity)
|
.data::<Location>(entity)
|
||||||
.and_then(|x| x.path_opt())
|
.and_then(|x| x.path_opt())
|
||||||
.map(|x| x.to_path_buf())
|
.map(ToOwned::to_owned)
|
||||||
{
|
{
|
||||||
self.open_file(&path);
|
self.open_file(&[path]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavMenuAction::OpenWith(entity) => {
|
NavMenuAction::OpenWith(entity) => {
|
||||||
|
|
@ -4427,7 +4463,7 @@ impl Application for App {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut column = widget::list_column();
|
let mut column = widget::list_column();
|
||||||
let available_programs = self.get_programs_for_mime(&mime);
|
let available_programs = self.get_programs_for_mime(mime);
|
||||||
let item_height = 32.0;
|
let item_height = 32.0;
|
||||||
let mut displayed_default = false;
|
let mut displayed_default = false;
|
||||||
|
|
||||||
|
|
|
||||||
129
src/mime_app.rs
129
src/mime_app.rs
|
|
@ -6,32 +6,123 @@ use cosmic::desktop;
|
||||||
use cosmic::widget;
|
use cosmic::widget;
|
||||||
pub use mime_guess::Mime;
|
pub use mime_guess::Mime;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering, collections::HashMap, env, ffi::OsString, fs, io, path::PathBuf, process,
|
cmp::Ordering,
|
||||||
|
collections::HashMap,
|
||||||
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn exec_to_command(exec: &str, path_opt: Option<OsString>) -> Option<process::Command> {
|
// Supported exec key field codes
|
||||||
let args_vec: Vec<String> = shlex::split(exec)?;
|
const EXEC_HANDLERS: [&str; 4] = ["%f", "%F", "%u", "%U"];
|
||||||
let mut args = args_vec.iter();
|
// Deprecated field codes. The spec advises to ignore these handlers.
|
||||||
let mut command = process::Command::new(args.next()?);
|
const DEPRECATED_HANDLERS: [&str; 6] = ["%d", "%D", "%n", "%N", "%v", "%m"];
|
||||||
for arg in args {
|
|
||||||
if arg.starts_with('%') {
|
pub fn exec_to_command(
|
||||||
match arg.as_str() {
|
exec: &str,
|
||||||
"%f" | "%F" | "%u" | "%U" => {
|
path_opt: &[impl AsRef<OsStr>],
|
||||||
if let Some(path) = &path_opt {
|
) -> Option<Vec<process::Command>> {
|
||||||
command.arg(path);
|
let args_vec = shlex::split(exec)?;
|
||||||
}
|
let program = args_vec.first()?;
|
||||||
|
|
||||||
|
// Base Command instance(s)
|
||||||
|
// 1. We may need to launch multiple of the same process.
|
||||||
|
// 2. Each of those processes will need to be passed args from exec.
|
||||||
|
// 3. Each of those args may appear in any order.
|
||||||
|
//
|
||||||
|
// So, we'll go through exec in two passes. The first pass handles paths (%f etc) while the
|
||||||
|
// second passes extra, non-% args to each processes.
|
||||||
|
//
|
||||||
|
// While it'd be marginally faster to process everything in one pass, that's problematic:
|
||||||
|
// 1. path_opt may need to be cloned because it may be moved on each iteration (borrowck
|
||||||
|
// doesn't know we'll only use it once)
|
||||||
|
// 2. We have to keep track of which modifier (%f etc) we've used/seen already
|
||||||
|
// 3. We have to keep track of which processes received non-modifier args which gets messy fast
|
||||||
|
// 4. `exec` is likely small so looping over it twice is not a big deal
|
||||||
|
let args_handler = args_vec
|
||||||
|
.iter()
|
||||||
|
.find(|arg| EXEC_HANDLERS.contains(&arg.as_str()));
|
||||||
|
// msrv
|
||||||
|
// .inspect(|handler| log::trace!("Found paths handler: {handler} for exec: {exec}"));
|
||||||
|
let mut processes = match args_handler.map(|s| s.as_str()) {
|
||||||
|
Some("%f") => {
|
||||||
|
let mut processes = Vec::with_capacity(path_opt.len());
|
||||||
|
|
||||||
|
for path in path_opt.iter().map(AsRef::as_ref) {
|
||||||
|
// TODO: %f and %F need to handle non-file URLs (see spec)
|
||||||
|
if from_file_or_dir(path).is_none() {
|
||||||
|
log::warn!("Desktop file expects a file path instead of a URL: {path:?}");
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
log::warn!("unsupported Exec code {:?} in {:?}", arg, exec);
|
// Passing multiple paths to %f should open an instance per path
|
||||||
return None;
|
let mut process = process::Command::new(program);
|
||||||
|
process.arg(path);
|
||||||
|
processes.push(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
processes
|
||||||
|
}
|
||||||
|
Some("%F") => {
|
||||||
|
// TODO: %f and %F need to handle non-file URLs (see spec)
|
||||||
|
for invalid in path_opt
|
||||||
|
.iter()
|
||||||
|
.map(AsRef::as_ref)
|
||||||
|
.filter(|path| from_file_or_dir(path).is_none())
|
||||||
|
{
|
||||||
|
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch one instance with all args
|
||||||
|
let mut process = process::Command::new(program);
|
||||||
|
process.args(path_opt);
|
||||||
|
|
||||||
|
vec![process]
|
||||||
|
}
|
||||||
|
Some("%u") => path_opt
|
||||||
|
.iter()
|
||||||
|
.map(|path| {
|
||||||
|
let mut process = process::Command::new(program);
|
||||||
|
process.arg(path);
|
||||||
|
process
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
Some("%U") => {
|
||||||
|
let mut process = process::Command::new(program);
|
||||||
|
process.args(path_opt);
|
||||||
|
vec![process]
|
||||||
|
}
|
||||||
|
Some(invalid) => unreachable!("All valid variants were checked; got: {invalid}"),
|
||||||
|
None => vec![process::Command::new(program)],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass 2: Add every argument that's not % to each process
|
||||||
|
for arg in args_vec.into_iter().skip(1) {
|
||||||
|
match arg.as_str() {
|
||||||
|
unsupported
|
||||||
|
if arg.starts_with('%')
|
||||||
|
&& !EXEC_HANDLERS.contains(&unsupported)
|
||||||
|
&& !DEPRECATED_HANDLERS.contains(&unsupported) =>
|
||||||
|
{
|
||||||
|
log::warn!("unsupported Exec code {:?} in {:?}", unsupported, exec);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
arg => {
|
||||||
|
for process in &mut processes {
|
||||||
|
process.arg(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
command.arg(arg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(command)
|
Some(processes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_file_or_dir(path: impl AsRef<Path>) -> Option<url::Url> {
|
||||||
|
url::Url::from_file_path(&path)
|
||||||
|
.ok()
|
||||||
|
.or_else(|| url::Url::from_directory_path(&path).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -46,7 +137,7 @@ pub struct MimeApp {
|
||||||
|
|
||||||
impl MimeApp {
|
impl MimeApp {
|
||||||
//TODO: move to libcosmic, support multiple files
|
//TODO: move to libcosmic, support multiple files
|
||||||
pub fn command(&self, path_opt: Option<OsString>) -> Option<process::Command> {
|
pub fn command<O: AsRef<OsStr>>(&self, path_opt: &[O]) -> Option<Vec<process::Command>> {
|
||||||
exec_to_command(self.exec.as_deref()?, path_opt)
|
exec_to_command(self.exec.as_deref()?, path_opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/tab.rs
23
src/tab.rs
|
|
@ -1143,7 +1143,7 @@ pub enum Command {
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
ExecEntryAction(cosmic::desktop::DesktopEntryData, usize),
|
ExecEntryAction(cosmic::desktop::DesktopEntryData, usize),
|
||||||
Iced(TaskWrapper),
|
Iced(TaskWrapper),
|
||||||
OpenFile(PathBuf),
|
OpenFile(Vec<PathBuf>),
|
||||||
OpenInNewTab(PathBuf),
|
OpenInNewTab(PathBuf),
|
||||||
OpenInNewWindow(PathBuf),
|
OpenInNewWindow(PathBuf),
|
||||||
OpenTrash,
|
OpenTrash,
|
||||||
|
|
@ -2361,7 +2361,7 @@ impl Tab {
|
||||||
if clicked_item.metadata.is_dir() {
|
if clicked_item.metadata.is_dir() {
|
||||||
cd = Some(location.clone());
|
cd = Some(location.clone());
|
||||||
} else if let Some(path) = location.path_opt() {
|
} else if let Some(path) = location.path_opt() {
|
||||||
commands.push(Command::OpenFile(path.to_path_buf()));
|
commands.push(Command::OpenFile(vec![path.to_path_buf()]));
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no path for item {:?}", clicked_item);
|
log::warn!("no path for item {:?}", clicked_item);
|
||||||
}
|
}
|
||||||
|
|
@ -2467,6 +2467,7 @@ impl Tab {
|
||||||
.any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected)
|
.any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected)
|
||||||
});
|
});
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
let mut paths_to_open = vec![];
|
||||||
for (i, item) in items.iter_mut().enumerate() {
|
for (i, item) in items.iter_mut().enumerate() {
|
||||||
if Some(i) == click_i_opt {
|
if Some(i) == click_i_opt {
|
||||||
// Single click to open.
|
// Single click to open.
|
||||||
|
|
@ -2475,7 +2476,7 @@ impl Tab {
|
||||||
if item.metadata.is_dir() {
|
if item.metadata.is_dir() {
|
||||||
cd = Some(location.clone());
|
cd = Some(location.clone());
|
||||||
} else if let Some(path) = location.path_opt() {
|
} else if let Some(path) = location.path_opt() {
|
||||||
commands.push(Command::OpenFile(path.to_path_buf()));
|
paths_to_open.push(path.to_path_buf());
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no path for item {:?}", item);
|
log::warn!("no path for item {:?}", item);
|
||||||
}
|
}
|
||||||
|
|
@ -2508,6 +2509,9 @@ impl Tab {
|
||||||
item.selected = false;
|
item.selected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !paths_to_open.is_empty() {
|
||||||
|
commands.push(Command::OpenFile(paths_to_open));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2897,7 +2901,7 @@ impl Tab {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
cd = Some(location);
|
cd = Some(location);
|
||||||
} else {
|
} else {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
commands.push(Command::OpenFile(vec![path.clone()]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -2920,11 +2924,12 @@ impl Tab {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
cd = Some(Location::Path(path));
|
cd = Some(Location::Path(path));
|
||||||
} else {
|
} else {
|
||||||
commands.push(Command::OpenFile(path));
|
commands.push(Command::OpenFile(vec![path]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
let mut open_files = Vec::new();
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(location) = &item.location_opt {
|
if let Some(location) = &item.location_opt {
|
||||||
|
|
@ -2932,13 +2937,15 @@ impl Tab {
|
||||||
//TODO: allow opening multiple tabs?
|
//TODO: allow opening multiple tabs?
|
||||||
cd = Some(location.clone());
|
cd = Some(location.clone());
|
||||||
} else if let Some(path) = location.path_opt() {
|
} else if let Some(path) = location.path_opt() {
|
||||||
commands.push(Command::OpenFile(path.to_path_buf()));
|
open_files.push(path.to_path_buf());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//TODO: open properties?
|
//TODO: open properties?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commands.push(Command::OpenFile(open_files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2978,7 +2985,7 @@ impl Tab {
|
||||||
//cd = Some(Location::Path(path.clone()));
|
//cd = Some(Location::Path(path.clone()));
|
||||||
commands.push(Command::OpenInNewTab(path.clone()))
|
commands.push(Command::OpenInNewTab(path.clone()))
|
||||||
} else {
|
} else {
|
||||||
commands.push(Command::OpenFile(path.clone()));
|
commands.push(Command::OpenFile(vec![path.clone()]));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::warn!("no path for item {:?}", clicked_item);
|
log::warn!("no path for item {:?}", clicked_item);
|
||||||
|
|
@ -3310,7 +3317,7 @@ impl Tab {
|
||||||
if matches!(self.mode, Mode::Desktop) {
|
if matches!(self.mode, Mode::Desktop) {
|
||||||
match location {
|
match location {
|
||||||
Location::Path(path) => {
|
Location::Path(path) => {
|
||||||
commands.push(Command::OpenFile(path));
|
commands.push(Command::OpenFile(vec![path]));
|
||||||
}
|
}
|
||||||
Location::Trash => {
|
Location::Trash => {
|
||||||
commands.push(Command::OpenTrash);
|
commands.push(Command::OpenTrash);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue