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
510use gtk:: { gio, glib, prelude:: * } ;
611
712use super :: {
813 blur:: BlurBin ,
914 browser:: { dismiss_modal_layer, modal_layer, show_error_dialog} ,
1015 controls:: modal_layout,
16+ recent_apps,
1117} ;
1218
1319pub ( 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+
3848fn 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+
83134pub ( super ) struct Applications {
84135 pub ( super ) recommended : Vec < gio:: AppInfo > ,
85136 pub ( super ) other : Vec < gio:: AppInfo > ,
@@ -174,14 +225,16 @@ fn common_applications(
174225pub ( 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
182234fn 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(
345405struct 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 ) ]
353420pub ( 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