diff --git a/Cargo.lock b/Cargo.lock index ed0611c..e6bd2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "almost" version = "0.2.0" @@ -880,6 +886,39 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cached" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.14.5", + "once_cell", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "calloop" version = "0.13.0" @@ -1229,6 +1268,7 @@ version = "0.1.0" dependencies = [ "bzip2", "chrono", + "cosmic-mime-apps", "dirs 5.0.1", "env_logger", "fastrand 2.3.0", @@ -1299,6 +1339,17 @@ dependencies = [ "xdg", ] +[[package]] +name = "cosmic-mime-apps" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-mime-apps.git#a5aefbd2e914682c151f3b8054dd711e7f57941d" +dependencies = [ + "freedesktop-desktop-entry 0.7.7", + "mime 0.3.17", + "quick-xml 0.37.2", + "xdg", +] + [[package]] name = "cosmic-protocols" version = "0.1.0" @@ -2138,6 +2189,23 @@ dependencies = [ "xdg", ] +[[package]] +name = "freedesktop-desktop-entry" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016f6ee9509f11c985aa402451f4ee900d1fafeb501a4c3d734ebecfc1130e05" +dependencies = [ + "cached", + "dirs 5.0.1", + "gettext-rs", + "log", + "memchr", + "strsim 0.11.1", + "textdistance", + "thiserror 2.0.11", + "xdg", +] + [[package]] name = "freedesktop_entry_parser" version = "1.3.0" @@ -2567,6 +2635,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -3537,7 +3609,7 @@ dependencies = [ "cosmic-theme", "css-color", "derive_setters", - "freedesktop-desktop-entry", + "freedesktop-desktop-entry 0.5.2", "iced", "iced_core", "iced_futures", @@ -4923,6 +4995,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.38" @@ -5881,6 +5962,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "textdistance" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa672c55ab69f787dbc9126cc387dbe57fdd595f585e4524cf89018fa44ab819" + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 52b6721..bbe76f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ vergen = { version = "8", features = ["git", "gitcl"] } [dependencies] chrono = { version = "0.4", features = ["unstable-locales"] } +cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true } dirs = "5.0.1" env_logger = "0.11" freedesktop_entry_parser = "1.3" @@ -67,7 +68,7 @@ features = ["multi-window", "tokio", "winit"] [features] default = ["bzip2", "desktop", "gvfs", "liblzma", "notify", "wgpu"] -desktop = ["libcosmic/desktop", "dep:xdg"] +desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"] gvfs = ["dep:gio", "dep:glib"] jemalloc = ["dep:tikv-jemallocator"] notify = ["dep:notify-rust"] diff --git a/i18n/ar/cosmic_files.ftl b/i18n/ar/cosmic_files.ftl index 3e5e906..9282444 100644 --- a/i18n/ar/cosmic_files.ftl +++ b/i18n/ar/cosmic_files.ftl @@ -62,7 +62,7 @@ complete = انتهى copy_noun = ينسخ ## Open with -open-with = افتح ب‍استخدام +menu-open-with = افتح ب‍استخدام default-app = {$name} (المبدئي) ## Properties diff --git a/i18n/be/cosmic_files.ftl b/i18n/be/cosmic_files.ftl index 646ba1d..48bcdca 100644 --- a/i18n/be/cosmic_files.ftl +++ b/i18n/be/cosmic_files.ftl @@ -159,7 +159,7 @@ restored = Адноўлена {$items} {$items -> unknown-folder = невядомая папка ## Open with -open-with = Адкрыць з дапамогай +menu-open-with = Адкрыць з дапамогай default-app = {$name} (па змаўчанні) ## Properties diff --git a/i18n/cs/cosmic_files.ftl b/i18n/cs/cosmic_files.ftl index 6021795..335d008 100644 --- a/i18n/cs/cosmic_files.ftl +++ b/i18n/cs/cosmic_files.ftl @@ -62,7 +62,7 @@ complete = Hotovo copy_noun = Kopírovat ## Open with -open-with = Otevřít v +menu-open-with = Otevřít v default-app = {$name} (výchozí) ## Properties diff --git a/i18n/da/cosmic_files.ftl b/i18n/da/cosmic_files.ftl index 949366c..cd2c6d3 100644 --- a/i18n/da/cosmic_files.ftl +++ b/i18n/da/cosmic_files.ftl @@ -196,7 +196,7 @@ restored = Genoprettet {$items} {$items -> unknown-folder = ukendt mappe ## Open with -open-with = Åbn med... +menu-open-with = Åbn med... default-app = {$name} (standardindstilling) ## Show details diff --git a/i18n/de/cosmic_files.ftl b/i18n/de/cosmic_files.ftl index b329cc9..ea61c30 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -196,7 +196,7 @@ restored = {$items} {$items -> unknown-folder = unbekannter Ordner ## Öffnen mit -open-with = Öffnen mit +menu-open-with = Öffnen mit default-app = {$name} (Standard) ## Details anzeigen diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index a05cd2d..c7179ac 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -96,6 +96,7 @@ set-executable-and-launch-description = Do you want to set "{$name}" as executab set-and-launch = Set and launch ## Metadata Dialog +open-with = Open with owner = Owner group = Group other = Other @@ -196,7 +197,7 @@ restored = Restored {$items} {$items -> unknown-folder = unknown folder ## Open with -open-with = Open with... +menu-open-with = Open with... default-app = {$name} (default) ## Show details diff --git a/i18n/es-419/cosmic_files.ftl b/i18n/es-419/cosmic_files.ftl index 5cea7d2..4653132 100644 --- a/i18n/es-419/cosmic_files.ftl +++ b/i18n/es-419/cosmic_files.ftl @@ -181,7 +181,7 @@ restored = Se ha restaurado {$items} {$items -> unknown-folder = carpeta desconocida ## Open with -open-with = Abrir con +menu-open-with = Abrir con default-app = {$name} (predeterminado) ## Show details diff --git a/i18n/es/cosmic_files.ftl b/i18n/es/cosmic_files.ftl index 3b9a3ac..f2e567d 100644 --- a/i18n/es/cosmic_files.ftl +++ b/i18n/es/cosmic_files.ftl @@ -62,7 +62,7 @@ complete = Completadas copy_noun = Copia ## Open with -open-with = Abrir con +menu-open-with = Abrir con default-app = {$name} (por defecto) ## Properties diff --git a/i18n/fi/cosmic_files.ftl b/i18n/fi/cosmic_files.ftl index 0c455af..cecca7d 100644 --- a/i18n/fi/cosmic_files.ftl +++ b/i18n/fi/cosmic_files.ftl @@ -201,7 +201,7 @@ restored = Palautettu {$items} {$items -> unknown-folder = Tuntematon kansio ## Open with -open-with = Avaa ohjelmalla… +menu-open-with = Avaa ohjelmalla… default-app = {$name} (oletus) ## Show details diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index e735d57..076429e 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -184,7 +184,7 @@ restored = {$items} {$items -> unknown-folder = Dossier inconnu ## Open with -open-with = Ouvrir avec +menu-open-with = Ouvrir avec default-app = {$name} (défaut) ## Show details diff --git a/i18n/hi/cosmic_files.ftl b/i18n/hi/cosmic_files.ftl index 0f696b9..f897144 100644 --- a/i18n/hi/cosmic_files.ftl +++ b/i18n/hi/cosmic_files.ftl @@ -181,7 +181,7 @@ restored = {$items} {$items -> unknown-folder = अज्ञात फ़ोल्डर ## Open with -open-with = इसके साथ खोलें +menu-open-with = इसके साथ खोलें default-app = {$name} (डिफ़ॉल्ट) ## Show details diff --git a/i18n/hu/cosmic_files.ftl b/i18n/hu/cosmic_files.ftl index c352cf8..56d4d7e 100644 --- a/i18n/hu/cosmic_files.ftl +++ b/i18n/hu/cosmic_files.ftl @@ -102,7 +102,7 @@ undo = Visszavonás unknown-folder = ismeretlen mappa ## Open with -open-with = Megnyitás ezzel +menu-open-with = Megnyitás ezzel default-app = {$name} (alapértelmezett) ## Properties diff --git a/i18n/it/cosmic_files.ftl b/i18n/it/cosmic_files.ftl index 9849a9a..9815127 100644 --- a/i18n/it/cosmic_files.ftl +++ b/i18n/it/cosmic_files.ftl @@ -192,7 +192,7 @@ restored = Ripristinato {$items} {$items -> unknown-folder = cartella sconosciuta ## Open with -open-with = Apri con +menu-open-with = Apri con default-app = {$name} (default) ## Show details diff --git a/i18n/ja/cosmic_files.ftl b/i18n/ja/cosmic_files.ftl index c7e1e0c..3444ea5 100644 --- a/i18n/ja/cosmic_files.ftl +++ b/i18n/ja/cosmic_files.ftl @@ -128,7 +128,7 @@ undo = 元に戻す unknown-folder = 不明なフォルダー ## Open with -open-with = 別のアプリケーションで開く +menu-open-with = 別のアプリケーションで開く default-app = {$name} (デフォルト) ## Properties diff --git a/i18n/kn/cosmic_fiiles.ftl b/i18n/kn/cosmic_fiiles.ftl index 663faa2..c69bb3b 100644 --- a/i18n/kn/cosmic_fiiles.ftl +++ b/i18n/kn/cosmic_fiiles.ftl @@ -181,7 +181,7 @@ restored = {$items} {$items -> unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್ ## Open with -open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ +menu-open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ default-app = {$name} (ಸ್ಥೂಲ) ## Show details diff --git a/i18n/ko/cosmic_files.ftl b/i18n/ko/cosmic_files.ftl index c41b8fb..847d1cd 100644 --- a/i18n/ko/cosmic_files.ftl +++ b/i18n/ko/cosmic_files.ftl @@ -52,7 +52,7 @@ failed = 실패 complete = 완료 ## Open with -open-with = 다른 앱으로 열기 +menu-open-with = 다른 앱으로 열기 default-app = {$name} (기본) ## Properties diff --git a/i18n/nl/cosmic_files.ftl b/i18n/nl/cosmic_files.ftl index dfd0a43..e1e1814 100644 --- a/i18n/nl/cosmic_files.ftl +++ b/i18n/nl/cosmic_files.ftl @@ -192,7 +192,7 @@ restored = {$items} {$items -> unknown-folder = Onbekende map ## Open with -open-with = Openen met... +menu-open-with = Openen met... default-app = {$name} (standaard) ## Show details diff --git a/i18n/pl/cosmic_files.ftl b/i18n/pl/cosmic_files.ftl index 16b622d..918acc0 100644 --- a/i18n/pl/cosmic_files.ftl +++ b/i18n/pl/cosmic_files.ftl @@ -200,7 +200,7 @@ restored = Przywrócono {$items} {$items -> unknown-folder = nieznany katalog ## Open with -open-with = Otwórz za pomocą… +menu-open-with = Otwórz za pomocą… default-app = {$name} (domyślnie) ## Show details diff --git a/i18n/pt-BR/cosmic_files.ftl b/i18n/pt-BR/cosmic_files.ftl index ec9690c..1d0be7c 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -196,7 +196,7 @@ restored = Restaurado {$items} {$items -> unknown-folder = pasta desconhecida ## Open with -open-with = Abrir com... +menu-open-with = Abrir com... default-app = {$name} (padrão) ## Show details diff --git a/i18n/pt/cosmic_files.ftl b/i18n/pt/cosmic_files.ftl index e8d0f1d..22703bd 100644 --- a/i18n/pt/cosmic_files.ftl +++ b/i18n/pt/cosmic_files.ftl @@ -122,7 +122,7 @@ undo = Desfazer unknown-folder = pasta desconhecida ## Open with -open-with = Abrir com... +menu-open-with = Abrir com... default-app = {$name} (predefinição) ## Show details diff --git a/i18n/ro/cosmic_files.ftl b/i18n/ro/cosmic_files.ftl index 45a2f2d..00a6f6e 100644 --- a/i18n/ro/cosmic_files.ftl +++ b/i18n/ro/cosmic_files.ftl @@ -181,7 +181,7 @@ restored = Restaurat {$items} {$items -> unknown-folder = dosar necunoscut ## Open with -open-with = Deschide cu... +menu-open-with = Deschide cu... default-app = {$name} (implicit) ## Show details diff --git a/i18n/ru/cosmic_files.ftl b/i18n/ru/cosmic_files.ftl index c7d257a..0e7947d 100644 --- a/i18n/ru/cosmic_files.ftl +++ b/i18n/ru/cosmic_files.ftl @@ -161,7 +161,7 @@ restored = Восстановлено {$items} {$items -> unknown-folder = неизвестная папка ## Open with -open-with = Открыть с помощью +menu-open-with = Открыть с помощью default-app = {$name} (по умолчанию) ## Show details diff --git a/i18n/sk/cosmic_files.ftl b/i18n/sk/cosmic_files.ftl index 8de22ba..c54ac35 100644 --- a/i18n/sk/cosmic_files.ftl +++ b/i18n/sk/cosmic_files.ftl @@ -195,7 +195,7 @@ undo = Späť unknown-folder = neznámy priečinok ## Open with -open-with = Otvoriť s +menu-open-with = Otvoriť s default-app = {$name} (Predvolené) ## Show details diff --git a/i18n/sv/cosmic_files.ftl b/i18n/sv/cosmic_files.ftl index 948283d..f357780 100644 --- a/i18n/sv/cosmic_files.ftl +++ b/i18n/sv/cosmic_files.ftl @@ -203,7 +203,7 @@ restored = Återställt {$items} {$items -> unknown-folder = okänd katalog ## Öppna med -open-with = Öppna med... +menu-open-with = Öppna med... default-app = {$name} (default) ## Visa detaljer diff --git a/i18n/th/cosmic_files.ftl b/i18n/th/cosmic_files.ftl index f00c4c4..7485e6d 100644 --- a/i18n/th/cosmic_files.ftl +++ b/i18n/th/cosmic_files.ftl @@ -196,7 +196,7 @@ restored = Restored {$items} {$items -> unknown-folder = แฟ้มที่ไม่รู้จัก ## Open with -open-with = เปิดด้วย... +menu-open-with = เปิดด้วย... default-app = {$name} (ค่าเริ่มต้น) ## Show details diff --git a/i18n/tr/cosmic_files.ftl b/i18n/tr/cosmic_files.ftl index 24e40c2..cd789a6 100644 --- a/i18n/tr/cosmic_files.ftl +++ b/i18n/tr/cosmic_files.ftl @@ -166,7 +166,7 @@ restored = {$items} öge "{$trash}"ten "{$to}" dizinine geri yüklenildi ({$prog unknown-folder = bilinmeyen klasör ## Open with -open-with = Birlikte aç... +menu-open-with = Birlikte aç... default-app = {$name} (varsayılan) ## Show details diff --git a/i18n/uk/cosmic_files.ftl b/i18n/uk/cosmic_files.ftl index 3359e9e..3a07b9f 100644 --- a/i18n/uk/cosmic_files.ftl +++ b/i18n/uk/cosmic_files.ftl @@ -106,7 +106,7 @@ undo = Скасувати unknown-folder = невідома тека ## Open with -open-with = Відкрити за допомогою +menu-open-with = Відкрити за допомогою default-app = {$name} (типово) ## Properties diff --git a/i18n/zh-CN/cosmic_files.ftl b/i18n/zh-CN/cosmic_files.ftl index 4dc0c9a..dee9d31 100644 --- a/i18n/zh-CN/cosmic_files.ftl +++ b/i18n/zh-CN/cosmic_files.ftl @@ -166,7 +166,7 @@ restored = 已从 {trash} 还原 {$items} 个项目 unknown-folder = 未知文件夹 ## Open with -open-with = 打开方式... +menu-open-with = 打开方式... default-app = {$name} (默认) ## Show details diff --git a/i18n/zh-TW/cosmic_files.ftl b/i18n/zh-TW/cosmic_files.ftl index a87b9f7..f21417f 100644 --- a/i18n/zh-TW/cosmic_files.ftl +++ b/i18n/zh-TW/cosmic_files.ftl @@ -159,7 +159,7 @@ restored = 已還原 {$items} 項目 {$items -> unknown-folder = 未知資料夾 ## Open with -open-with = 開啟方式... +menu-open-with = 開啟方式... default-app = {$name} (預設) ## Show details diff --git a/src/app.rs b/src/app.rs index d9e667b..e18977e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -444,7 +444,6 @@ pub enum DialogPage { OpenWith { path: PathBuf, mime: mime_guess::Mime, - apps: Vec, selected: usize, store_opt: Option, }, @@ -514,6 +513,7 @@ pub struct App { dialog_text_input: widget::Id, key_binds: HashMap, margin: HashMap, + mime_app_cache: mime_app::MimeAppCache, modifiers: Modifiers, mounter_items: HashMap, network_drive_connecting: Option<(MounterKey, String)>, @@ -592,7 +592,7 @@ impl App { } // Try mime apps, which should be faster than xdg-open - for app in mime_app::mime_apps(&mime) { + for app in self.mime_app_cache.get(&mime) { let Some(mut command) = app.command(Some(path.clone().into())) else { continue; }; @@ -1431,21 +1431,24 @@ impl App { entity_opt: &Option, kind: &'a PreviewKind, context_drawer: bool, - ) -> Element<'a, Message> { + ) -> Element<'a, tab::Message> { let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing; let mut children = Vec::with_capacity(1); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); match kind { PreviewKind::Custom(PreviewItem(item)) => { - children.push(item.preview_view(IconSizes::default())); + children.push(item.preview_view(Some(&self.mime_app_cache), IconSizes::default())); } PreviewKind::Location(location) => { if let Some(tab) = self.tab_model.data::(entity) { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.location_opt.as_ref() == Some(location) { - children.push(item.preview_view(tab.config.icon_sizes)); + children.push(item.preview_view( + Some(&self.mime_app_cache), + tab.config.icon_sizes, + )); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -1459,7 +1462,10 @@ impl App { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.selected { - children.push(item.preview_view(tab.config.icon_sizes)); + children.push(item.preview_view( + Some(&self.mime_app_cache), + tab.config.icon_sizes, + )); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -1467,7 +1473,10 @@ impl App { } if children.is_empty() { if let Some(item) = &tab.parent_item_opt { - children.push(item.preview_view(tab.config.icon_sizes)); + children.push(item.preview_view( + Some(&self.mime_app_cache), + tab.config.icon_sizes, + )); } } } @@ -1573,6 +1582,7 @@ impl Application for App { dialog_text_input: widget::Id::unique(), key_binds, margin: HashMap::new(), + mime_app_cache: mime_app::MimeAppCache::new(), modifiers: Modifiers::empty(), mounter_items: HashMap::new(), network_drive_connecting: None, @@ -1688,7 +1698,7 @@ impl Application for App { NavMenuAction::Open(entity), )); items.push(cosmic::widget::menu::Item::Button( - fl!("open-with"), + fl!("menu-open-with"), None, NavMenuAction::OpenWith(entity), )); @@ -2016,11 +2026,11 @@ impl Application for App { } DialogPage::OpenWith { path, - apps, + mime, selected, .. } => { - if let Some(app) = apps.get(selected) { + if let Some(app) = self.mime_app_cache.get(&mime).get(selected) { if let Some(mut command) = app.command(Some(path.clone().into())) { match spawn_detached(&mut command) { Ok(()) => { @@ -2345,7 +2355,7 @@ impl Application for App { } }, Message::OpenTerminal(entity_opt) => { - if let Some(terminal) = mime_app::terminal() { + if let Some(terminal) = self.mime_app_cache.terminal() { let mut paths = Vec::new(); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { @@ -2471,12 +2481,13 @@ impl Application for App { return self.update(Message::DialogPush(DialogPage::OpenWith { path: path.to_path_buf(), mime: item.mime.clone(), - apps: item.open_with.clone(), selected: 0, store_opt: "x-scheme-handler/mime" .parse::() .ok() - .and_then(|mime| mime_app::mime_apps(&mime).first().cloned()), + .and_then(|mime| { + self.mime_app_cache.get(&mime).first().cloned() + }), })); } } @@ -2905,9 +2916,11 @@ impl Application for App { 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)) - })); + commands.push( + iced_command.0.map(move |x| { + message::app(Message::TabMessage(Some(entity), x)) + }), + ); } tab::Command::MoveToTrash(paths) => { self.operation(Operation::Delete { paths }); @@ -2942,6 +2955,10 @@ impl Application for App { self.context_page = ContextPage::Preview(Some(entity), kind); self.set_show_context(true); } + tab::Command::SetOpenWith(mime, id) => { + //TODO: this will block for a few ms, run in background? + self.mime_app_cache.set_default(mime, id); + } tab::Command::WindowDrag => { if let Some(window_id) = &self.window_id_opt { commands.push(window::drag(*window_id)); @@ -3282,13 +3299,12 @@ impl Application for App { return self.update(Message::DialogPush(DialogPage::OpenWith { path: path.to_path_buf(), mime: item.mime.clone(), - apps: item.open_with.clone(), selected: 0, store_opt: "x-scheme-handler/mime" .parse::() .ok() .and_then(|mime| { - mime_app::mime_apps(&mime).first().cloned() + self.mime_app_cache.get(&mime).first().cloned() }), })); } @@ -3530,14 +3546,17 @@ impl Application for App { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.selected { - actions.extend(item.preview_header()) + actions.extend(item.preview_header().into_iter().map(|element| { + element.map(move |x| Message::TabMessage(Some(entity), x)) + })); } } } }; context_drawer::context_drawer( - self.preview(entity_opt, kind, true), - Message::ToggleContextPage(ContextPage::Preview(*entity_opt, kind.clone())), + self.preview(entity_opt, kind, true) + .map(move |x| Message::TabMessage(Some(entity), x)), + Message::ToggleContextPage(ContextPage::Preview(Some(entity), kind.clone())), ) .header_actions(actions) } @@ -3556,7 +3575,7 @@ impl Application for App { if tab.gallery { return Some( tab.gallery_view() - .map(move |tab_message| Message::TabMessage(Some(entity), tab_message)), + .map(move |x| Message::TabMessage(Some(entity), x)), ); } } @@ -3878,7 +3897,7 @@ impl Application for App { } DialogPage::OpenWith { path, - apps, + mime, selected, store_opt, .. @@ -3889,7 +3908,7 @@ impl Application for App { }; let mut column = widget::list_column(); - for (i, app) in apps.iter().enumerate() { + for (i, app) in self.mime_app_cache.get(mime).iter().enumerate() { column = column.add( widget::button::custom( widget::row::with_children(vec![ @@ -4026,8 +4045,14 @@ impl Application for App { let dialog = widget::dialog() .title(fl!("replace-title", filename = to.name.as_str())) .body(fl!("replace-warning-operation")) - .control(to.replace_view(fl!("original-file"), IconSizes::default())) - .control(from.replace_view(fl!("replace-with"), IconSizes::default())) + .control( + to.replace_view(fl!("original-file"), IconSizes::default()) + .map(|x| Message::TabMessage(None, x)), + ) + .control( + from.replace_view(fl!("replace-with"), IconSizes::default()) + .map(|x| Message::TabMessage(None, x)), + ) .primary_action(widget::button::suggested(fl!("replace")).on_press( Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)), )); @@ -4365,7 +4390,9 @@ impl Application for App { }; } Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(), - Some(WindowKind::Preview(entity_opt, kind)) => self.preview(entity_opt, kind, false), + Some(WindowKind::Preview(entity_opt, kind)) => self + .preview(entity_opt, kind, false) + .map(|x| Message::TabMessage(*entity_opt, x)), None => { //TODO: distinct views per monitor in desktop mode return self.view_main().map(|message| match message { diff --git a/src/dialog.rs b/src/dialog.rs index c57c966..dc10905 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -471,17 +471,17 @@ impl App { .into() } - fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, AppMessage> { + fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> { let mut children = Vec::with_capacity(1); match kind { PreviewKind::Custom(PreviewItem(item)) => { - children.push(item.preview_view(IconSizes::default())); + children.push(item.preview_view(None, IconSizes::default())); } PreviewKind::Location(location) => { if let Some(items) = self.tab.items_opt() { for item in items.iter() { if item.location_opt.as_ref() == Some(location) { - children.push(item.preview_view(self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -493,7 +493,7 @@ impl App { if let Some(items) = self.tab.items_opt() { for item in items.iter() { if item.selected { - children.push(item.preview_view(self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -501,7 +501,7 @@ impl App { } if children.is_empty() { if let Some(item) = &self.tab.parent_item_opt { - children.push(item.preview_view(self.tab.config.icon_sizes)); + children.push(item.preview_view(None, self.tab.config.icon_sizes)); } } } @@ -814,14 +814,14 @@ impl Application for App { actions.extend( item.preview_header() .into_iter() - .map(|element| element.map(Message::from)), + .map(|element| element.map(Message::TabMessage)), ) } } }; Some( context_drawer::context_drawer( - self.preview(kind).map(Message::from), + self.preview(kind).map(Message::TabMessage), Message::Preview, ) .header_actions(actions), diff --git a/src/menu.rs b/src/menu.rs index 219a463..c65b26e 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -157,7 +157,7 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("open"), Action::Open).into()); } if selected == 1 { - children.push(menu_item(fl!("open-with"), Action::OpenWith).into()); + children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into()); if selected_dir == 1 { children .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); @@ -531,7 +531,7 @@ pub fn menu_bar<'a>( Action::Open, (selected > 0 && selected_dir == 0) || (selected_dir == 1 && selected == 1), ), - menu_button_optional(fl!("open-with"), Action::OpenWith, selected == 1), + menu_button_optional(fl!("menu-open-with"), Action::OpenWith, selected == 1), menu::Item::Divider, menu_button_optional(fl!("rename"), Action::Rename, selected > 0), menu::Item::Divider, diff --git a/src/mime_app.rs b/src/mime_app.rs index 3dfa1c3..5e2556b 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -5,9 +5,8 @@ use cosmic::desktop; use cosmic::widget; pub use mime_guess::Mime; -use once_cell::sync::Lazy; use std::{ - cmp::Ordering, collections::HashMap, env, ffi::OsString, path::PathBuf, process, sync::Mutex, + cmp::Ordering, collections::HashMap, env, ffi::OsString, fs, io, path::PathBuf, process, time::Instant, }; @@ -52,6 +51,13 @@ impl MimeApp { } } +// This allows usage of MimeApp in a dropdown +impl AsRef for MimeApp { + fn as_ref(&self) -> &str { + &self.name + } +} + #[cfg(feature = "desktop")] impl From<&desktop::DesktopEntryData> for MimeApp { fn from(app: &desktop::DesktopEntryData) -> Self { @@ -82,6 +88,7 @@ fn filename_eq(path_opt: &Option, filename: &str) -> bool { pub struct MimeAppCache { cache: HashMap>, + icons: HashMap>, terminals: Vec, } @@ -89,6 +96,7 @@ impl MimeAppCache { pub fn new() -> Self { let mut mime_app_cache = Self { cache: HashMap::new(), + icons: HashMap::new(), terminals: Vec::new(), }; mime_app_cache.reload(); @@ -106,6 +114,7 @@ impl MimeAppCache { let start = Instant::now(); self.cache.clear(); + self.icons.clear(); self.terminals.clear(); //TODO: get proper locale? @@ -254,37 +263,88 @@ impl MimeAppCache { }); } + // Copy icons to special cache + //TODO: adjust dropdown API so this is no longer needed + for (mime, apps) in self.cache.iter() { + self.icons.insert( + mime.clone(), + apps.iter().map(|app| app.icon.clone()).collect(), + ); + } + 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()) + pub fn get(&self, key: &Mime) -> &[MimeApp] { + static EMPTY: Vec = Vec::new(); + self.cache.get(key).unwrap_or_else(|| &EMPTY) } -} -static MIME_APP_CACHE: Lazy> = Lazy::new(|| Mutex::new(MimeAppCache::new())); + pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] { + static EMPTY: Vec = Vec::new(); + self.icons.get(key).unwrap_or_else(|| &EMPTY) + } -pub fn mime_apps(mime: &Mime) -> Vec { - let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); - mime_app_cache.get(mime) -} + pub fn terminal(&self) -> Option<&MimeApp> { + //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec -pub fn terminal() -> Option { - let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); + // Look for and return preferred terminals + //TODO: fallback order beyond cosmic-term? + for id in &["com.system76.CosmicTerm"] { + for terminal in self.terminals.iter() { + if &terminal.id == id { + return Some(terminal); + } + } + } - //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec + // Return whatever was the first terminal found + self.terminals.first() + } - // Look for and return preferred terminals - //TODO: fallback order beyond cosmic-term? - for id in &["com.system76.CosmicTerm"] { - for terminal in mime_app_cache.terminals.iter() { - if &terminal.id == id { - return Some(terminal.clone()); + #[cfg(not(feature = "desktop"))] + pub fn set_default(&mut self, mime: Mime, id: String) { + log::warn!( + "failed to set default handler for {mime:?} to {id:?}: desktop feature not enabled" + ); + } + + #[cfg(feature = "desktop")] + pub fn set_default(&mut self, mime: Mime, mut id: String) { + let Some(path) = cosmic_mime_apps::local_list_path() else { + log::warn!("failed to find mimeapps.list path"); + return; + }; + + let mut list = cosmic_mime_apps::List::default(); + match fs::read_to_string(&path) { + Ok(string) => { + list.load_from(&string); + } + Err(err) => { + if err.kind() != io::ErrorKind::NotFound { + log::warn!("failed to read {path:?}: {err}"); + return; + } + } + } + + let suffix = ".desktop"; + if !id.ends_with(suffix) { + id.push_str(suffix); + } + list.set_default_app(mime, id); + + let mut string = list.to_string(); + string.push('\n'); + match fs::write(&path, string) { + Ok(()) => { + self.reload(); + } + Err(err) => { + log::warn!("failed to write {path:?}: {err}"); } } } - - // Return whatever was the first terminal found - mime_app_cache.terminals.first().cloned() } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index c24fd92..78df185 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -127,7 +127,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - open_with: Vec::new(), thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), diff --git a/src/tab.rs b/src/tab.rs index d4fadb3..63d734c 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -58,14 +58,13 @@ use tokio::sync::mpsc; use walkdir::WalkDir; use crate::{ - app::{self, Action, PreviewItem, PreviewKind}, + app::{Action, PreviewItem, PreviewKind}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID}, dialog::DialogKind, fl, localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER}, - menu, - mime_app::{mime_apps, MimeApp}, + menu, mime_app, mime_icon::{mime_for_path, mime_icon}, mounter::MOUNTERS, mouse_area, @@ -459,8 +458,6 @@ pub fn item_from_entry( } }; - let open_with = mime_apps(&mime); - let children = if metadata.is_dir() { //TODO: calculate children in the background (and make it cancellable?) match fs::read_dir(&path) { @@ -490,7 +487,6 @@ pub fn item_from_entry( icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - open_with, thumbnail_opt: None, button_id: widget::Id::unique(), pos_opt: Cell::new(None), @@ -720,7 +716,6 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - open_with: Vec::new(), thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), @@ -904,7 +899,6 @@ pub fn scan_desktop( icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - open_with: Vec::new(), thumbnail_opt: Some(ItemThumbnail::NotImage), button_id: widget::Id::unique(), pos_opt: Cell::new(None), @@ -1027,6 +1021,7 @@ pub enum Command { OpenInNewWindow(PathBuf), OpenTrash, Preview(PreviewKind), + SetOpenWith(Mime, String), WindowDrag, WindowToggleMaximize, } @@ -1073,6 +1068,7 @@ pub enum Message { SelectAll, SelectFirst, SelectLast, + SetOpenWith(Mime, String), SetSort(HeadingOptions, bool), Thumbnail(PathBuf, ItemThumbnail), ToggleShowHidden, @@ -1318,7 +1314,6 @@ pub struct Item { pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle, - pub open_with: Vec, pub thumbnail_opt: Option, pub button_id: widget::Id, pub pos_opt: Cell>, @@ -1343,7 +1338,7 @@ impl Item { self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT } - fn preview<'a>(&'a self, sizes: IconSizes) -> Element<'a, app::Message> { + fn preview<'a>(&'a self, sizes: IconSizes) -> Element<'a, Message> { let spacing = cosmic::theme::active().cosmic().spacing; // This loads the image only if thumbnailing worked let icon = widget::icon::icon(self.icon_handle_grid.clone()) @@ -1376,23 +1371,23 @@ impl Item { } } - pub fn preview_header(&self) -> Vec> { + pub fn preview_header(&self) -> Vec> { let mut row = Vec::with_capacity(3); row.push( widget::button::icon(widget::icon::from_name("go-previous-symbolic")) - .on_press(app::Message::TabMessage(None, Message::ItemLeft)) + .on_press(Message::ItemLeft) .into(), ); row.push( widget::button::icon(widget::icon::from_name("go-next-symbolic")) - .on_press(app::Message::TabMessage(None, Message::ItemRight)) + .on_press(Message::ItemRight) .into(), ); if self.can_gallery() { if let Some(_path) = self.path_opt() { row.push( widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) - .on_press(app::Message::TabMessage(None, Message::Gallery(true))) + .on_press(Message::Gallery(true)) .into(), ); } @@ -1400,7 +1395,11 @@ impl Item { row } - pub fn preview_view<'a>(&'a self, sizes: IconSizes) -> Element<'a, app::Message> { + pub fn preview_view<'a>( + &'a self, + mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>, + sizes: IconSizes, + ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, space_m, @@ -1422,6 +1421,24 @@ impl Item { mime = self.mime.to_string() ))); let mut settings = Vec::new(); + if let Some(mime_app_cache) = mime_app_cache_opt { + let mime_apps = mime_app_cache.get(&self.mime); + if !mime_apps.is_empty() { + settings.push( + widget::settings::item::builder(fl!("open-with")).control( + widget::dropdown( + mime_apps, + mime_apps.iter().position(|x| x.is_default), + |index| { + let mime_app = &mime_apps[index]; + Message::SetOpenWith(self.mime.clone(), mime_app.id.clone()) + }, + ) + .icons(mime_app_cache.icons(&self.mime)), + ), + ); + } + } match &self.metadata { ItemMetadata::Path { metadata, children } => { if metadata.is_dir() { @@ -1509,9 +1526,10 @@ impl Item { column = column.push(details); if let Some(path) = self.path_opt() { - column = column.push(widget::button::standard(fl!("open")).on_press( - app::Message::TabMessage(None, Message::Open(Some(path.to_path_buf()))), - )); + column = column.push( + widget::button::standard(fl!("open")) + .on_press(Message::Open(Some(path.to_path_buf()))), + ); } if !settings.is_empty() { @@ -1525,11 +1543,7 @@ impl Item { column.into() } - pub fn replace_view<'a>( - &'a self, - heading: String, - sizes: IconSizes, - ) -> Element<'a, app::Message> { + pub fn replace_view<'a>(&'a self, heading: String, sizes: IconSizes) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let mut row = widget::row().spacing(space_xxxs); @@ -2831,6 +2845,9 @@ impl Tab { } } } + Message::SetOpenWith(mime, id) => { + commands.push(Command::SetOpenWith(mime, id)); + } Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; @@ -4432,7 +4449,7 @@ impl Tab { dnd_dest.into() } - pub fn view<'a>(&'a self, key_binds: &'a HashMap) -> Element { + pub fn view<'a>(&'a self, key_binds: &'a HashMap) -> Element<'a, Message> { widget::responsive(|size| self.view_responsive(key_binds, size)).into() }