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
menu-open-with = Open with...
default-app = {$name} (default)
search-application = Search by app name
## Show details
show-details = Show details

View file

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

View file

@ -37,7 +37,7 @@ use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::segmented_button::{self, Entity, ReorderEvent};
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 notify_debouncer_full::notify::{self, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
@ -395,6 +395,7 @@ pub enum Message {
OpenWithBrowse,
OpenWithDialog(Option<Entity>),
OpenWithSelection(usize),
OpenWithSearchClear,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Overlap(window::Id, OverlapNotifyEvent),
Paste(Option<Entity>),
@ -566,6 +567,7 @@ pub enum DialogPage {
mime: mime_guess::Mime,
selected: usize,
store_opt: Option<Arc<MimeApp>>,
search_app_name: String,
},
PermanentlyDelete {
paths: Box<[PathBuf]>,
@ -3209,9 +3211,17 @@ impl Application for App {
path,
mime,
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(mut command) =
@ -3838,7 +3848,8 @@ impl Application for App {
let Some(path) = item.path_opt() else {
continue;
};
return self.push_dialog(
return Task::batch([
self.push_dialog(
DialogPage::OpenWith {
path: path.clone(),
mime: item.mime.clone(),
@ -3849,9 +3860,12 @@ impl Application for App {
.and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
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;
}
}
Message::OpenWithSearchClear => {
if let Some(DialogPage::OpenWith {
search_app_name, ..
}) = self.dialog_pages.front_mut()
{
*search_app_name = String::new();
}
}
Message::Paste(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
@ -5114,6 +5136,7 @@ impl Application for App {
.and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
search_app_name: String::new(),
},
None,
);
@ -5970,6 +5993,7 @@ impl Application for App {
mime,
selected,
store_opt,
search_app_name,
..
} => {
let name = match path.file_name() {
@ -5978,7 +6002,13 @@ impl Application for App {
};
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 mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact;
@ -6042,6 +6072,24 @@ impl Application for App {
.secondary_action(
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({
let max_size = self
.size