From e3226d8dc2c256c4e5d7ff79e12f20c46bb255f7 Mon Sep 17 00:00:00 2001 From: ellieplayswow <164806095+ellieplayswow@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:20:31 +0000 Subject: [PATCH 1/4] Adding in new functionality to open / open-with based off of MIME subclasses where needed --- src/app.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++- src/mime_icon.rs | 6 +++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index e469bce..5f02aaa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use cosmic::iced::mouse::Event::CursorMoved; #[cfg(feature = "wayland")] use cosmic::iced::{ event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, @@ -57,6 +56,8 @@ use std::{ sync::{Arc, Mutex}, time::{self, Instant}, }; +use cosmic::iced::mouse::Event::CursorMoved; +use cosmic::widget::{Button, ListColumn}; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(feature = "wayland")] @@ -75,6 +76,7 @@ use crate::{ spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, }; +use crate::mime_app::MimeApp; #[derive(Clone, Debug)] pub enum Mode { @@ -560,6 +562,7 @@ pub struct App { impl App { fn open_file(&mut self, path: &PathBuf) { let mime = mime_icon::mime_for_path(path); + if mime == "application/x-desktop" { // Try opening desktop application match freedesktop_entry_parser::parse_entry(path) { @@ -627,6 +630,31 @@ impl App { } } + // loop through subclasses if available + 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(()) => { @@ -2187,7 +2215,8 @@ impl Application for App { selected, .. } => { - if let Some(app) = self.mime_app_cache.get(&mime).get(selected) { + let direct_apps = self.mime_app_cache.get(&mime); + if let Some(app) = direct_apps.get(selected) { if let Some(mut command) = app.command(Some(path.clone().into())) { match spawn_detached(&mut command) { Ok(()) => { @@ -2215,6 +2244,46 @@ impl Application for App { ); } } + + let mut sub_class_index = selected - direct_apps.len(); + if let Some(sub_classes) = mime_icon::parent_mime_types(&mime) { + for sub_class in sub_classes { + let sub_class_apps = self.mime_app_cache.get(&sub_class); + if let Some(app) = sub_class_apps.get(sub_class_index) { + if let Some(mut command) = app.command(Some(path.clone().into())) { + match spawn_detached(&mut command) { + 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 {:?} with {:?}: {}", + path, + app.id, + err + ) + } + } + } else { + log::warn!( + "failed to open {:?} with {:?}: failed to get command", + path, + app.id + ); + } + } + else { + sub_class_index -= sub_class_apps.len(); + } + + } + } + } DialogPage::RenameItem { from, parent, name, .. @@ -4229,6 +4298,7 @@ impl Application for App { }; let mut column = widget::list_column(); + let mut open_index = 0; for (i, app) in self.mime_app_cache.get(mime).iter().enumerate() { column = column.add( widget::button::custom( @@ -4260,8 +4330,48 @@ impl Application for App { .class(theme::Button::MenuItem) .on_press(Message::OpenWithSelection(i)), ); + open_index += 1; } + if let Some(sub_classes) = mime_icon::parent_mime_types(&mime) { + for sub_class in sub_classes { + for (i, app) in self.mime_app_cache.get(&sub_class).iter().enumerate() { + column = column.add( + widget::button::custom( + widget::row::with_children(vec![ + widget::icon(app.icon.clone()).size(32).into(), + if app.is_default { + widget::text::body(fl!( + "default-app", + name = Some(app.name.as_str()) + )) + .into() + } else { + widget::text::body(app.name.to_string()).into() + }, + widget::horizontal_space().into(), + if *selected == open_index { + widget::icon::from_name("checkbox-checked-symbolic") + .size(16) + .into() + } else { + widget::Space::with_width(Length::Fixed(16.0)).into() + }, + ]) + .spacing(space_s) + .height(Length::Fixed(32.0)) + .align_y(Alignment::Center), + ) + .width(Length::Fill) + .class(theme::Button::MenuItem) + .on_press(Message::OpenWithSelection(open_index)), + ); + open_index += 1; + } + } + } + + let mut dialog = widget::dialog() .title(fl!("open-with-title", name = name)) .primary_action( diff --git a/src/mime_icon.rs b/src/mime_icon.rs index a50ff8a..9be98b6 100644 --- a/src/mime_icon.rs +++ b/src/mime_icon.rs @@ -73,3 +73,9 @@ pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle { None => icon::from_name(FALLBACK_MIME_ICON).size(size).handle(), } } + +pub fn parent_mime_types(mime: &Mime) -> Option> { + let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); + + mime_icon_cache.shared_mime_info.get_parents_aliased(mime) +} \ No newline at end of file From b288cd058174d36830c02d316ba9ecafebadeb19 Mon Sep 17 00:00:00 2001 From: ellieplayswow <164806095+ellieplayswow@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:03:42 +0000 Subject: [PATCH 2/4] simplifying list generation for mime types, applying xdg-mime patch & cargo fmt --- Cargo.lock | 3 +- Cargo.toml | 3 +- src/app.rs | 123 ++++++++++++++--------------------------------- src/mime_icon.rs | 2 +- 4 files changed, 41 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b36c014..4938df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7546,8 +7546,7 @@ dependencies = [ [[package]] name = "xdg-mime" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58d325d0ca93fb1984a56eb926f019acfc67bd2ec559b0dbf09cafcc92e81ec9" +source = "git+https://github.com/ellieplayswow/xdg-mime-rs?branch=feature/get-same-as#4f8d07ceedabbe58368a8e7f5547232490860790" dependencies = [ "dirs-next", "glob", diff --git a/Cargo.toml b/Cargo.toml index 7392f56..8733df5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ url = "2.5" walkdir = "2.5.0" wayland-client = { version = "0.31.8", optional = true } xdg = { version = "2.5.2", optional = true } -xdg-mime = "0.4" +xdg-mime = "0.4.0" # Compression bzip2 = { version = "0.5", optional = true } #TODO: replace with pure Rust crate flate2 = "1.0" @@ -96,6 +96,7 @@ tokio = { version = "1", features = ["rt", "macros"] } [patch.crates-io] # https://github.com/alexcrichton/filetime/pull/104 filetime = { git = "https://github.com/jackpot51/filetime" } +xdg-mime = { git = "https://github.com/ellieplayswow/xdg-mime-rs", branch = "feature/get-same-as" } # [patch.'https://github.com/pop-os/cosmic-text'] # cosmic-text = { path = "../cosmic-text" } diff --git a/src/app.rs b/src/app.rs index 5f02aaa..9e916b2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use cosmic::iced::{ }; #[cfg(feature = "wayland")] use cosmic::iced_winit::commands::overlap_notify::overlap_notify; +use cosmic::widget::{Button, ListColumn}; use cosmic::{ app::{self, context_drawer, message, Core, Task}, cosmic_config, cosmic_theme, executor, @@ -40,6 +41,7 @@ use cosmic::{ }, Application, ApplicationExt, Element, }; +use mime_guess::Mime; use notify_debouncer_full::{ new_debouncer, notify::{self, RecommendedWatcher, Watcher}, @@ -57,12 +59,12 @@ use std::{ time::{self, Instant}, }; use cosmic::iced::mouse::Event::CursorMoved; -use cosmic::widget::{Button, ListColumn}; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(feature = "wayland")] use wayland_client::{protocol::wl_output::WlOutput, Proxy}; +use crate::mime_app::MimeApp; use crate::operation::{OperationError, OperationErrorType}; use crate::{ clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, @@ -76,7 +78,6 @@ use crate::{ spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, }; -use crate::mime_app::MimeApp; #[derive(Clone, Debug)] pub enum Mode { @@ -1595,6 +1596,36 @@ impl App { .into() } + fn get_programs_for_mime(&self, mime_type: &Mime) -> Vec<&MimeApp> { + let mut results = Vec::new(); + + let mut dedupe = HashSet::new(); + + // start with exact matches + for mime_app in self.mime_app_cache.get(mime_type) { + let app_id = &mime_app.id; + if !dedupe.contains(app_id) { + results.push(mime_app); + dedupe.insert(app_id); + } + } + + // grab matches based off of subclass / parent mime type + if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) { + for parent_type in parent_types { + for mime_app in self.mime_app_cache.get(&parent_type) { + let app_id = &mime_app.id; + if !dedupe.contains(app_id) { + results.push(mime_app); + dedupe.insert(app_id); + } + } + } + } + + results + } + // Update favorites based on renaming or moving dirs. fn update_favorites(&mut self, path_changes: &[(PathBuf, PathBuf)]) -> bool { let mut favorites_changed = false; @@ -2215,8 +2246,9 @@ impl Application for App { selected, .. } => { - let direct_apps = self.mime_app_cache.get(&mime); - if let Some(app) = direct_apps.get(selected) { + let all_apps = self.get_programs_for_mime(&mime); + + if let Some(app) = all_apps.get(selected) { if let Some(mut command) = app.command(Some(path.clone().into())) { match spawn_detached(&mut command) { Ok(()) => { @@ -2244,46 +2276,6 @@ impl Application for App { ); } } - - let mut sub_class_index = selected - direct_apps.len(); - if let Some(sub_classes) = mime_icon::parent_mime_types(&mime) { - for sub_class in sub_classes { - let sub_class_apps = self.mime_app_cache.get(&sub_class); - if let Some(app) = sub_class_apps.get(sub_class_index) { - if let Some(mut command) = app.command(Some(path.clone().into())) { - match spawn_detached(&mut command) { - 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 {:?} with {:?}: {}", - path, - app.id, - err - ) - } - } - } else { - log::warn!( - "failed to open {:?} with {:?}: failed to get command", - path, - app.id - ); - } - } - else { - sub_class_index -= sub_class_apps.len(); - } - - } - } - } DialogPage::RenameItem { from, parent, name, .. @@ -4298,8 +4290,7 @@ impl Application for App { }; let mut column = widget::list_column(); - let mut open_index = 0; - for (i, app) in self.mime_app_cache.get(mime).iter().enumerate() { + for (i, app) in self.get_programs_for_mime(&mime).iter().enumerate() { column = column.add( widget::button::custom( widget::row::with_children(vec![ @@ -4330,48 +4321,8 @@ impl Application for App { .class(theme::Button::MenuItem) .on_press(Message::OpenWithSelection(i)), ); - open_index += 1; } - if let Some(sub_classes) = mime_icon::parent_mime_types(&mime) { - for sub_class in sub_classes { - for (i, app) in self.mime_app_cache.get(&sub_class).iter().enumerate() { - column = column.add( - widget::button::custom( - widget::row::with_children(vec![ - widget::icon(app.icon.clone()).size(32).into(), - if app.is_default { - widget::text::body(fl!( - "default-app", - name = Some(app.name.as_str()) - )) - .into() - } else { - widget::text::body(app.name.to_string()).into() - }, - widget::horizontal_space().into(), - if *selected == open_index { - widget::icon::from_name("checkbox-checked-symbolic") - .size(16) - .into() - } else { - widget::Space::with_width(Length::Fixed(16.0)).into() - }, - ]) - .spacing(space_s) - .height(Length::Fixed(32.0)) - .align_y(Alignment::Center), - ) - .width(Length::Fill) - .class(theme::Button::MenuItem) - .on_press(Message::OpenWithSelection(open_index)), - ); - open_index += 1; - } - } - } - - let mut dialog = widget::dialog() .title(fl!("open-with-title", name = name)) .primary_action( diff --git a/src/mime_icon.rs b/src/mime_icon.rs index 9be98b6..c776dc8 100644 --- a/src/mime_icon.rs +++ b/src/mime_icon.rs @@ -78,4 +78,4 @@ pub fn parent_mime_types(mime: &Mime) -> Option> { let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); mime_icon_cache.shared_mime_info.get_parents_aliased(mime) -} \ No newline at end of file +} From 6ec37975dea3334a98e01dc7dbf54586086ff4c1 Mon Sep 17 00:00:00 2001 From: ellieplayswow <164806095+ellieplayswow@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:14:47 +0000 Subject: [PATCH 3/4] cargo fmt --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 9e916b2..7fd1792 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only +use cosmic::iced::mouse::Event::CursorMoved; #[cfg(feature = "wayland")] use cosmic::iced::{ event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, @@ -58,7 +59,6 @@ use std::{ sync::{Arc, Mutex}, time::{self, Instant}, }; -use cosmic::iced::mouse::Event::CursorMoved; use tokio::sync::mpsc; use trash::TrashItem; #[cfg(feature = "wayland")] From 082ffbc0c4136a443e44511b1e8a31fecdfe24b5 Mon Sep 17 00:00:00 2001 From: ellieplayswow <164806095+ellieplayswow@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:19:07 +0000 Subject: [PATCH 4/4] adding note for xdg-mime patch --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8733df5..d9765ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ tokio = { version = "1", features = ["rt", "macros"] } [patch.crates-io] # https://github.com/alexcrichton/filetime/pull/104 filetime = { git = "https://github.com/jackpot51/filetime" } +# https://github.com/ebassi/xdg-mime-rs/pull/31 xdg-mime = { git = "https://github.com/ellieplayswow/xdg-mime-rs", branch = "feature/get-same-as" } # [patch.'https://github.com/pop-os/cosmic-text']