Clean up mime app handling and make it possible to set default application, part of #325

This commit is contained in:
Jeremy Soller 2025-01-24 11:55:56 -07:00
parent 691719ade7
commit ceab7835ad
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
37 changed files with 306 additions and 114 deletions

89
Cargo.lock generated
View file

@ -158,6 +158,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "almost" name = "almost"
version = "0.2.0" version = "0.2.0"
@ -880,6 +886,39 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "calloop" name = "calloop"
version = "0.13.0" version = "0.13.0"
@ -1229,6 +1268,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bzip2", "bzip2",
"chrono", "chrono",
"cosmic-mime-apps",
"dirs 5.0.1", "dirs 5.0.1",
"env_logger", "env_logger",
"fastrand 2.3.0", "fastrand 2.3.0",
@ -1299,6 +1339,17 @@ dependencies = [
"xdg", "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]] [[package]]
name = "cosmic-protocols" name = "cosmic-protocols"
version = "0.1.0" version = "0.1.0"
@ -2138,6 +2189,23 @@ dependencies = [
"xdg", "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]] [[package]]
name = "freedesktop_entry_parser" name = "freedesktop_entry_parser"
version = "1.3.0" version = "1.3.0"
@ -2567,6 +2635,10 @@ name = "hashbrown"
version = "0.14.5" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
@ -3537,7 +3609,7 @@ dependencies = [
"cosmic-theme", "cosmic-theme",
"css-color", "css-color",
"derive_setters", "derive_setters",
"freedesktop-desktop-entry", "freedesktop-desktop-entry 0.5.2",
"iced", "iced",
"iced_core", "iced_core",
"iced_futures", "iced_futures",
@ -4923,6 +4995,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "quick-xml"
version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.38"
@ -5881,6 +5962,12 @@ dependencies = [
"syn 2.0.96", "syn 2.0.96",
] ]
[[package]]
name = "textdistance"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa672c55ab69f787dbc9126cc387dbe57fdd595f585e4524cf89018fa44ab819"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"

View file

@ -11,6 +11,7 @@ vergen = { version = "8", features = ["git", "gitcl"] }
[dependencies] [dependencies]
chrono = { version = "0.4", features = ["unstable-locales"] } 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" dirs = "5.0.1"
env_logger = "0.11" env_logger = "0.11"
freedesktop_entry_parser = "1.3" freedesktop_entry_parser = "1.3"
@ -67,7 +68,7 @@ features = ["multi-window", "tokio", "winit"]
[features] [features]
default = ["bzip2", "desktop", "gvfs", "liblzma", "notify", "wgpu"] 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"] gvfs = ["dep:gio", "dep:glib"]
jemalloc = ["dep:tikv-jemallocator"] jemalloc = ["dep:tikv-jemallocator"]
notify = ["dep:notify-rust"] notify = ["dep:notify-rust"]

View file

@ -62,7 +62,7 @@ complete = انتهى
copy_noun = ينسخ copy_noun = ينسخ
## Open with ## Open with
open-with = افتح ب‍استخدام menu-open-with = افتح ب‍استخدام
default-app = {$name} (المبدئي) default-app = {$name} (المبدئي)
## Properties ## Properties

View file

@ -159,7 +159,7 @@ restored = Адноўлена {$items} {$items ->
unknown-folder = невядомая папка unknown-folder = невядомая папка
## Open with ## Open with
open-with = Адкрыць з дапамогай menu-open-with = Адкрыць з дапамогай
default-app = {$name} (па змаўчанні) default-app = {$name} (па змаўчанні)
## Properties ## Properties

View file

@ -62,7 +62,7 @@ complete = Hotovo
copy_noun = Kopírovat copy_noun = Kopírovat
## Open with ## Open with
open-with = Otevřít v menu-open-with = Otevřít v
default-app = {$name} (výchozí) default-app = {$name} (výchozí)
## Properties ## Properties

View file

@ -196,7 +196,7 @@ restored = Genoprettet {$items} {$items ->
unknown-folder = ukendt mappe unknown-folder = ukendt mappe
## Open with ## Open with
open-with = Åbn med... menu-open-with = Åbn med...
default-app = {$name} (standardindstilling) default-app = {$name} (standardindstilling)
## Show details ## Show details

View file

@ -196,7 +196,7 @@ restored = {$items} {$items ->
unknown-folder = unbekannter Ordner unknown-folder = unbekannter Ordner
## Öffnen mit ## Öffnen mit
open-with = Öffnen mit menu-open-with = Öffnen mit
default-app = {$name} (Standard) default-app = {$name} (Standard)
## Details anzeigen ## Details anzeigen

View file

@ -96,6 +96,7 @@ set-executable-and-launch-description = Do you want to set "{$name}" as executab
set-and-launch = Set and launch set-and-launch = Set and launch
## Metadata Dialog ## Metadata Dialog
open-with = Open with
owner = Owner owner = Owner
group = Group group = Group
other = Other other = Other
@ -196,7 +197,7 @@ restored = Restored {$items} {$items ->
unknown-folder = unknown folder unknown-folder = unknown folder
## Open with ## Open with
open-with = Open with... menu-open-with = Open with...
default-app = {$name} (default) default-app = {$name} (default)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = Se ha restaurado {$items} {$items ->
unknown-folder = carpeta desconocida unknown-folder = carpeta desconocida
## Open with ## Open with
open-with = Abrir con menu-open-with = Abrir con
default-app = {$name} (predeterminado) default-app = {$name} (predeterminado)
## Show details ## Show details

View file

@ -62,7 +62,7 @@ complete = Completadas
copy_noun = Copia copy_noun = Copia
## Open with ## Open with
open-with = Abrir con menu-open-with = Abrir con
default-app = {$name} (por defecto) default-app = {$name} (por defecto)
## Properties ## Properties

View file

@ -201,7 +201,7 @@ restored = Palautettu {$items} {$items ->
unknown-folder = Tuntematon kansio unknown-folder = Tuntematon kansio
## Open with ## Open with
open-with = Avaa ohjelmalla… menu-open-with = Avaa ohjelmalla…
default-app = {$name} (oletus) default-app = {$name} (oletus)
## Show details ## Show details

View file

@ -184,7 +184,7 @@ restored = {$items} {$items ->
unknown-folder = Dossier inconnu unknown-folder = Dossier inconnu
## Open with ## Open with
open-with = Ouvrir avec menu-open-with = Ouvrir avec
default-app = {$name} (défaut) default-app = {$name} (défaut)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = {$items} {$items ->
unknown-folder = अज्ञात फ़ोल्डर unknown-folder = अज्ञात फ़ोल्डर
## Open with ## Open with
open-with = इसके साथ खोलें menu-open-with = इसके साथ खोलें
default-app = {$name} (डिफ़ॉल्ट) default-app = {$name} (डिफ़ॉल्ट)
## Show details ## Show details

View file

@ -102,7 +102,7 @@ undo = Visszavonás
unknown-folder = ismeretlen mappa unknown-folder = ismeretlen mappa
## Open with ## Open with
open-with = Megnyitás ezzel menu-open-with = Megnyitás ezzel
default-app = {$name} (alapértelmezett) default-app = {$name} (alapértelmezett)
## Properties ## Properties

View file

@ -192,7 +192,7 @@ restored = Ripristinato {$items} {$items ->
unknown-folder = cartella sconosciuta unknown-folder = cartella sconosciuta
## Open with ## Open with
open-with = Apri con menu-open-with = Apri con
default-app = {$name} (default) default-app = {$name} (default)
## Show details ## Show details

View file

@ -128,7 +128,7 @@ undo = 元に戻す
unknown-folder = 不明なフォルダー unknown-folder = 不明なフォルダー
## Open with ## Open with
open-with = 別のアプリケーションで開く menu-open-with = 別のアプリケーションで開く
default-app = {$name} (デフォルト) default-app = {$name} (デフォルト)
## Properties ## Properties

View file

@ -181,7 +181,7 @@ restored = {$items} {$items ->
unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್ unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್
## Open with ## Open with
open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ menu-open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ
default-app = {$name} (ಸ್ಥೂಲ) default-app = {$name} (ಸ್ಥೂಲ)
## Show details ## Show details

View file

@ -52,7 +52,7 @@ failed = 실패
complete = 완료 complete = 완료
## Open with ## Open with
open-with = 다른 앱으로 열기 menu-open-with = 다른 앱으로 열기
default-app = {$name} (기본) default-app = {$name} (기본)
## Properties ## Properties

View file

@ -192,7 +192,7 @@ restored = {$items} {$items ->
unknown-folder = Onbekende map unknown-folder = Onbekende map
## Open with ## Open with
open-with = Openen met... menu-open-with = Openen met...
default-app = {$name} (standaard) default-app = {$name} (standaard)
## Show details ## Show details

View file

@ -200,7 +200,7 @@ restored = Przywrócono {$items} {$items ->
unknown-folder = nieznany katalog unknown-folder = nieznany katalog
## Open with ## Open with
open-with = Otwórz za pomocą… menu-open-with = Otwórz za pomocą…
default-app = {$name} (domyślnie) default-app = {$name} (domyślnie)
## Show details ## Show details

View file

@ -196,7 +196,7 @@ restored = Restaurado {$items} {$items ->
unknown-folder = pasta desconhecida unknown-folder = pasta desconhecida
## Open with ## Open with
open-with = Abrir com... menu-open-with = Abrir com...
default-app = {$name} (padrão) default-app = {$name} (padrão)
## Show details ## Show details

View file

@ -122,7 +122,7 @@ undo = Desfazer
unknown-folder = pasta desconhecida unknown-folder = pasta desconhecida
## Open with ## Open with
open-with = Abrir com... menu-open-with = Abrir com...
default-app = {$name} (predefinição) default-app = {$name} (predefinição)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = Restaurat {$items} {$items ->
unknown-folder = dosar necunoscut unknown-folder = dosar necunoscut
## Open with ## Open with
open-with = Deschide cu... menu-open-with = Deschide cu...
default-app = {$name} (implicit) default-app = {$name} (implicit)
## Show details ## Show details

View file

@ -161,7 +161,7 @@ restored = Восстановлено {$items} {$items ->
unknown-folder = неизвестная папка unknown-folder = неизвестная папка
## Open with ## Open with
open-with = Открыть с помощью menu-open-with = Открыть с помощью
default-app = {$name} (по умолчанию) default-app = {$name} (по умолчанию)
## Show details ## Show details

View file

@ -195,7 +195,7 @@ undo = Späť
unknown-folder = neznámy priečinok unknown-folder = neznámy priečinok
## Open with ## Open with
open-with = Otvoriť s menu-open-with = Otvoriť s
default-app = {$name} (Predvolené) default-app = {$name} (Predvolené)
## Show details ## Show details

View file

@ -203,7 +203,7 @@ restored = Återställt {$items} {$items ->
unknown-folder = okänd katalog unknown-folder = okänd katalog
## Öppna med ## Öppna med
open-with = Öppna med... menu-open-with = Öppna med...
default-app = {$name} (default) default-app = {$name} (default)
## Visa detaljer ## Visa detaljer

View file

@ -196,7 +196,7 @@ restored = Restored {$items} {$items ->
unknown-folder = แฟ้มที่ไม่รู้จัก unknown-folder = แฟ้มที่ไม่รู้จัก
## Open with ## Open with
open-with = เปิดด้วย... menu-open-with = เปิดด้วย...
default-app = {$name} (ค่าเริ่มต้น) default-app = {$name} (ค่าเริ่มต้น)
## Show details ## Show details

View file

@ -166,7 +166,7 @@ restored = {$items} öge "{$trash}"ten "{$to}" dizinine geri yüklenildi ({$prog
unknown-folder = bilinmeyen klasör unknown-folder = bilinmeyen klasör
## Open with ## Open with
open-with = Birlikte aç... menu-open-with = Birlikte aç...
default-app = {$name} (varsayılan) default-app = {$name} (varsayılan)
## Show details ## Show details

View file

@ -106,7 +106,7 @@ undo = Скасувати
unknown-folder = невідома тека unknown-folder = невідома тека
## Open with ## Open with
open-with = Відкрити за допомогою menu-open-with = Відкрити за допомогою
default-app = {$name} (типово) default-app = {$name} (типово)
## Properties ## Properties

View file

@ -166,7 +166,7 @@ restored = 已从 {trash} 还原 {$items} 个项目
unknown-folder = 未知文件夹 unknown-folder = 未知文件夹
## Open with ## Open with
open-with = 打开方式... menu-open-with = 打开方式...
default-app = {$name} (默认) default-app = {$name} (默认)
## Show details ## Show details

View file

@ -159,7 +159,7 @@ restored = 已還原 {$items} 項目 {$items ->
unknown-folder = 未知資料夾 unknown-folder = 未知資料夾
## Open with ## Open with
open-with = 開啟方式... menu-open-with = 開啟方式...
default-app = {$name} (預設) default-app = {$name} (預設)
## Show details ## Show details

View file

@ -444,7 +444,6 @@ pub enum DialogPage {
OpenWith { OpenWith {
path: PathBuf, path: PathBuf,
mime: mime_guess::Mime, mime: mime_guess::Mime,
apps: Vec<mime_app::MimeApp>,
selected: usize, selected: usize,
store_opt: Option<mime_app::MimeApp>, store_opt: Option<mime_app::MimeApp>,
}, },
@ -514,6 +513,7 @@ pub struct App {
dialog_text_input: widget::Id, dialog_text_input: widget::Id,
key_binds: HashMap<KeyBind, Action>, key_binds: HashMap<KeyBind, Action>,
margin: HashMap<window::Id, (f32, f32, f32, f32)>, margin: HashMap<window::Id, (f32, f32, f32, f32)>,
mime_app_cache: mime_app::MimeAppCache,
modifiers: Modifiers, modifiers: Modifiers,
mounter_items: HashMap<MounterKey, MounterItems>, mounter_items: HashMap<MounterKey, MounterItems>,
network_drive_connecting: Option<(MounterKey, String)>, network_drive_connecting: Option<(MounterKey, String)>,
@ -592,7 +592,7 @@ impl App {
} }
// Try mime apps, which should be faster than xdg-open // 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 { let Some(mut command) = app.command(Some(path.clone().into())) else {
continue; continue;
}; };
@ -1431,21 +1431,24 @@ impl App {
entity_opt: &Option<Entity>, entity_opt: &Option<Entity>,
kind: &'a PreviewKind, kind: &'a PreviewKind,
context_drawer: bool, context_drawer: bool,
) -> Element<'a, Message> { ) -> Element<'a, tab::Message> {
let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing;
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { 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) => { PreviewKind::Location(location) => {
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { 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 // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1459,7 +1462,10 @@ impl App {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { 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 // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1467,7 +1473,10 @@ impl App {
} }
if children.is_empty() { if children.is_empty() {
if let Some(item) = &tab.parent_item_opt { 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(), dialog_text_input: widget::Id::unique(),
key_binds, key_binds,
margin: HashMap::new(), margin: HashMap::new(),
mime_app_cache: mime_app::MimeAppCache::new(),
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
mounter_items: HashMap::new(), mounter_items: HashMap::new(),
network_drive_connecting: None, network_drive_connecting: None,
@ -1688,7 +1698,7 @@ impl Application for App {
NavMenuAction::Open(entity), NavMenuAction::Open(entity),
)); ));
items.push(cosmic::widget::menu::Item::Button( items.push(cosmic::widget::menu::Item::Button(
fl!("open-with"), fl!("menu-open-with"),
None, None,
NavMenuAction::OpenWith(entity), NavMenuAction::OpenWith(entity),
)); ));
@ -2016,11 +2026,11 @@ impl Application for App {
} }
DialogPage::OpenWith { DialogPage::OpenWith {
path, path,
apps, mime,
selected, 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())) { if let Some(mut command) = app.command(Some(path.clone().into())) {
match spawn_detached(&mut command) { match spawn_detached(&mut command) {
Ok(()) => { Ok(()) => {
@ -2345,7 +2355,7 @@ impl Application for App {
} }
}, },
Message::OpenTerminal(entity_opt) => { 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 mut paths = Vec::new();
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
@ -2471,12 +2481,13 @@ impl Application for App {
return self.update(Message::DialogPush(DialogPage::OpenWith { return self.update(Message::DialogPush(DialogPage::OpenWith {
path: path.to_path_buf(), path: path.to_path_buf(),
mime: item.mime.clone(), mime: item.mime.clone(),
apps: item.open_with.clone(),
selected: 0, selected: 0,
store_opt: "x-scheme-handler/mime" store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>() .parse::<mime_guess::Mime>()
.ok() .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); App::exec_entry_action(entry, action);
} }
tab::Command::Iced(iced_command) => { tab::Command::Iced(iced_command) => {
commands.push(iced_command.0.map(move |tab_message| { commands.push(
message::app(Message::TabMessage(Some(entity), tab_message)) iced_command.0.map(move |x| {
})); message::app(Message::TabMessage(Some(entity), x))
}),
);
} }
tab::Command::MoveToTrash(paths) => { tab::Command::MoveToTrash(paths) => {
self.operation(Operation::Delete { paths }); self.operation(Operation::Delete { paths });
@ -2942,6 +2955,10 @@ impl Application for App {
self.context_page = ContextPage::Preview(Some(entity), kind); self.context_page = ContextPage::Preview(Some(entity), kind);
self.set_show_context(true); 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 => { tab::Command::WindowDrag => {
if let Some(window_id) = &self.window_id_opt { if let Some(window_id) = &self.window_id_opt {
commands.push(window::drag(*window_id)); commands.push(window::drag(*window_id));
@ -3282,13 +3299,12 @@ impl Application for App {
return self.update(Message::DialogPush(DialogPage::OpenWith { return self.update(Message::DialogPush(DialogPage::OpenWith {
path: path.to_path_buf(), path: path.to_path_buf(),
mime: item.mime.clone(), mime: item.mime.clone(),
apps: item.open_with.clone(),
selected: 0, selected: 0,
store_opt: "x-scheme-handler/mime" store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>() .parse::<mime_guess::Mime>()
.ok() .ok()
.and_then(|mime| { .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() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { 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( context_drawer::context_drawer(
self.preview(entity_opt, kind, true), self.preview(entity_opt, kind, true)
Message::ToggleContextPage(ContextPage::Preview(*entity_opt, kind.clone())), .map(move |x| Message::TabMessage(Some(entity), x)),
Message::ToggleContextPage(ContextPage::Preview(Some(entity), kind.clone())),
) )
.header_actions(actions) .header_actions(actions)
} }
@ -3556,7 +3575,7 @@ impl Application for App {
if tab.gallery { if tab.gallery {
return Some( return Some(
tab.gallery_view() 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 { DialogPage::OpenWith {
path, path,
apps, mime,
selected, selected,
store_opt, store_opt,
.. ..
@ -3889,7 +3908,7 @@ impl Application for App {
}; };
let mut column = widget::list_column(); 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( column = column.add(
widget::button::custom( widget::button::custom(
widget::row::with_children(vec![ widget::row::with_children(vec![
@ -4026,8 +4045,14 @@ impl Application for App {
let dialog = widget::dialog() let dialog = widget::dialog()
.title(fl!("replace-title", filename = to.name.as_str())) .title(fl!("replace-title", filename = to.name.as_str()))
.body(fl!("replace-warning-operation")) .body(fl!("replace-warning-operation"))
.control(to.replace_view(fl!("original-file"), IconSizes::default())) .control(
.control(from.replace_view(fl!("replace-with"), IconSizes::default())) 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( .primary_action(widget::button::suggested(fl!("replace")).on_press(
Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)), Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)),
)); ));
@ -4365,7 +4390,9 @@ impl Application for App {
}; };
} }
Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(), 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 => { None => {
//TODO: distinct views per monitor in desktop mode //TODO: distinct views per monitor in desktop mode
return self.view_main().map(|message| match message { return self.view_main().map(|message| match message {

View file

@ -471,17 +471,17 @@ impl App {
.into() .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); let mut children = Vec::with_capacity(1);
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { PreviewKind::Custom(PreviewItem(item)) => {
children.push(item.preview_view(IconSizes::default())); children.push(item.preview_view(None, IconSizes::default()));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { 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 // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -493,7 +493,7 @@ impl App {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { 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 // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -501,7 +501,7 @@ impl App {
} }
if children.is_empty() { if children.is_empty() {
if let Some(item) = &self.tab.parent_item_opt { 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( actions.extend(
item.preview_header() item.preview_header()
.into_iter() .into_iter()
.map(|element| element.map(Message::from)), .map(|element| element.map(Message::TabMessage)),
) )
} }
} }
}; };
Some( Some(
context_drawer::context_drawer( context_drawer::context_drawer(
self.preview(kind).map(Message::from), self.preview(kind).map(Message::TabMessage),
Message::Preview, Message::Preview,
) )
.header_actions(actions), .header_actions(actions),

View file

@ -157,7 +157,7 @@ pub fn context_menu<'a>(
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open).into());
} }
if selected == 1 { 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 { if selected_dir == 1 {
children children
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
@ -531,7 +531,7 @@ pub fn menu_bar<'a>(
Action::Open, Action::Open,
(selected > 0 && selected_dir == 0) || (selected_dir == 1 && selected == 1), (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::Item::Divider,
menu_button_optional(fl!("rename"), Action::Rename, selected > 0), menu_button_optional(fl!("rename"), Action::Rename, selected > 0),
menu::Item::Divider, menu::Item::Divider,

View file

@ -5,9 +5,8 @@
use cosmic::desktop; use cosmic::desktop;
use cosmic::widget; use cosmic::widget;
pub use mime_guess::Mime; pub use mime_guess::Mime;
use once_cell::sync::Lazy;
use std::{ 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, time::Instant,
}; };
@ -52,6 +51,13 @@ impl MimeApp {
} }
} }
// This allows usage of MimeApp in a dropdown
impl AsRef<str> for MimeApp {
fn as_ref(&self) -> &str {
&self.name
}
}
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
impl From<&desktop::DesktopEntryData> for MimeApp { impl From<&desktop::DesktopEntryData> for MimeApp {
fn from(app: &desktop::DesktopEntryData) -> Self { fn from(app: &desktop::DesktopEntryData) -> Self {
@ -82,6 +88,7 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
pub struct MimeAppCache { pub struct MimeAppCache {
cache: HashMap<Mime, Vec<MimeApp>>, cache: HashMap<Mime, Vec<MimeApp>>,
icons: HashMap<Mime, Vec<widget::icon::Handle>>,
terminals: Vec<MimeApp>, terminals: Vec<MimeApp>,
} }
@ -89,6 +96,7 @@ impl MimeAppCache {
pub fn new() -> Self { pub fn new() -> Self {
let mut mime_app_cache = Self { let mut mime_app_cache = Self {
cache: HashMap::new(), cache: HashMap::new(),
icons: HashMap::new(),
terminals: Vec::new(), terminals: Vec::new(),
}; };
mime_app_cache.reload(); mime_app_cache.reload();
@ -106,6 +114,7 @@ impl MimeAppCache {
let start = Instant::now(); let start = Instant::now();
self.cache.clear(); self.cache.clear();
self.icons.clear();
self.terminals.clear(); self.terminals.clear();
//TODO: get proper locale? //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(); let elapsed = start.elapsed();
log::info!("loaded mime app cache in {:?}", elapsed); log::info!("loaded mime app cache in {:?}", elapsed);
} }
pub fn get(&self, key: &Mime) -> Vec<MimeApp> { pub fn get(&self, key: &Mime) -> &[MimeApp] {
self.cache.get(key).map_or_else(Vec::new, |x| x.clone()) static EMPTY: Vec<MimeApp> = Vec::new();
self.cache.get(key).unwrap_or_else(|| &EMPTY)
} }
}
static MIME_APP_CACHE: Lazy<Mutex<MimeAppCache>> = Lazy::new(|| Mutex::new(MimeAppCache::new())); pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
static EMPTY: Vec<widget::icon::Handle> = Vec::new();
self.icons.get(key).unwrap_or_else(|| &EMPTY)
}
pub fn mime_apps(mime: &Mime) -> Vec<MimeApp> { pub fn terminal(&self) -> Option<&MimeApp> {
let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
mime_app_cache.get(mime)
}
pub fn terminal() -> Option<MimeApp> { // Look for and return preferred terminals
let mime_app_cache = MIME_APP_CACHE.lock().unwrap(); //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 #[cfg(not(feature = "desktop"))]
//TODO: fallback order beyond cosmic-term? pub fn set_default(&mut self, mime: Mime, id: String) {
for id in &["com.system76.CosmicTerm"] { log::warn!(
for terminal in mime_app_cache.terminals.iter() { "failed to set default handler for {mime:?} to {id:?}: desktop feature not enabled"
if &terminal.id == id { );
return Some(terminal.clone()); }
#[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()
} }

View file

@ -127,7 +127,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),

View file

@ -58,14 +58,13 @@ use tokio::sync::mpsc;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::{ use crate::{
app::{self, Action, PreviewItem, PreviewKind}, app::{Action, PreviewItem, PreviewKind},
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID}, config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID},
dialog::DialogKind, dialog::DialogKind,
fl, fl,
localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER}, localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER},
menu, menu, mime_app,
mime_app::{mime_apps, MimeApp},
mime_icon::{mime_for_path, mime_icon}, mime_icon::{mime_for_path, mime_icon},
mounter::MOUNTERS, mounter::MOUNTERS,
mouse_area, mouse_area,
@ -459,8 +458,6 @@ pub fn item_from_entry(
} }
}; };
let open_with = mime_apps(&mime);
let children = if metadata.is_dir() { let children = if metadata.is_dir() {
//TODO: calculate children in the background (and make it cancellable?) //TODO: calculate children in the background (and make it cancellable?)
match fs::read_dir(&path) { match fs::read_dir(&path) {
@ -490,7 +487,6 @@ pub fn item_from_entry(
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with,
thumbnail_opt: None, thumbnail_opt: None,
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -720,7 +716,6 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -904,7 +899,6 @@ pub fn scan_desktop(
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -1027,6 +1021,7 @@ pub enum Command {
OpenInNewWindow(PathBuf), OpenInNewWindow(PathBuf),
OpenTrash, OpenTrash,
Preview(PreviewKind), Preview(PreviewKind),
SetOpenWith(Mime, String),
WindowDrag, WindowDrag,
WindowToggleMaximize, WindowToggleMaximize,
} }
@ -1073,6 +1068,7 @@ pub enum Message {
SelectAll, SelectAll,
SelectFirst, SelectFirst,
SelectLast, SelectLast,
SetOpenWith(Mime, String),
SetSort(HeadingOptions, bool), SetSort(HeadingOptions, bool),
Thumbnail(PathBuf, ItemThumbnail), Thumbnail(PathBuf, ItemThumbnail),
ToggleShowHidden, ToggleShowHidden,
@ -1318,7 +1314,6 @@ pub struct Item {
pub icon_handle_grid: widget::icon::Handle, pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle,
pub icon_handle_list_condensed: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle,
pub open_with: Vec<MimeApp>,
pub thumbnail_opt: Option<ItemThumbnail>, pub thumbnail_opt: Option<ItemThumbnail>,
pub button_id: widget::Id, pub button_id: widget::Id,
pub pos_opt: Cell<Option<(usize, usize)>>, pub pos_opt: Cell<Option<(usize, usize)>>,
@ -1343,7 +1338,7 @@ impl Item {
self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT 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; let spacing = cosmic::theme::active().cosmic().spacing;
// This loads the image only if thumbnailing worked // This loads the image only if thumbnailing worked
let icon = widget::icon::icon(self.icon_handle_grid.clone()) let icon = widget::icon::icon(self.icon_handle_grid.clone())
@ -1376,23 +1371,23 @@ impl Item {
} }
} }
pub fn preview_header(&self) -> Vec<Element<app::Message>> { pub fn preview_header(&self) -> Vec<Element<Message>> {
let mut row = Vec::with_capacity(3); let mut row = Vec::with_capacity(3);
row.push( row.push(
widget::button::icon(widget::icon::from_name("go-previous-symbolic")) widget::button::icon(widget::icon::from_name("go-previous-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemLeft)) .on_press(Message::ItemLeft)
.into(), .into(),
); );
row.push( row.push(
widget::button::icon(widget::icon::from_name("go-next-symbolic")) widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemRight)) .on_press(Message::ItemRight)
.into(), .into(),
); );
if self.can_gallery() { if self.can_gallery() {
if let Some(_path) = self.path_opt() { if let Some(_path) = self.path_opt() {
row.push( row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) 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(), .into(),
); );
} }
@ -1400,7 +1395,11 @@ impl Item {
row 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 { let cosmic_theme::Spacing {
space_xxxs, space_xxxs,
space_m, space_m,
@ -1422,6 +1421,24 @@ impl Item {
mime = self.mime.to_string() mime = self.mime.to_string()
))); )));
let mut settings = Vec::new(); 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 { match &self.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path { metadata, children } => {
if metadata.is_dir() { if metadata.is_dir() {
@ -1509,9 +1526,10 @@ impl Item {
column = column.push(details); column = column.push(details);
if let Some(path) = self.path_opt() { if let Some(path) = self.path_opt() {
column = column.push(widget::button::standard(fl!("open")).on_press( column = column.push(
app::Message::TabMessage(None, Message::Open(Some(path.to_path_buf()))), widget::button::standard(fl!("open"))
)); .on_press(Message::Open(Some(path.to_path_buf()))),
);
} }
if !settings.is_empty() { if !settings.is_empty() {
@ -1525,11 +1543,7 @@ impl Item {
column.into() column.into()
} }
pub fn replace_view<'a>( pub fn replace_view<'a>(&'a self, heading: String, sizes: IconSizes) -> Element<'a, Message> {
&'a self,
heading: String,
sizes: IconSizes,
) -> Element<'a, app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut row = widget::row().spacing(space_xxxs); 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) => { Message::SetSort(heading_option, dir) => {
if !matches!(self.location, Location::Search(..)) { if !matches!(self.location, Location::Search(..)) {
self.sort_name = heading_option; self.sort_name = heading_option;
@ -4432,7 +4449,7 @@ impl Tab {
dnd_dest.into() dnd_dest.into()
} }
pub fn view<'a>(&'a self, key_binds: &'a HashMap<KeyBind, Action>) -> Element<Message> { pub fn view<'a>(&'a self, key_binds: &'a HashMap<KeyBind, Action>) -> Element<'a, Message> {
widget::responsive(|size| self.view_responsive(key_binds, size)).into() widget::responsive(|size| self.view_responsive(key_binds, size)).into()
} }