diff --git a/Cargo.lock b/Cargo.lock index a9b7a26..5607e70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1127,7 +1127,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1144,7 +1144,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "quote", "syn 1.0.109", @@ -1159,6 +1159,7 @@ dependencies = [ "env_logger", "fastrand 2.0.1", "fork", + "freedesktop_entry_parser", "i18n-embed", "i18n-embed-fl", "image 0.24.9", @@ -1179,6 +1180,7 @@ dependencies = [ "tokio", "trash", "vergen", + "xdg", ] [[package]] @@ -1206,7 +1208,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "almost", "cosmic-config", @@ -2070,6 +2072,16 @@ dependencies = [ "xdg", ] +[[package]] +name = "freedesktop_entry_parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +dependencies = [ + "nom 7.1.3", + "thiserror", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2650,7 +2662,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_accessibility", "iced_core", @@ -2665,7 +2677,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "accesskit", "accesskit_winit", @@ -2674,7 +2686,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "bitflags 1.3.2", "log", @@ -2691,7 +2703,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "futures", "iced_core", @@ -2704,7 +2716,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2728,7 +2740,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2740,7 +2752,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_core", "iced_futures", @@ -2750,7 +2762,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_core", "once_cell", @@ -2760,7 +2772,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "bytemuck", "cosmic-text", @@ -2777,7 +2789,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2796,7 +2808,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_renderer", "iced_runtime", @@ -2810,7 +2822,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "iced_graphics", "iced_runtime", @@ -3154,7 +3166,7 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#268b47d0002a5daa4cc50b124223737afe9a1dc5" +source = "git+https://github.com/pop-os/libcosmic.git#ce45af20f8f1d7c803b20c8af37fdd3adac641e2" dependencies = [ "apply", "ashpd", @@ -3439,6 +3451,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.3.7" @@ -3483,9 +3501,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -3673,6 +3691,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "6.1.1" @@ -3687,7 +3715,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio 0.8.10", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -5406,7 +5434,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio 0.8.10", + "mio 0.8.11", "num_cpus", "pin-project-lite", "signal-hook-registry", @@ -6654,7 +6682,7 @@ dependencies = [ "dirs-next", "glob", "mime", - "nom", + "nom 5.1.3", "unicase", ] diff --git a/Cargo.toml b/Cargo.toml index f83fcc5..4f539a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ vergen = { version = "8", features = ["git", "gitcl"] } chrono = { version = "0.4", features = ["unstable-locales"] } dirs = "5.0.1" env_logger = "0.11" +freedesktop_entry_parser = { version = "1.3", optional = true } image = "0.24" once_cell = "1.19" open = "5.0.2" @@ -24,6 +25,7 @@ paste = "1.0" serde = { version = "1", features = ["serde_derive"] } tokio = { version = "1" } trash = "3.2.0" +xdg = { version = "2.5.2", optional = true } # Internationalization i18n-embed = { version = "0.14", features = [ "fluent-system", @@ -46,8 +48,8 @@ features = ["serde"] git = "https://github.com/jackpot51/systemicons" [features] -default = ["xdg", "wgpu"] -xdg = ["libcosmic/desktop"] +default = ["desktop", "wgpu"] +desktop = ["libcosmic/desktop", "dep:freedesktop_entry_parser", "dep:xdg"] wgpu = ["libcosmic/wgpu"] [profile.release-with-debug] diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 8c2b7ac..06384f7 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -71,6 +71,7 @@ light = Light # Context menu new-file = New file new-folder = New folder +open-with = Open with move-to-trash = Move to trash restore-from-trash = Restore from trash diff --git a/justfile b/justfile index 9bd3c14..0cb63b5 100644 --- a/justfile +++ b/justfile @@ -52,7 +52,6 @@ check-json: (check '--message-format=json') # Developer target dev *args: cargo fmt - cargo test just run {{args}} # Run with debug logs @@ -60,6 +59,10 @@ run *args: cargo build --release env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full target/release/cosmic-files {{args}} +# Run tests +test *args: + cargo test {{args}} + # Installs files install: install -Dm0755 {{bin-src}} {{bin-dst}} diff --git a/src/app.rs b/src/app.rs index 9f0fa9e..f7f7141 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,6 +58,7 @@ pub enum Action { NewFile, NewFolder, Open, + OpenWith, Operations, Paste, Properties, @@ -94,6 +95,7 @@ impl Action { Action::NewFile => Message::NewItem(entity_opt, false), Action::NewFolder => Message::NewItem(entity_opt, true), Action::Open => Message::TabMessage(entity_opt, tab::Message::Open), + Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith), Action::Operations => Message::ToggleContextPage(ContextPage::Operations), Action::Paste => Message::Paste(entity_opt), Action::Properties => Message::ToggleContextPage(ContextPage::Properties), @@ -159,6 +161,7 @@ pub enum Message { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ContextPage { About, + OpenWith, Operations, Properties, Settings, @@ -168,6 +171,7 @@ impl ContextPage { fn title(&self) -> String { match self { Self::About => String::new(), + Self::OpenWith => fl!("open-with"), Self::Operations => fl!("operations"), Self::Properties => fl!("properties"), Self::Settings => fl!("settings"), @@ -380,6 +384,24 @@ impl App { .into() } + fn open_with(&self) -> Element { + let mut children = Vec::new(); + let entity = self.tab_model.active(); + if let Some(tab) = self.tab_model.data::(entity) { + if let Some(items) = tab.items_opt() { + for item in items.iter() { + if item.selected { + children.push(item.open_with_view(tab.config.icon_sizes)); + // Only show one property view to avoid issues like hangs when generating + // preview images on thousands of files + break; + } + } + } + } + widget::settings::view_column(children).into() + } + fn operations(&self) -> Element { let mut children = Vec::new(); @@ -1101,6 +1123,7 @@ impl Application for App { Some(match self.context_page { ContextPage::About => self.about(), + ContextPage::OpenWith => self.open_with(), ContextPage::Operations => self.operations(), ContextPage::Properties => self.properties(), ContextPage::Settings => self.settings(), diff --git a/src/lib.rs b/src/lib.rs index 9e236ef..a76c206 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,12 +16,11 @@ pub mod dialog; mod key_bind; mod localize; mod menu; +mod mime_app; mod mime_icon; mod mouse_area; mod operation; mod tab; -#[cfg(feature = "xdg")] -mod xdg; pub fn home_dir() -> PathBuf { match dirs::home_dir() { diff --git a/src/menu.rs b/src/menu.rs index e71fddc..e7ae618 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -66,7 +66,9 @@ pub fn context_menu<'a>( Location::Path(_) => { if selected > 0 { children.push(menu_item(fl!("open"), Action::Open).into()); - //TODO: Open with + if selected == 1 { + children.push(menu_item(fl!("open-with"), Action::OpenWith).into()); + } children.push(horizontal_rule(1).into()); children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("cut"), Action::Cut).into()); diff --git a/src/mime_app.rs b/src/mime_app.rs new file mode 100644 index 0000000..7062351 --- /dev/null +++ b/src/mime_app.rs @@ -0,0 +1,204 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +#[cfg(feature = "desktop")] +use cosmic::desktop; +use cosmic::widget; +pub use mime_guess::Mime; +use once_cell::sync::Lazy; +use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Instant}; + +#[derive(Clone, Debug)] +pub struct MimeApp { + pub id: String, + pub path: Option, + pub name: String, + pub exec: Option, + pub icon: widget::icon::Handle, + pub is_default: bool, +} + +#[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, + } + } +} + +#[cfg(feature = "desktop")] +fn filename_eq(path_opt: &Option, filename: &str) -> bool { + path_opt + .as_ref() + .and_then(|path| path.file_name()) + .map(|x| x == filename) + .unwrap_or(false) +} + +pub struct MimeAppCache { + cache: HashMap>, +} + +impl MimeAppCache { + pub fn new() -> Self { + let mut mime_app_cache = Self { + cache: HashMap::new(), + }; + mime_app_cache.reload(); + mime_app_cache + } + + #[cfg(not(feature = "desktop"))] + pub fn reload(&mut self) {} + + // Only available when using desktop feature of libcosmic, which only works on Unix-likes + #[cfg(feature = "desktop")] + pub fn reload(&mut self) { + let start = Instant::now(); + + self.cache.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)); + } + } + } + + // Load mimeapps.list files + // https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html + //TODO: support lookup by desktop (colon separated list in $XDG_CURRENT_DESKTOP, converted to lowercase) + 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 path in xdg_dirs.find_config_files("mimeapps.list") { + mimeapps_paths.push(path); + } + } + Err(err) => { + log::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) => { + log::warn!("failed to parse {:?}: {}", path, err); + continue; + } + }; + + for attr in entry.section("Added Associations").attrs() { + if let Ok(mime) = attr.name.parse::() { + if let Some(filenames) = attr.value { + for filename in filenames.split_terminator(';') { + println!("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 { + log::warn!("failed to find application {:?}", filename); + } + } + } + } + } + } + + for attr in entry.section("Removed Associations").attrs() { + if let Ok(mime) = attr.name.parse::() { + if let Some(filenames) = attr.value { + for filename in filenames.split_terminator(';') { + println!("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::() { + if let Some(filenames) = attr.value { + for filename in filenames.split_terminator(';') { + println!("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 { + log::warn!("failed to find application {:?}", filename); + } + } + } + } + } + } + } + + // Sort apps by name + for apps in self.cache.values_mut() { + apps.sort_by(|a, b| lexical_sort::natural_lexical_cmp(&a.name, &b.name)); + } + + let elapsed = start.elapsed(); + log::info!("loaded mime app cache in {:?}", elapsed); + } + + pub fn get(&self, key: &Mime) -> Vec { + self.cache + .get(&key) + .map_or_else(|| Vec::new(), |x| x.clone()) + } +} + +static MIME_APP_CACHE: Lazy> = Lazy::new(|| Mutex::new(MimeAppCache::new())); + +pub fn mime_apps(mime: &Mime) -> Vec { + let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); + mime_app_cache.get(mime) +} diff --git a/src/tab.rs b/src/tab.rs index 9050738..1f6e30a 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -20,20 +20,17 @@ use cosmic::{ }, theme, widget, Element, }; -use mime_guess::MimeGuess; +use mime_guess::{mime, Mime, MimeGuess}; use once_cell::sync::Lazy; use std::{ cell::Cell, cmp::Ordering, collections::HashMap, - fmt, fs::{self, Metadata}, path::PathBuf, time::{Duration, Instant}, }; -#[cfg(feature = "xdg")] -use crate::xdg::{mime_apps, DesktopEntryData}; use crate::{ app::Action, config::{IconSizes, TabConfig}, @@ -41,6 +38,7 @@ use crate::{ fl, key_bind::KeyBind, menu, + mime_app::{mime_apps, MimeApp}, mime_icon::mime_icon, mouse_area, }; @@ -224,23 +222,39 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { let path = entry.path(); - let mime_guess = MimeGuess::from_path(&path); - - let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = + let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if metadata.is_dir() { ( + //TODO: make this a static + "inode/directory".parse().unwrap(), folder_icon(&path, sizes.grid()), folder_icon(&path, sizes.list()), folder_icon(&path, sizes.list_condensed()), ) } else { ( + //TODO: best fallback mime for files? + MimeGuess::from_path(&path).first_or_octet_stream(), mime_icon(&path, sizes.grid()), mime_icon(&path, sizes.list()), mime_icon(&path, sizes.list_condensed()), ) }; + let mut open_with = mime_apps(&mime); + if open_with.is_empty() { + //TODO: more fallbacks + if mime.type_() == "text" { + open_with = mime_apps(&mime::TEXT_PLAIN); + } + } + + let thumbnail_res_opt = if mime.type_() == "image" { + None + } else { + Some(Err(())) + }; + let children = if metadata.is_dir() { //TODO: calculate children in the background (and make it cancellable?) match fs::read_dir(&path) { @@ -259,19 +273,12 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { metadata: ItemMetadata::Path { metadata, children }, hidden, path, - mime_guess, + mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - #[cfg(feature = "xdg")] - open_with: mime_guess - .first() - .map(|mime| mime_apps(&mime)) - .unwrap_or_default(), - thumbnail_res_opt: match mime_guess.first() { - Some(mime) if mime.type_() == "image" => None, - _ => Some(Err(())), - }, + open_with, + thumbnail_res_opt, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), @@ -333,16 +340,18 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { let path = entry.original_path(); let name = entry.name.clone(); - let mime_guess = MimeGuess::from_path(&path); - - let (icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = + let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size { trash::TrashItemSize::Entries(_) => ( + //TODO: make this a static + "inode/directory".parse().unwrap(), folder_icon(&path, sizes.grid()), folder_icon(&path, sizes.list()), folder_icon(&path, sizes.list_condensed()), ), trash::TrashItemSize::Bytes(_) => ( + //TODO: best fallback mime for files? + MimeGuess::from_path(&path).first_or_octet_stream(), mime_icon(&path, sizes.grid()), mime_icon(&path, sizes.list()), mime_icon(&path, sizes.list_condensed()), @@ -354,11 +363,10 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { metadata: ItemMetadata::Trash { metadata, entry }, hidden: false, path, - mime_guess, + mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - #[cfg(feature = "xdg")] open_with: Vec::new(), thumbnail_res_opt: Some(Err(())), button_id: widget::Id::unique(), @@ -457,18 +465,17 @@ impl ItemMetadata { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Item { pub name: String, pub metadata: ItemMetadata, pub hidden: bool, pub path: PathBuf, - pub mime_guess: MimeGuess, + pub mime: Mime, pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle, - #[cfg(feature = "xdg")] - pub open_with: Vec, + pub open_with: Vec, pub thumbnail_res_opt: Option>, pub button_id: widget::Id, pub pos_opt: Cell>, @@ -478,20 +485,47 @@ pub struct Item { } impl Item { + pub fn open_with_view(&self, sizes: IconSizes) -> Element { + let cosmic_theme::Spacing { + space_xs, + space_xxxs, + .. + } = theme::active().cosmic().spacing; + + let mut column = widget::column().spacing(space_xxxs); + + column = column.push(widget::text::heading(&self.name)); + + for app in self.open_with.iter() { + column = column.push( + widget::button( + widget::row::with_children(vec![ + widget::icon(app.icon.clone()).into(), + widget::text(&app.name).into(), + ]) + .spacing(space_xs), + ) + .padding(space_xs) + .width(Length::Fill), + ); + } + + column.into() + } + pub fn property_view(&self, sizes: IconSizes) -> Element { let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let mut column = widget::column().spacing(space_xxxs); - let is_image = if let Some(mime) = self.mime_guess.first() { - mime.type_() == "image" && self.path.is_file() - } else { - false - }; - column = column.push(widget::row::with_children(vec![ widget::horizontal_space(Length::Fill).into(), - if is_image { + // This loads the image only if thumbnailing worked + if self + .thumbnail_res_opt + .as_ref() + .map_or(false, |res| res.is_ok()) + { widget::image::viewer(widget::image::Handle::from_path(&self.path)) .min_scale(1.0) .into() @@ -506,9 +540,7 @@ impl Item { column = column.push(widget::text::heading(self.name.clone())); - if let Some(mime) = self.mime_guess.first() { - column = column.push(widget::text(format!("Type: {}", mime))); - } + column = column.push(widget::text(format!("Type: {}", self.mime))); //TODO: translate! //TODO: correct display of folder size? @@ -553,27 +585,6 @@ impl Item { } } -impl fmt::Debug for Item { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = f.debug_struct("Item"); - d.field("name", &self.name); - d.field("metadata", &self.metadata); - d.field("hidden", &self.hidden); - d.field("path", &self.path); - d.field("mime_guess", &self.mime_guess); - // icon_handles - #[cfg(feature = "xdg")] - d.field("open_with", &self.open_with); - // thumbnail_res_opt - d.field("button_id", &self.button_id); - d.field("pos_opt", &self.pos_opt); - d.field("rect_opt", &self.rect_opt); - d.field("selected", &self.selected); - d.field("click_time", &self.click_time); - d.finish() - } -} - #[derive(Clone, Copy, Debug)] pub enum View { Grid, diff --git a/src/xdg/mime_app.rs b/src/xdg/mime_app.rs deleted file mode 100644 index 2f0e89c..0000000 --- a/src/xdg/mime_app.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: GPL-3.0-only - -pub use cosmic::desktop::DesktopEntryData; -use cosmic::desktop::{load_applications, Mime}; -use once_cell::sync::Lazy; -use std::{collections::HashMap, sync::Mutex, time::Instant}; - -pub struct MimeAppCache { - cache: HashMap>, - empty: Vec, -} - -impl MimeAppCache { - pub fn new() -> Self { - let mut mime_app_cache = Self { - cache: HashMap::new(), - empty: Vec::new(), - }; - mime_app_cache.reload(); - mime_app_cache - } - - pub fn reload(&mut self) { - let start = Instant::now(); - - self.cache.clear(); - - //TODO: get proper locale? - let locale = None; - for app in load_applications(locale, false) { - for mime_type in app.mime_types.iter() { - self.cache - .entry(mime_type.clone()) - .or_insert_with(|| Vec::with_capacity(1)) - .push(app.clone()); - } - } - - for apps in self.cache.values_mut() { - apps.sort_by(|a, b| lexical_sort::natural_lexical_cmp(&a.name, &b.name)); - } - - let elapsed = start.elapsed(); - log::info!("loaded mime app cache in {:?}", elapsed); - } - - pub fn get(&self, key: &Mime) -> &Vec { - self.cache.get(&key).unwrap_or_else(|| &self.empty) - } -} - -static MIME_APP_CACHE: Lazy> = Lazy::new(|| Mutex::new(MimeAppCache::new())); - -pub fn mime_apps(mime: &Mime) -> Vec { - let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); - mime_app_cache.get(mime).clone() -} diff --git a/src/xdg/mod.rs b/src/xdg/mod.rs deleted file mode 100644 index bce8d5d..0000000 --- a/src/xdg/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: GPL-3.0-only - -pub mod mime_app; -pub use mime_app::*;