From 0f60ffa7c26c5e541cb963a4fc2820fba253c54c Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Tue, 29 Oct 2024 03:18:05 -0400 Subject: [PATCH] Improved desktop entry menu Currently, desktop entries display a generic menu with items that aren't relevant to apps. This patch improves the menu by removing the unneeded items and listing desktop specific entries such as actions. For example, right clicking a Firefox desktop entry will show the action to open a private tab. Should compose well with pop-os/cosmic-applibrary#179 --- src/app.rs | 31 +++++++++++++++++++++++++++++++ src/menu.rs | 36 ++++++++++++++++++++++++++++++++++-- src/tab.rs | 21 +++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index d30c600..37dc4a9 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 1649c50..86fcb73 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 64f0c4c..c2d0143 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; }