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"
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"

View file

@ -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"]

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

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
## 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -444,7 +444,6 @@ pub enum DialogPage {
OpenWith {
path: PathBuf,
mime: mime_guess::Mime,
apps: Vec<mime_app::MimeApp>,
selected: usize,
store_opt: Option<mime_app::MimeApp>,
},
@ -514,6 +513,7 @@ pub struct App {
dialog_text_input: widget::Id,
key_binds: HashMap<KeyBind, Action>,
margin: HashMap<window::Id, (f32, f32, f32, f32)>,
mime_app_cache: mime_app::MimeAppCache,
modifiers: Modifiers,
mounter_items: HashMap<MounterKey, MounterItems>,
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<Entity>,
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::<Tab>(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::<Tab>(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::<mime_guess::Mime>()
.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::<mime_guess::Mime>()
.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 {

View file

@ -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),

View file

@ -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,

View file

@ -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<str> 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<PathBuf>, filename: &str) -> bool {
pub struct MimeAppCache {
cache: HashMap<Mime, Vec<MimeApp>>,
icons: HashMap<Mime, Vec<widget::icon::Handle>>,
terminals: Vec<MimeApp>,
}
@ -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<MimeApp> {
self.cache.get(key).map_or_else(Vec::new, |x| x.clone())
pub fn get(&self, key: &Mime) -> &[MimeApp] {
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> {
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<MimeApp> {
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()
}

View file

@ -127,7 +127,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, 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),

View file

@ -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<Item> {
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<MimeApp>,
pub thumbnail_opt: Option<ItemThumbnail>,
pub button_id: widget::Id,
pub pos_opt: Cell<Option<(usize, usize)>>,
@ -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<Element<app::Message>> {
pub fn preview_header(&self) -> Vec<Element<Message>> {
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<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()
}