add search field in 'open with' dialog (#1943)

The "Open With" context menu item, which allows you to open a file, now
includes a search option by application name.

Demo:


https://github.com/user-attachments/assets/b4ff5da3-4bdf-4a0f-a20b-b6783f293fef



- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
This commit is contained in:
Jeremy Soller 2026-08-19 16:01:52 -06:00 • committed by GitHub
commit 3aa55b07b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 17 deletions

View file

@ -293,6 +293,7 @@ unknown-folder = unknown folder
## Open with ## Open with
menu-open-with = Open with... menu-open-with = Open with...
default-app = {$name} (default) default-app = {$name} (default)
search-application = Search by app name
## Show details ## Show details
show-details = Show details show-details = Show details

View file

@ -174,6 +174,7 @@ unknown-folder = неизвестная папка
menu-open-with = Открыть с помощью… menu-open-with = Открыть с помощью…
default-app = { $name } (по умолчанию) default-app = { $name } (по умолчанию)
search-application = Поиск по названию приложения
## Show details ## Show details

View file

@ -37,7 +37,7 @@ use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::segmented_button::{self, Entity, ReorderEvent}; use cosmic::widget::segmented_button::{self, Entity, ReorderEvent};
use cosmic::widget::{self, icon, settings, space}; use cosmic::widget::{self, icon, settings, space};
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme}; use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, surface, theme};
use mime_guess::Mime; use mime_guess::Mime;
use notify_debouncer_full::notify::{self, RecommendedWatcher}; use notify_debouncer_full::notify::{self, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
@ -395,6 +395,7 @@ pub enum Message {
OpenWithBrowse, OpenWithBrowse,
OpenWithDialog(Option<Entity>), OpenWithDialog(Option<Entity>),
OpenWithSelection(usize), OpenWithSelection(usize),
OpenWithSearchClear,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Overlap(window::Id, OverlapNotifyEvent), Overlap(window::Id, OverlapNotifyEvent),
Paste(Option<Entity>), Paste(Option<Entity>),
@ -566,6 +567,7 @@ pub enum DialogPage {
mime: mime_guess::Mime, mime: mime_guess::Mime,
selected: usize, selected: usize,
store_opt: Option<Arc<MimeApp>>, store_opt: Option<Arc<MimeApp>>,
search_app_name: String,
}, },
PermanentlyDelete { PermanentlyDelete {
paths: Box<[PathBuf]>, paths: Box<[PathBuf]>,
@ -3209,9 +3211,17 @@ impl Application for App {
path, path,
mime, mime,
selected, selected,
search_app_name,
.. ..
} => { } => {
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true); let mut available_apps =
self.mime_app_cache.get_apps_for_mime(&mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
if let Some((app, _)) = available_apps.get(selected) { if let Some((app, _)) = available_apps.get(selected) {
if let Some(mut command) = if let Some(mut command) =
@ -3838,20 +3848,24 @@ impl Application for App {
let Some(path) = item.path_opt() else { let Some(path) = item.path_opt() else {
continue; continue;
}; };
return self.push_dialog( return Task::batch([
DialogPage::OpenWith { self.push_dialog(
path: path.clone(), DialogPage::OpenWith {
mime: item.mime.clone(), path: path.clone(),
selected: 0, mime: item.mime.clone(),
store_opt: "x-scheme-handler/mime" selected: 0,
.parse::<mime_guess::Mime>() store_opt: "x-scheme-handler/mime"
.ok() .parse::<mime_guess::Mime>()
.and_then(|mime| { .ok()
self.mime_app_cache.get(&mime).first().cloned() .and_then(|mime| {
}), self.mime_app_cache.get(&mime).first().cloned()
}, }),
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), search_app_name: String::new(),
); },
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
} }
} }
} }
@ -3860,6 +3874,14 @@ impl Application for App {
*selected = index; *selected = index;
} }
} }
Message::OpenWithSearchClear => {
if let Some(DialogPage::OpenWith {
search_app_name, ..
}) = self.dialog_pages.front_mut()
{
*search_app_name = String::new();
}
}
Message::Paste(entity_opt) => { Message::Paste(entity_opt) => {
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)
@ -5114,6 +5136,7 @@ impl Application for App {
.and_then(|mime| { .and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned() self.mime_app_cache.get(&mime).first().cloned()
}), }),
search_app_name: String::new(),
}, },
None, None,
); );
@ -5970,6 +5993,7 @@ impl Application for App {
mime, mime,
selected, selected,
store_opt, store_opt,
search_app_name,
.. ..
} => { } => {
let name = match path.file_name() { let name = match path.file_name() {
@ -5978,7 +6002,13 @@ impl Application for App {
}; };
let mut column = widget::list_column(); let mut column = widget::list_column();
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true); let mut available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
let item_height = 32.0; let item_height = 32.0;
let mut displayed_default = false; let mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact; let mut last_kind = MimeAppMatch::Exact;
@ -6042,6 +6072,24 @@ impl Application for App {
.secondary_action( .secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
) )
.control(
widget::text_input::search_input(
fl!("search-application"),
search_app_name,
)
.id(self.dialog_text_input.clone())
.on_clear(Message::OpenWithSearchClear)
.on_input(move |search_app_name| {
Message::DialogUpdate(DialogPage::OpenWith {
path: path.clone(),
mime: mime.clone(),
selected: *selected,
store_opt: store_opt.clone(),
search_app_name,
})
})
.on_submit(|_| Message::DialogComplete),
)
.control(widget::scrollable(column).height({ .control(widget::scrollable(column).height({
let max_size = self let max_size = self
.size .size