@@ -92,6 +92,134 @@ fn clear_config_patterns() {
9292 }
9393}
9494
95+ fn search_files ( pattern : & str , start_path : & Path ) {
96+ // Convert glob pattern to regex
97+ let regex_pattern = pattern. replace ( "*" , ".*" ) . replace ( "?" , "." ) ;
98+ let re = match Regex :: new ( & format ! ( "^{}$" , regex_pattern) ) {
99+ Ok ( r) => r,
100+ Err ( e) => {
101+ eprintln ! ( "invalid pattern: {}" , e) ;
102+ return ;
103+ }
104+ } ;
105+
106+ let mut found_count = 0 ;
107+ let mut matching_paths: HashSet < PathBuf > = HashSet :: new ( ) ;
108+
109+ // Search through all files
110+ for entry in WalkDir :: new ( start_path)
111+ . into_iter ( )
112+ . filter_entry ( |e| {
113+ // Skip common ignore directories to make search faster
114+ if let Some ( name) = e. file_name ( ) . to_str ( ) {
115+ !should_ignore_dir ( name)
116+ } else {
117+ true
118+ }
119+ } )
120+ . filter_map ( |e| e. ok ( ) )
121+ {
122+ if entry. file_type ( ) . is_file ( ) {
123+ if let Some ( filename) = entry. file_name ( ) . to_str ( ) {
124+ if re. is_match ( filename) {
125+ // Add the file path
126+ let file_path = entry. path ( ) . to_path_buf ( ) ;
127+ matching_paths. insert ( file_path. clone ( ) ) ;
128+
129+ // Add all parent directories
130+ let mut current = file_path. parent ( ) ;
131+ while let Some ( parent) = current {
132+ if parent == start_path {
133+ break ;
134+ }
135+ matching_paths. insert ( parent. to_path_buf ( ) ) ;
136+ current = parent. parent ( ) ;
137+ }
138+
139+ found_count += 1 ;
140+ }
141+ }
142+ }
143+ }
144+
145+ if found_count == 0 {
146+ println ! ( "{}" , format!( "no files matching '{}' found" , pattern) . yellow( ) ) ;
147+ return ;
148+ }
149+
150+ println ! ( "{} {}" , format!( "found {} file(s) matching" , found_count) . green( ) , pattern. cyan( ) ) ;
151+ println ! ( ) ;
152+
153+ // Display as tree
154+ display_search_tree ( start_path, & matching_paths, 0 , "" , true ) ;
155+ }
156+
157+ fn display_search_tree (
158+ path : & Path ,
159+ matching_paths : & HashSet < PathBuf > ,
160+ current_depth : usize ,
161+ prefix : & str ,
162+ _is_last : bool ,
163+ ) {
164+ let mut entries: Vec < _ > = match fs:: read_dir ( path) {
165+ Ok ( entries) => entries
166+ . filter_map ( |e| e. ok ( ) )
167+ . filter ( |e| {
168+ let entry_path = e. path ( ) ;
169+ // Only show entries that are in our matching set or are parents of matches
170+ matching_paths. contains ( & entry_path) ||
171+ matching_paths. iter ( ) . any ( |p| p. starts_with ( & entry_path) )
172+ } )
173+ . collect ( ) ,
174+ Err ( _) => return ,
175+ } ;
176+
177+ // Sort: directories first, then alphabetically
178+ entries. sort_by_key ( |e| {
179+ let path = e. path ( ) ;
180+ let is_dir = path. is_dir ( ) ;
181+ let name = e. file_name ( ) . to_string_lossy ( ) . to_lowercase ( ) ;
182+ ( !is_dir, name)
183+ } ) ;
184+
185+ let total = entries. len ( ) ;
186+
187+ for ( idx, entry) in entries. iter ( ) . enumerate ( ) {
188+ let is_last_entry = idx == total - 1 ;
189+ let entry_path = entry. path ( ) ;
190+ let name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
191+ let is_dir = entry_path. is_dir ( ) ;
192+
193+ let connector = if is_last_entry { "└── " } else { "├── " } ;
194+
195+ if is_dir {
196+ let dir_name = format ! ( "{}/" , name) . blue ( ) . bold ( ) ;
197+ println ! ( "{}{}{}" , prefix, connector, dir_name) ;
198+
199+ let new_prefix = if is_last_entry {
200+ format ! ( "{} " , prefix)
201+ } else {
202+ format ! ( "{}│ " , prefix)
203+ } ;
204+ display_search_tree ( & entry_path, matching_paths, current_depth + 1 , & new_prefix, is_last_entry) ;
205+ } else {
206+ // This is a matching file
207+ let file_name = if is_executable ( & entry_path) {
208+ name. green ( ) . bold ( )
209+ } else {
210+ name. cyan ( ) . bold ( )
211+ } ;
212+
213+ if let Ok ( metadata) = fs:: metadata ( & entry_path) {
214+ let size_str = format ! ( " ({})" , format_size( metadata. len( ) ) ) . bright_black ( ) ;
215+ println ! ( "{}{}{}{}" , prefix, connector, file_name, size_str) ;
216+ } else {
217+ println ! ( "{}{}{}" , prefix, connector, file_name) ;
218+ }
219+ }
220+ }
221+ }
222+
95223#[ derive( Parser , Debug ) ]
96224#[ command( name = "struct" ) ]
97225#[ command( about = "A smarter tree command with intelligent defaults" , long_about = None ) ]
@@ -140,6 +268,14 @@ enum Commands {
140268 List ,
141269 /// Clear all custom ignore patterns
142270 Clear ,
271+ /// Search for files matching a pattern
272+ Search {
273+ /// Pattern to search for (e.g., "*.env", "config", "test*")
274+ pattern : String ,
275+ /// Starting directory
276+ #[ arg( default_value = "." ) ]
277+ path : PathBuf ,
278+ } ,
143279}
144280
145281struct StructConfig {
@@ -172,6 +308,10 @@ fn main() {
172308 clear_config_patterns ( ) ;
173309 return ;
174310 }
311+ Commands :: Search { pattern, path } => {
312+ search_files ( & pattern, & path) ;
313+ return ;
314+ }
175315 }
176316 }
177317
0 commit comments