Skip to content

Commit bd097a3

Browse files
fadilasifl0gicgate
andauthored
feat(open-with): add recently used applications (lgse#1354)
* feat(open-with): add recently used applications * fix(open-with): recover exhausted history counters and tighten coverage --------- Co-authored-by: Pierre Berube <pierre@lgse.com>
1 parent 830d55a commit bd097a3

7 files changed

Lines changed: 1031 additions & 19 deletions

File tree

‎src/ui/browser/context_menu.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,12 @@ pub(in crate::ui) fn install_resolved_item_context_menu(
11841184
return;
11851185
};
11861186
let context = state.overlay.display().app_launch_context();
1187-
if let Err(error) = crate::ui::open_with::launch(&app, &selection.files, Some(&context)) {
1187+
if let Err(error) = crate::ui::open_with::launch(
1188+
&app,
1189+
&selection.files,
1190+
&selection.content_types,
1191+
Some(&context),
1192+
) {
11881193
crate::ui::modal::show_error_dialog(
11891194
&state.overlay,
11901195
"Unable to open file",

‎src/ui/browser/desktop.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ pub(in crate::ui) fn open_location(
3434
let browser = Rc::downgrade(browser);
3535
glib::MainContext::default().spawn_local(async move {
3636
match resolve_default_application(&file).await {
37-
Ok((_content_type, Some(app))) => {
37+
Ok((content_type, Some(app))) => {
3838
let result = crate::ui::open_with::launch(
3939
&app,
4040
std::slice::from_ref(&file),
41+
std::slice::from_ref(&content_type),
4142
None::<&gio::AppLaunchContext>,
4243
);
4344
if let Some(parent) = parent.upgrade() {

‎src/ui/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mod portal_preferences;
3232
pub(crate) mod preferences;
3333
mod preview;
3434
mod raw_details;
35+
mod recent_apps;
3536
mod scrolling;
3637
mod search;
3738
mod search_session;

‎src/ui/open_with.rs‎

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
// SPDX-License-Identifier: MIT
22

3-
use std::{cell::Cell, rc::Rc, time::Duration};
3+
use std::{
4+
cell::Cell,
5+
collections::{HashMap, HashSet},
6+
rc::Rc,
7+
time::Duration,
8+
};
49

510
use gtk::{gio, glib, prelude::*};
611

712
use super::{
813
blur::BlurBin,
914
browser::{dismiss_modal_layer, modal_layer, show_error_dialog},
1015
controls::modal_layout,
16+
recent_apps,
1117
};
1218

1319
pub(super) fn categorized_apps(
@@ -35,14 +41,18 @@ fn path_requires_uri_handlers(path: Option<&std::path::Path>) -> bool {
3541
path.is_none()
3642
}
3743

44+
fn app_is_candidate(app: &gio::AppInfo, requires_uris: bool) -> bool {
45+
app.should_show() && (!requires_uris || app.supports_uris())
46+
}
47+
3848
fn filter_apps(
3949
apps: Vec<gio::AppInfo>,
4050
default: Option<gio::AppInfo>,
4151
requires_uris: bool,
4252
) -> Vec<gio::AppInfo> {
4353
let mut apps = apps
4454
.into_iter()
45-
.filter(|app| app.should_show() && (!requires_uris || app.supports_uris()))
55+
.filter(|app| app_is_candidate(app, requires_uris))
4656
.collect::<Vec<_>>();
4757
apps.sort_by_cached_key(|app| app.display_name().to_lowercase());
4858
let mut unique = Vec::with_capacity(apps.len());
@@ -65,9 +75,7 @@ pub(super) fn filter_other_apps(
6575
let mut apps = apps
6676
.into_iter()
6777
.filter(|app| {
68-
app.should_show()
69-
&& (!requires_uris || app.supports_uris())
70-
&& !recommended.iter().any(|rec| rec.equal(app))
78+
app_is_candidate(app, requires_uris) && !recommended.iter().any(|rec| rec.equal(app))
7179
})
7280
.collect::<Vec<_>>();
7381
apps.sort_by_cached_key(|app| app.display_name().to_lowercase());
@@ -80,6 +88,49 @@ pub(super) fn filter_other_apps(
8088
unique
8189
}
8290

91+
fn app_ids(apps: &[gio::AppInfo]) -> HashSet<String> {
92+
apps.iter()
93+
.filter_map(|app| app.id().map(|id| id.to_string()))
94+
.collect()
95+
}
96+
97+
// For mixed types, only promote handlers shared by every type. Single-type
98+
// history may also promote applications explicitly chosen from Other.
99+
fn split_recent(
100+
content_types: &[String],
101+
recommended: &mut Vec<gio::AppInfo>,
102+
other: &mut Vec<gio::AppInfo>,
103+
) -> Vec<gio::AppInfo> {
104+
let ordered = recent_apps::load().recent_ids(content_types);
105+
if ordered.is_empty() {
106+
return Vec::new();
107+
}
108+
let mut eligible = app_ids(recommended);
109+
if content_types.len() <= 1 {
110+
eligible.extend(app_ids(other));
111+
}
112+
let mut wanted = recent_apps::select_recent(&ordered, &eligible);
113+
wanted.truncate(recent_apps::MAX_PER_TYPE);
114+
if wanted.is_empty() {
115+
return Vec::new();
116+
}
117+
let mut by_id: HashMap<String, gio::AppInfo> = HashMap::new();
118+
for app in recommended.iter().chain(other.iter()) {
119+
if let Some(id) = app.id() {
120+
by_id.entry(id.to_string()).or_insert_with(|| app.clone());
121+
}
122+
}
123+
let recent: Vec<gio::AppInfo> = wanted.iter().filter_map(|id| by_id.remove(id)).collect();
124+
if recent.is_empty() {
125+
return Vec::new();
126+
}
127+
let recent_ids: HashSet<String> = wanted.into_iter().collect();
128+
for apps in [recommended, other] {
129+
apps.retain(|app| app.id().is_none_or(|id| !recent_ids.contains(id.as_str())));
130+
}
131+
recent
132+
}
133+
83134
pub(super) struct Applications {
84135
pub(super) recommended: Vec<gio::AppInfo>,
85136
pub(super) other: Vec<gio::AppInfo>,
@@ -174,14 +225,16 @@ fn common_applications(
174225
pub(super) fn launch(
175226
app: &gio::AppInfo,
176227
files: &[gio::File],
228+
content_types: &[String],
177229
context: Option<&impl IsA<gio::AppLaunchContext>>,
178230
) -> Result<(), glib::Error> {
179-
launch_with_recent_registration(app, files, context, register_recent_file)
231+
launch_with_recent_registration(app, files, content_types, context, register_recent_file)
180232
}
181233

182234
fn launch_with_recent_registration(
183235
app: &gio::AppInfo,
184236
files: &[gio::File],
237+
content_types: &[String],
185238
context: Option<&impl IsA<gio::AppLaunchContext>>,
186239
register_recent: impl Fn(&gio::File) -> bool + 'static,
187240
) -> Result<(), glib::Error> {
@@ -193,6 +246,13 @@ fn launch_with_recent_registration(
193246
));
194247
}
195248
app.launch(files, context)?;
249+
if let Some(app_id) = app.id() {
250+
let known_ids = gio::AppInfo::all()
251+
.into_iter()
252+
.filter_map(|candidate| candidate.id().map(|id| id.to_string()))
253+
.collect::<HashSet<_>>();
254+
recent_apps::record(content_types, app_id.as_str(), &known_ids);
255+
}
196256
let candidates: Vec<gio::File> = files
197257
.iter()
198258
.filter(|file| !file.has_uri_scheme("recent"))
@@ -345,10 +405,17 @@ fn install_list_tab_navigation(
345405
struct AppEntry {
346406
app: gio::AppInfo,
347407
row: gtk::ListBoxRow,
348-
is_recommended: bool,
408+
section: AppSection,
349409
haystack: String,
350410
}
351411

412+
#[derive(Clone, Copy, PartialEq, Eq)]
413+
enum AppSection {
414+
Recent,
415+
Recommended,
416+
Other,
417+
}
418+
352419
#[derive(Clone, Copy)]
353420
pub(super) enum OpenWithContext {
354421
Explicit,
@@ -418,8 +485,8 @@ pub(super) fn show(
418485
parent: &impl IsA<gtk::Widget>,
419486
files: Vec<gio::File>,
420487
content_types: Vec<String>,
421-
recommended_apps: Vec<gio::AppInfo>,
422-
other_apps: Vec<gio::AppInfo>,
488+
mut recommended_apps: Vec<gio::AppInfo>,
489+
mut other_apps: Vec<gio::AppInfo>,
423490
context: OpenWithContext,
424491
on_close: Rc<dyn Fn()>,
425492
) {
@@ -481,6 +548,34 @@ pub(super) fn show(
481548

482549
let mut entries = Vec::new();
483550

551+
let mut recent_heading_row = None;
552+
let recent = split_recent(&content_types, &mut recommended_apps, &mut other_apps);
553+
if !recent.is_empty() {
554+
let heading = create_section_header("Recently Used");
555+
list.append(&heading);
556+
recent_heading_row = Some(heading);
557+
558+
for app in &recent {
559+
let row = create_app_row(app, &list.display());
560+
let haystack = format!(
561+
"{} {} {} {} {}",
562+
app.display_name(),
563+
app.name(),
564+
app.description().unwrap_or_default(),
565+
app.executable().to_string_lossy(),
566+
app.id().unwrap_or_default(),
567+
)
568+
.to_lowercase();
569+
list.append(&row);
570+
entries.push(AppEntry {
571+
app: app.clone(),
572+
row,
573+
section: AppSection::Recent,
574+
haystack,
575+
});
576+
}
577+
}
578+
484579
let mut recommended_heading_row = None;
485580
if !recommended_apps.is_empty() {
486581
let heading = create_section_header("Recommended Applications");
@@ -502,7 +597,7 @@ pub(super) fn show(
502597
entries.push(AppEntry {
503598
app: app.clone(),
504599
row,
505-
is_recommended: true,
600+
section: AppSection::Recommended,
506601
haystack,
507602
});
508603
}
@@ -529,7 +624,7 @@ pub(super) fn show(
529624
entries.push(AppEntry {
530625
app: app.clone(),
531626
row,
532-
is_recommended: false,
627+
section: AppSection::Other,
533628
haystack,
534629
});
535630
}
@@ -698,6 +793,7 @@ pub(super) fn show(
698793
let empty_search_for_filter = empty_search.downgrade();
699794
let confirm_for_filter = layout.confirm.downgrade();
700795
let rec_heading = recommended_heading_row;
796+
let recent_heading = recent_heading_row;
701797
let oth_heading = other_heading_row;
702798

703799
search_entry.connect_changed(move |search| {
@@ -710,6 +806,7 @@ pub(super) fn show(
710806
return;
711807
};
712808
let query = search.text().trim().to_lowercase();
809+
let mut recent_count = 0;
713810
let mut rec_count = 0;
714811
let mut oth_count = 0;
715812
let mut first_visible_row: Option<gtk::ListBoxRow> = None;
@@ -720,10 +817,10 @@ pub(super) fn show(
720817
let matches = query.is_empty() || entry.haystack.contains(&query);
721818
entry.row.set_visible(matches);
722819
if matches {
723-
if entry.is_recommended {
724-
rec_count += 1;
725-
} else {
726-
oth_count += 1;
820+
match entry.section {
821+
AppSection::Recent => recent_count += 1,
822+
AppSection::Recommended => rec_count += 1,
823+
AppSection::Other => oth_count += 1,
727824
}
728825
if first_visible_row.is_none() {
729826
first_visible_row = Some(entry.row.clone());
@@ -736,14 +833,17 @@ pub(super) fn show(
736833
}
737834
}
738835

836+
if let Some(heading) = &recent_heading {
837+
heading.set_visible(recent_count > 0);
838+
}
739839
if let Some(heading) = &rec_heading {
740840
heading.set_visible(rec_count > 0);
741841
}
742842
if let Some(heading) = &oth_heading {
743843
heading.set_visible(oth_count > 0);
744844
}
745845

746-
let total_matches = rec_count + oth_count;
846+
let total_matches = recent_count + rec_count + oth_count;
747847
if total_matches == 0 {
748848
empty_label.set_visible(true);
749849
list_scroll.set_visible(false);
@@ -810,7 +910,7 @@ pub(super) fn show(
810910
}
811911
}
812912
let context = list.display().app_launch_context();
813-
if let Err(error) = launch(&entry.app, &open_files, Some(&context)) {
913+
if let Err(error) = launch(&entry.app, &open_files, &open_content_types, Some(&context)) {
814914
let detail = error.to_string();
815915
open_dismiss();
816916
let open_parent = open_parent.clone();

0 commit comments

Comments
 (0)