11use druid:: widget:: { Button , Flex , Label , List , Scroll , TextBox } ;
2- use druid:: widget:: prelude:: * ;
32use druid:: {
43 AppDelegate , AppLauncher , Command , Data , DelegateCtx , Env , Lens , Selector , Target ,
5- Widget , WidgetExt , WindowDesc ,
4+ Widget , WidgetExt , WindowDesc , commands , FileDialogOptions , theme ,
65} ;
76use regex:: Regex ;
8- use std:: path:: PathBuf ;
7+ use std:: path:: { Path , PathBuf } ; // added Path
98use std:: sync:: Arc ;
109use std:: thread;
1110use walkdir:: WalkDir ;
11+ use std:: fs;
12+
13+ #[ cfg( target_os = "macos" ) ]
14+ fn open_path ( path : & str ) {
15+ std:: process:: Command :: new ( "open" )
16+ . arg ( path)
17+ . spawn ( )
18+ . expect ( "failed to open file" ) ;
19+ }
20+
21+ #[ cfg( target_os = "windows" ) ]
22+ fn open_path ( path : & str ) {
23+ std:: process:: Command :: new ( "explorer" )
24+ . arg ( path)
25+ . spawn ( )
26+ . expect ( "failed to open file" ) ;
27+ }
1228
1329// A selector for updating search results from a background thread.
1430// Note: Now the payload is an Arc<Vec<String>>
@@ -25,78 +41,97 @@ struct AppState {
2541
2642fn build_ui ( ) -> impl Widget < AppState > {
2743 // Button to let the user choose a directory (using rfd for a native dialog)
28- let choose_dir_btn = Button :: new ( "Choose Directory" ) . on_click ( |_ctx, data : & mut AppState , _env| {
29- // Use rfd's file dialog (this will show a native folder chooser on macOS)
30- if let Some ( path) = rfd:: FileDialog :: new ( ) . pick_folder ( ) {
31- data. root_path = path. to_string_lossy ( ) . to_string ( ) ;
32- // Clear any previous search results when the directory changes.
33- data. search_results = Arc :: new ( Vec :: new ( ) ) ;
34- }
35- } ) ;
44+ let choose_dir_btn = Button :: new ( "Choose Directory" )
45+ . padding ( 8.0 )
46+ . background ( theme:: BUTTON_DARK )
47+ . on_click ( |ctx, _data, _env| {
48+ ctx. submit_command ( Command :: new ( commands:: SHOW_OPEN_PANEL , FileDialogOptions :: default ( ) , Target :: Auto ) ) ;
49+ } ) ;
3650
37- // A label showing the currently selected directory.
38- let dir_label = Label :: new ( |data : & AppState , _env : & _ | {
39- format ! ( "Current Directory: {}" , data. root_path)
40- } )
41- . with_text_size ( 14.0 ) ;
51+ // Replace the static label with an editable text box for directory input.
52+ let directory_box = TextBox :: new ( )
53+ . with_placeholder ( "Enter directory path" )
54+ . with_text_size ( 14.0 )
55+ . padding ( 8.0 )
56+ . lens ( AppState :: root_path) ;
4257
4358 // Text box for entering the search term.
4459 let search_box = TextBox :: new ( )
4560 . with_placeholder ( "Enter search term" )
61+ . with_text_size ( 14.0 )
62+ . padding ( 8.0 )
4663 . lens ( AppState :: search_term) ;
4764
4865 // Button to kick off the search.
49- let search_btn = Button :: new ( "Search" ) . on_click ( |ctx, data : & mut AppState , _env| {
50- let root = data. root_path . clone ( ) ;
51- let term = data. search_term . clone ( ) ;
52-
53- // Clear any previous search results.
54- data. search_results = Arc :: new ( Vec :: new ( ) ) ;
66+ let search_btn = Button :: new ( "Search" )
67+ . padding ( 8.0 )
68+ . background ( theme:: BUTTON_DARK )
69+ . on_click ( |ctx, data : & mut AppState , _env| {
70+ let root = data. root_path . clone ( ) ;
71+ let term = data. search_term . clone ( ) ;
72+
73+ // Clear any previous search results.
74+ data. search_results = Arc :: new ( Vec :: new ( ) ) ;
5575
56- let sink = ctx. get_external_handle ( ) ;
76+ let sink = ctx. get_external_handle ( ) ;
5777
58- thread:: spawn ( move || {
59- let results = search_files ( & root, & term) ;
60- // Send the search results back to the UI thread.
61- sink. submit_command ( UPDATE_SEARCH_RESULTS , results, Target :: Auto )
62- . expect ( "Failed to submit command" ) ;
78+ thread:: spawn ( move || {
79+ let results = search_files ( & root, & term) ;
80+ // Send the search results back to the UI thread.
81+ sink. submit_command ( UPDATE_SEARCH_RESULTS , results, Target :: Auto )
82+ . expect ( "Failed to submit command" ) ;
83+ } ) ;
6384 } ) ;
64- } ) ;
6585
6686 // Create a list widget to display search results.
6787 let results_list = List :: new ( || {
6888 Label :: new ( |item : & String , _env : & _ | format ! ( "{}" , item) )
69- . padding ( 5.0 )
89+ . with_text_size ( 14.0 )
90+ . padding ( 6.0 )
91+ . on_click ( |_ctx, item : & mut String , _env| {
92+ open_path ( item) ;
93+ } )
7094 } )
71- . with_spacing ( 2 .0)
95+ . with_spacing ( 4 .0)
7296 // Lens into the search_results field (which is now an Arc<Vec<String>>)
7397 . lens ( AppState :: search_results) ;
7498
7599 // Layout the UI elements vertically.
76100 Flex :: column ( )
77- . with_child ( choose_dir_btn. padding ( 5.0 ) )
78- . with_child ( dir_label. padding ( 5.0 ) )
79- . with_child ( search_box. padding ( 5.0 ) )
80- . with_child ( search_btn. padding ( 5.0 ) )
81- . with_flex_child ( Scroll :: new ( results_list) , 1.0 )
101+ . with_child ( choose_dir_btn. padding ( 8.0 ) )
102+ . with_child ( directory_box. padding ( 8.0 ) ) // new text box for directory input
103+ . with_child ( search_box. padding ( 8.0 ) )
104+ . with_child ( search_btn. padding ( 8.0 ) )
105+ . with_flex_child ( Scroll :: new ( results_list) . expand ( ) , 1.0 )
106+ . padding ( 12.0 )
107+ . background ( theme:: WINDOW_BACKGROUND_COLOR )
82108}
83109
84- /// Searches files under the given directory whose names match the search term (case-insensitive)
110+ /// Searches files and directories under the given directory whose names match the search term (case-insensitive)
85111/// and returns an Arc<Vec<String>>.
86112fn search_files ( root_path : & str , search_term : & str ) -> Arc < Vec < String > > {
87113 let regex = Regex :: new ( & format ! ( r"(?i){}" , search_term) ) . unwrap ( ) ;
88114 let root = PathBuf :: from ( root_path) ;
115+ let results = search_files_recursive ( & root, & regex) ;
116+ Arc :: new ( results)
117+ }
118+
119+ fn search_files_recursive ( dir : & Path , regex : & Regex ) -> Vec < String > {
89120 let mut results = Vec :: new ( ) ;
90- for entry in WalkDir :: new ( root) . into_iter ( ) . filter_map ( |e| e. ok ( ) ) {
91- if entry. path ( ) . is_file ( ) {
92- if let Some ( name) = entry. path ( ) . file_name ( ) . and_then ( |n| n. to_str ( ) ) {
93- if regex. is_match ( name) {
94- results. push ( entry. path ( ) . display ( ) . to_string ( ) ) ;
121+ if dir. is_dir ( ) {
122+ for entry in fs:: read_dir ( dir) . expect ( "read_dir call failed" ) {
123+ if let Ok ( entry) = entry {
124+ if entry. path ( ) . is_file ( ) || entry. path ( ) . is_dir ( ) {
125+ if let Some ( name) = entry. path ( ) . file_name ( ) . and_then ( |n| n. to_str ( ) ) {
126+ if regex. is_match ( name) {
127+ results. push ( entry. path ( ) . display ( ) . to_string ( ) ) ;
128+ }
129+ }
95130 }
96131 }
97132 }
98133 }
99- Arc :: new ( results)
134+ results
100135}
101136
102137/// A delegate to handle commands coming from the background thread.
@@ -115,6 +150,15 @@ impl AppDelegate<AppState> for Delegate {
115150 data. search_results = results. clone ( ) ;
116151 return druid:: Handled :: Yes ;
117152 }
153+ if cmd. is ( commands:: SHOW_OPEN_PANEL ) {
154+ let dialog = rfd:: FileDialog :: new ( ) ;
155+ if let Some ( folder) = dialog. pick_folder ( ) {
156+ data. root_path = folder. to_string_lossy ( ) . to_string ( ) ;
157+ data. search_results = Arc :: new ( Vec :: new ( ) ) ;
158+ return druid:: Handled :: Yes ;
159+ }
160+ // Removed file selection to force folder-only selection.
161+ }
118162 druid:: Handled :: No
119163 }
120164}
0 commit comments