@@ -6,6 +6,51 @@ use std::fs;
66use std:: io:: Write ;
77use std:: path:: { Path , PathBuf } ;
88use bitnet_tools:: combine:: { is_combined_file, file_matches_filter, combine_files_to_path} ;
9+ use ignore:: WalkBuilder ;
10+
11+ // Common binary and non-text file extensions to ignore
12+ const IGNORED_EXTENSIONS : & [ & str ] = & [
13+ // Binary formats
14+ ".bin" , ".exe" , ".dll" , ".so" , ".dylib" , ".pdb" ,
15+ ".safetensors" , ".onnx" , ".pt" , ".pth" , ".h5" , ".ckpt" ,
16+ // Image formats
17+ ".jpg" , ".jpeg" , ".png" , ".gif" , ".bmp" , ".tiff" , ".webp" ,
18+ ".ico" , ".svg" ,
19+ // Audio/Video
20+ ".mp3" , ".wav" , ".mp4" , ".avi" , ".mov" ,
21+ // Archives
22+ ".zip" , ".tar" , ".gz" , ".7z" , ".rar" ,
23+ // Other binary formats
24+ ".pdf" , ".doc" , ".docx" , ".xls" , ".xlsx" ,
25+ // Cache and build artifacts
26+ ".pyc" , ".pyo" , ".rlib" , ".rmeta" , ".d" ,
27+ // Database files
28+ ".db" , ".sqlite" , ".sqlite3" ,
29+ // Rust specific
30+ ".rs.bk" , ".lock" ,
31+ ] ;
32+
33+ // Common directories to ignore (in addition to .gitignore patterns)
34+ const IGNORED_DIRECTORIES : & [ & str ] = & [
35+ // Version control
36+ ".git" , ".github" , ".gitignore" , ".gitattributes" , ".gitmodules" ,
37+ // IDE and editor
38+ ".vscode" , ".idea" , ".vs" , ".settings" ,
39+ // Build and cache
40+ "target" , "node_modules" , "__pycache__" , ".cache" ,
41+ // Environment and config
42+ ".env" , ".config" , ".local" ,
43+ // Logs and temporary files
44+ "logs" , "temp" , "tmp" ,
45+ // Dependencies
46+ "vendor" , "packages" , "deps" ,
47+ // Documentation build
48+ "docs/_build" , "site" , "public" ,
49+ // Test coverage
50+ "coverage" , ".coverage" , "htmlcov" ,
51+ // Project specific (from your .gitignore)
52+ "References" , "models" , "Original" , "Converted" ,
53+ ] ;
954
1055#[ derive( PartialEq ) ]
1156enum Tab {
@@ -509,39 +554,143 @@ fn checkbox_tristate(ui: &mut egui::Ui, state: &mut CheckState) -> bool {
509554 }
510555}
511556
557+ /// Returns true if the directory should be ignored
558+ fn should_ignore_directory ( path : & Path ) -> bool {
559+ if let Some ( dir_name) = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) {
560+ IGNORED_DIRECTORIES . iter ( ) . any ( |ignored| {
561+ dir_name. eq_ignore_ascii_case ( ignored) ||
562+ // Handle nested cases like "docs/_build"
563+ ignored. split ( '/' ) . all ( |part| dir_name. eq_ignore_ascii_case ( part) )
564+ } )
565+ } else {
566+ false
567+ }
568+ }
569+
570+ /// Returns true if the file should be ignored based on extension
571+ fn should_ignore_file ( path : & Path ) -> bool {
572+ // Check file extensions
573+ if let Some ( ext) = path. extension ( ) . and_then ( |e| e. to_str ( ) ) {
574+ let ext = format ! ( ".{}" , ext. to_lowercase( ) ) ;
575+ if IGNORED_EXTENSIONS . contains ( & ext. as_str ( ) ) {
576+ return true ;
577+ }
578+ }
579+
580+ // Check if the file itself is in the ignored list (like .gitignore)
581+ if let Some ( file_name) = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) {
582+ if IGNORED_DIRECTORIES . contains ( & file_name) {
583+ return true ;
584+ }
585+ }
586+
587+ is_combined_file ( path) // Also ignore *_combined.txt files
588+ }
589+
512590fn build_tree_with_filter ( path : & Path , filter_exts : & [ String ] ) -> std:: io:: Result < DirEntryNode > {
513- let is_dir = fs:: metadata ( path) ?. is_dir ( ) ;
514- let mut node = DirEntryNode :: new ( path. to_path_buf ( ) , is_dir) ;
515-
516- if is_dir {
517- for entry in fs:: read_dir ( path) ? {
518- let entry = entry?;
519- let child_path = entry. path ( ) ;
520- if entry. file_type ( ) ?. is_dir ( ) {
521- if let Ok ( child_node) = build_tree_with_filter ( & child_path, filter_exts) {
522- if !child_node. children . is_empty ( ) {
523- node. children . push ( child_node) ;
591+ let mut root = DirEntryNode :: new ( path. to_path_buf ( ) , true ) ;
592+
593+ // Use WalkBuilder to respect .gitignore
594+ let walker = WalkBuilder :: new ( path)
595+ . hidden ( true ) // Skip hidden files
596+ . git_ignore ( true ) // Respect .gitignore
597+ . build ( ) ;
598+
599+ let mut dirs: std:: collections:: HashMap < PathBuf , Vec < DirEntryNode > > = std:: collections:: HashMap :: new ( ) ;
600+
601+ for entry in walker. filter_map ( Result :: ok) {
602+ let path = entry. path ( ) . to_owned ( ) ;
603+ if path == root. path {
604+ continue ;
605+ }
606+
607+ // Skip ignored directories
608+ if entry. file_type ( ) . map ( |ft| ft. is_dir ( ) ) . unwrap_or ( false ) {
609+ if should_ignore_directory ( & path) {
610+ continue ;
611+ }
612+ let node = DirEntryNode :: new ( path. clone ( ) , true ) ;
613+ if let Some ( parent) = path. parent ( ) {
614+ dirs. entry ( parent. to_owned ( ) )
615+ . or_default ( )
616+ . push ( node) ;
617+ }
618+ } else if entry. file_type ( ) . map ( |ft| ft. is_file ( ) ) . unwrap_or ( false ) {
619+ // Skip files we want to ignore
620+ if should_ignore_file ( & path) {
621+ continue ;
622+ }
623+
624+ // Apply user's extension filter
625+ if !filter_exts. is_empty ( ) && !file_matches_filter ( & path, filter_exts) {
626+ continue ;
627+ }
628+
629+ let node = DirEntryNode :: new ( path. clone ( ) , false ) ;
630+ if let Some ( parent) = path. parent ( ) {
631+ dirs. entry ( parent. to_owned ( ) )
632+ . or_default ( )
633+ . push ( node) ;
634+ }
635+ }
636+ }
637+
638+ // Build the tree from bottom up
639+ fn build_tree_recursive (
640+ path : & Path ,
641+ dirs : & mut std:: collections:: HashMap < PathBuf , Vec < DirEntryNode > > ,
642+ ) -> Vec < DirEntryNode > {
643+ if let Some ( children) = dirs. remove ( path) {
644+ let mut result = Vec :: new ( ) ;
645+ for mut child in children {
646+ if child. is_dir {
647+ child. children = build_tree_recursive ( & child. path , dirs) ;
648+ if !child. children . is_empty ( ) {
649+ result. push ( child) ;
524650 }
651+ } else {
652+ result. push ( child) ;
525653 }
526- } else if ( filter_exts. is_empty ( ) || file_matches_filter ( & child_path, filter_exts) )
527- && !is_combined_file ( & child_path) // Exclude *_combined.txt
528- {
529- node. children . push ( DirEntryNode :: new ( child_path, false ) ) ;
530654 }
655+ result. sort_by ( |a, b| {
656+ // Directories first, then files
657+ if a. is_dir == b. is_dir {
658+ a. path . file_name ( ) . cmp ( & b. path . file_name ( ) )
659+ } else {
660+ b. is_dir . cmp ( & a. is_dir )
661+ }
662+ } ) ;
663+ result
664+ } else {
665+ Vec :: new ( )
531666 }
532667 }
533- Ok ( node)
668+
669+ root. children = build_tree_recursive ( path, & mut dirs) ;
670+ Ok ( root)
534671}
535672
536- fn main ( ) -> eframe:: Result < ( ) > {
673+ fn main ( ) {
674+ eprintln ! ( "Starting Universal File Combiner..." ) ;
675+
537676 let options = eframe:: NativeOptions {
538- viewport : egui:: ViewportBuilder :: default ( ) . with_inner_size ( [ 900.0 , 700.0 ] ) ,
677+ viewport : egui:: ViewportBuilder :: default ( )
678+ . with_inner_size ( [ 900.0 , 700.0 ] )
679+ . with_title ( "Universal File Combiner" ) ,
539680 ..Default :: default ( )
540681 } ;
541682
542- eframe:: run_native (
683+ eprintln ! ( "Initializing GUI..." ) ;
684+
685+ match eframe:: run_native (
543686 "Universal File Combiner" ,
544687 options,
545- Box :: new ( |_cc| Box :: new ( FileCombinerApp :: default ( ) ) ) ,
546- )
688+ Box :: new ( |_cc| {
689+ eprintln ! ( "Creating application instance..." ) ;
690+ Box :: new ( FileCombinerApp :: default ( ) )
691+ } ) ,
692+ ) {
693+ Ok ( _) => eprintln ! ( "GUI closed successfully" ) ,
694+ Err ( e) => eprintln ! ( "Error running GUI: {}" , e) ,
695+ }
547696}
0 commit comments