diff --git a/src/app.rs b/src/app.rs index d30c6004..37dc4a98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -97,6 +97,7 @@ pub enum Action { EditHistory, EditLocation, EmptyTrash, + ExecEntryAction(usize), ExtractHere, Gallery, HistoryNext, @@ -158,6 +159,9 @@ impl Action { } Action::EmptyTrash => Message::TabMessage(None, tab::Message::EmptyTrash), Action::ExtractHere => Message::ExtractHere(entity_opt), + Action::ExecEntryAction(action) => { + Message::TabMessage(entity_opt, tab::Message::ExecEntryAction(None, *action)) + } Action::Gallery => Message::TabMessage(entity_opt, tab::Message::GalleryToggle), Action::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext), Action::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious), @@ -614,6 +618,30 @@ impl App { } } + fn exec_entry_action(entry: cosmic::desktop::DesktopEntryData, action: usize) { + if let Some(action) = entry.desktop_actions.get(action) { + // Largely copied from COSMIC app library + let mut exec = shlex::Shlex::new(&action.exec); + match exec.next() { + Some(cmd) if !cmd.contains('=') => { + let mut proc = tokio::process::Command::new(cmd); + for arg in exec { + if !arg.starts_with('%') { + proc.arg(arg); + } + } + let _ = proc.spawn(); + } + _ => (), + } + } else { + log::warn!( + "Invalid actions index `{action}` for desktop entry {}", + entry.name + ); + } + } + fn open_tab_entity( &mut self, location: Location, @@ -2639,6 +2667,9 @@ impl Application for App { tab::Command::EmptyTrash => { self.dialog_pages.push_back(DialogPage::EmptyTrash); } + tab::Command::ExecEntryAction(entry, action) => { + App::exec_entry_action(entry, action); + } tab::Command::Iced(iced_command) => { commands.push(iced_command.0.map(move |tab_message| { message::app(Message::TabMessage(Some(entity), tab_message)) diff --git a/src/menu.rs b/src/menu.rs index 1649c50d..86fcb739 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -10,6 +10,7 @@ use cosmic::{ }, Element, }; +use i18n_embed::LanguageLoader; use mime_guess::Mime; use std::collections::HashMap; @@ -86,6 +87,7 @@ pub fn context_menu<'a>( let mut selected_dir = 0; let mut selected = 0; let mut selected_trash_only = false; + let mut selected_desktop_entry = None; let mut selected_types: Vec = vec![]; tab.items_opt().map(|items| { for item in items.iter() { @@ -94,8 +96,16 @@ pub fn context_menu<'a>( if item.metadata.is_dir() { selected_dir += 1; } - if item.location_opt == Some(Location::Trash) { - selected_trash_only = true; + match &item.location_opt { + Some(Location::Trash) => selected_trash_only = true, + Some(Location::Path(path)) => { + if selected == 1 + && path.extension().and_then(|s| s.to_str()) == Some("desktop") + { + selected_desktop_entry = Some(&**path); + } + } + _ => (), } selected_types.push(item.mime.clone()); } @@ -104,6 +114,17 @@ pub fn context_menu<'a>( selected_types.sort_unstable(); selected_types.dedup(); selected_trash_only = selected_trash_only && selected == 1; + // Parse the desktop entry if it is the only selection + let selected_desktop_entry = selected_desktop_entry.and_then(|path| { + if selected == 1 { + let lang_id = crate::localize::LANGUAGE_LOADER.current_language(); + let language = lang_id.language.as_str(); + // Cache? + cosmic::desktop::load_desktop_file(Some(language), path) + } else { + None + } + }); let mut children: Vec> = Vec::new(); match (&tab.mode, &tab.location) { @@ -116,6 +137,17 @@ pub fn context_menu<'a>( if tab::trash_entries() > 0 { children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); } + } else if let Some(entry) = selected_desktop_entry { + children.push(menu_item(fl!("open"), Action::Open).into()); + for (i, action) in entry.desktop_actions.into_iter().enumerate() { + children.push(menu_item(action.name, Action::ExecEntryAction(i)).into()) + } + children.push(divider::horizontal::light().into()); + children.push(menu_item(fl!("rename"), Action::Rename).into()); + children.push(menu_item(fl!("cut"), Action::Cut).into()); + children.push(menu_item(fl!("copy"), Action::Copy).into()); + // Should this simply bypass trash and remove the shortcut? + children.push(menu_item(fl!("move-to-trash"), Action::MoveToTrash).into()); } else if selected > 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 { children.push(menu_item(fl!("open"), Action::Open).into()); diff --git a/src/tab.rs b/src/tab.rs index 64f0c4c4..c2d01432 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -38,6 +38,7 @@ use cosmic::{ }; use chrono::{DateTime, Utc}; +use i18n_embed::LanguageLoader; use mime_guess::{mime, Mime}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -1005,6 +1006,7 @@ pub enum Command { ChangeLocation(String, Location, Option), DropFiles(PathBuf, ClipboardPaste), EmptyTrash, + ExecEntryAction(cosmic::desktop::DesktopEntryData, usize), Iced(TaskWrapper), MoveToTrash(Vec), OpenFile(PathBuf), @@ -1034,6 +1036,7 @@ pub enum Message { EditLocationEnable, OpenInNewTab(PathBuf), EmptyTrash, + ExecEntryAction(Option, usize), Gallery(bool), GalleryPrevious, GalleryNext, @@ -2312,6 +2315,24 @@ impl Tab { Message::EmptyTrash => { commands.push(Command::EmptyTrash); } + Message::ExecEntryAction(path, action) => { + let lang_id = crate::localize::LANGUAGE_LOADER.current_language(); + let language = lang_id.language.as_str(); + match path.map_or_else( + || { + let items = self.items_opt.as_deref()?; + items.iter().find(|item| item.selected).and_then(|item| { + let location = item.location_opt.as_ref()?; + let path = location.path_opt()?; + cosmic::desktop::load_desktop_file(Some(language), path) + }) + }, + |path| cosmic::desktop::load_desktop_file(Some(language), path), + ) { + Some(entry) => commands.push(Command::ExecEntryAction(entry, action)), + None => log::warn!("Invalid desktop entry path passed to ExecEntryAction"), + } + } Message::Gallery(gallery) => { self.gallery = gallery; }