@@ -45,6 +45,9 @@ use serde::{Deserialize, Serialize};
4545/// - On failure: Contains an error string describing why parsing failed
4646type RestoreMsg = Result < ( FolderTreeNode , PathBuf ) , String > ; // Result type for restore operations
4747
48+ /// Result of a background file dialog.
49+ type FileDialogMsg = Vec < PathBuf > ;
50+
4851/// A template representing a reusable set of file and folder paths.
4952///
5053/// Templates are serialized as JSON and can be saved/loaded by the user
@@ -118,14 +121,14 @@ fn build_tree_from_paths(paths: &[String]) -> FolderTreeNode {
118121
119122/// Entry point
120123///
121- /// Initializes enviroment variables, loads the application icon,
124+ /// Initializes environment variables, loads the application icon,
122125/// configures [`eframe::NativeOptions`], and launches the GUI.
123126///
124127/// Returns an [`eframe::Error`] if the GUI fails to start.
125128fn main ( ) -> Result < ( ) , eframe:: Error > {
126129 println ! ( "[DEBUG] main: Starting application" ) ;
127130
128- dotenv:: dotenv ( ) . ok ( ) ; // Load enviroment variables from .env if available
131+ dotenv:: dotenv ( ) . ok ( ) ; // Load environment variables from .env if available
129132 println ! ( "[DEBUG] .env loaded (if present)" ) ;
130133
131134 let icon = load_icon_image ( ) ; // Load application image
@@ -180,6 +183,9 @@ struct GUIApp {
180183 restore_progress : Option < Progress > ,
181184 restore_opening : bool ,
182185 restore_rx : Option < mpsc:: Receiver < RestoreMsg > > ,
186+ // async file dialog handling for linux being fuck and freezing.
187+ file_dialog_rx : Option < mpsc:: Receiver < FileDialogMsg > > ,
188+ file_dialog_opening : bool ,
183189 tab : MainTab ,
184190 compression_enabled : bool ,
185191 default_backup_location : Option < PathBuf > ,
@@ -212,6 +218,8 @@ impl Default for GUIApp {
212218 restore_progress : None ,
213219 restore_opening : false ,
214220 restore_rx : None ,
221+ file_dialog_rx : None ,
222+ file_dialog_opening : false ,
215223 tab : MainTab :: Home ,
216224 compression_enabled : config. compression_enabled ,
217225 default_backup_location : config. default_backup_location . clone ( ) ,
@@ -415,28 +423,97 @@ impl eframe::App for GUIApp {
415423 self . restore_rx = None ;
416424 }
417425
426+ if let Some ( rx) = self . file_dialog_rx . as_ref ( ) {
427+ use std:: sync:: mpsc:: TryRecvError ;
428+
429+ match rx. try_recv ( ) {
430+ Ok ( mut paths) => {
431+ self . selected_folders . append ( & mut paths) ;
432+ self . selected_folders . sort ( ) ;
433+ self . selected_folders . dedup ( ) ;
434+ self . file_dialog_rx = None ;
435+ self . file_dialog_opening = false ;
436+ }
437+ Err ( TryRecvError :: Disconnected ) => {
438+ self . file_dialog_rx = None ;
439+ self . file_dialog_opening = false ;
440+ }
441+ Err ( TryRecvError :: Empty ) => {
442+ // waiting...
443+ }
444+ }
445+ }
446+
418447 ui. heading ( "Konserve" ) ;
419448 ui. separator ( ) ;
420449
421450 // Folder and File Pickers
422451 ui. horizontal ( |ui| {
423452 if ui. button ( "Add Folders" ) . clicked ( ) {
424- if let Some ( folders) = FileDialog :: new ( ) . pick_folders ( ) {
425- self . selected_folders . extend ( folders) ;
426- self . selected_folders . sort ( ) ;
427- self . selected_folders . dedup ( ) ;
453+ #[ cfg( target_os = "macos" ) ]
454+ {
455+ // macOS wants dialogs on the main thread
456+ if let Some ( folders) = FileDialog :: new ( ) . pick_folders ( ) {
457+ self . selected_folders . extend ( folders) ;
458+ self . selected_folders . sort ( ) ;
459+ self . selected_folders . dedup ( ) ;
460+ }
461+ }
462+
463+ #[ cfg( not( target_os = "macos" ) ) ]
464+ {
465+ // Linux / Windows: run dialog in a background thread
466+ if self . file_dialog_rx . is_none ( ) {
467+ self . file_dialog_opening = true ;
468+
469+ let ( tx, rx) = mpsc:: channel :: < FileDialogMsg > ( ) ;
470+ self . file_dialog_rx = Some ( rx) ;
471+
472+ std:: thread:: spawn ( move || {
473+ let folders =
474+ FileDialog :: new ( ) . pick_folders ( ) . unwrap_or_default ( ) ;
475+ let _ = tx. send ( folders) ;
476+ } ) ;
477+ }
428478 }
429479 }
430480
431481 if ui. button ( "Add Files" ) . clicked ( ) {
432- if let Some ( files) = FileDialog :: new ( ) . pick_files ( ) {
433- self . selected_folders . extend ( files) ;
434- self . selected_folders . sort ( ) ;
435- self . selected_folders . dedup ( ) ;
482+ #[ cfg( target_os = "macos" ) ]
483+ {
484+ if let Some ( files) = FileDialog :: new ( ) . pick_files ( ) {
485+ self . selected_folders . extend ( files) ;
486+ self . selected_folders . sort ( ) ;
487+ self . selected_folders . dedup ( ) ;
488+ }
489+ }
490+
491+ #[ cfg( not( target_os = "macos" ) ) ]
492+ {
493+ if self . file_dialog_rx . is_none ( ) {
494+ self . file_dialog_opening = true ;
495+
496+ let ( tx, rx) = mpsc:: channel :: < FileDialogMsg > ( ) ;
497+ self . file_dialog_rx = Some ( rx) ;
498+
499+ std:: thread:: spawn ( move || {
500+ let files =
501+ FileDialog :: new ( ) . pick_files ( ) . unwrap_or_default ( ) ;
502+ let _ = tx. send ( files) ;
503+ } ) ;
504+ }
436505 }
437506 }
438507 } ) ;
439508
509+ if self . file_dialog_opening {
510+ ui. horizontal ( |ui| {
511+ ui. add ( egui:: Spinner :: new ( ) . size ( 12.0 ) ) ;
512+ ui. label ( "Waiting for file dialog…" ) ;
513+ } ) ;
514+ ctx. request_repaint_after ( std:: time:: Duration :: from_millis ( 50 ) ) ;
515+ }
516+
440517 // Show selected paths
441518 if !self . selected_folders . is_empty ( ) {
442519 ui. add_space ( 4.0 ) ;
0 commit comments