From e5efca4689d430c0f36a2533b8fe75aad6bcd525 Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Fri, 5 Sep 2025 19:20:46 -0400 Subject: [PATCH] Async file scan and adding additional paths --- Cargo.lock | 13 ++++ Cargo.toml | 1 + src/main.rs | 9 ++- src/plugins/file_search.rs | 138 +++++++++++++++++++++++++++---------- 4 files changed, 124 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3fe152..3499e6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -968,6 +968,18 @@ dependencies = [ "mio", "pin-project-lite", "slab", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1151,6 +1163,7 @@ dependencies = [ "gtk4-layer-shell", "relm4", "relm4-components", + "tokio", "tracker", "walkdir", ] diff --git a/Cargo.toml b/Cargo.toml index ccc6fd4..80fdb55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,6 @@ gtk = { version = "0.10.0", package = "gtk4" } gtk4-layer-shell = "0.6.1" relm4 = "0.10.0" relm4-components = "0.10.0" +tokio = { version = "1.0", features = ["rt", "time", "macros"] } tracker = "0.2.2" walkdir = "2.5.0" diff --git a/src/main.rs b/src/main.rs index 085ae11..748bb1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,10 +10,17 @@ fn main() { .build(); app.connect_activate(|app| { + let mut file_search_plugin = plugins::file_search::FileSearchPlugin::new(); + + match file_search_plugin.add_search_path("/home/javi/working-files/DJ Music/") { + Err(e) => eprintln!("{}", e), + _ => (), + } + // Create the core launcher let launcher = WaycastLauncher::new() .add_plugin(Box::new(plugins::drun::DrunPlugin {})) - .add_plugin(Box::new(plugins::file_search::FileSearchPlugin::new())) + .add_plugin(Box::new(file_search_plugin)) .init(); // Create and show the GTK UI diff --git a/src/plugins/file_search.rs b/src/plugins/file_search.rs index 4c34954..1abab81 100644 --- a/src/plugins/file_search.rs +++ b/src/plugins/file_search.rs @@ -1,8 +1,10 @@ use directories::UserDirs; use gio::prelude::FileExt; use glib::object::Cast; -use std::cell::RefCell; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; use walkdir::{DirEntry, WalkDir}; use crate::{LaunchError, LauncherListItem, LauncherPlugin}; @@ -66,26 +68,98 @@ impl LauncherListItem for FileEntry { // $HOME/Documents/wallpapers // Then we should just keep $HOME/Documents since wallpapers // will be included in it anyways + +pub fn default_search_list() -> Vec { + if let Some(ud) = UserDirs::new() { + let mut paths: Vec = Vec::new(); + let user_dirs = [ + ud.document_dir(), + ud.picture_dir(), + ud.audio_dir(), + ud.video_dir(), + ]; + + for d in user_dirs { + if let Some(path) = d { + paths.push(path.to_path_buf()); + } + } + + return paths; + } + + Vec::new() +} pub struct FileSearchPlugin { search_paths: Vec, skip_dirs: Vec, // Running list of files in memory - files: RefCell>, + files: Arc>>, } impl FileSearchPlugin { pub fn new() -> Self { return FileSearchPlugin { - search_paths: Vec::new(), + search_paths: default_search_list(), skip_dirs: vec![ String::from("vendor"), String::from("node_modules"), String::from("cache"), String::from("zig-cache"), ], - files: RefCell::new(Vec::new()), + files: Arc::new(Mutex::new(Vec::new())), }; } + + pub fn add_search_path>(&mut self, path: P) -> Result<(), String> { + let p = path.as_ref(); + + if !p.exists() { + return Err(format!("Path does not exist: {}", p.display())); + } + + if !p.is_dir() { + return Err(format!("Path is not a directory: {}", p.display())); + } + + self.search_paths.push(p.to_path_buf()); + Ok(()) + } + + async fn init_with_timeout(&self, timeout: Duration) { + let files_clone = Arc::clone(&self.files); + let skip_dirs_clone = self.skip_dirs.clone(); + + let scan_task = async move { + let mut local_files = Vec::new(); + + for path in &self.search_paths { + let walker = WalkDir::new(path).into_iter(); + for entry in walker + .filter_entry(|e| !skip_hidden(e) && !skip_dir(e, &skip_dirs_clone)) + .filter_map(|e| e.ok()) + { + if entry.path().is_file() { + local_files.push(FileEntry::from(entry)); + } + + // Yield control periodically to check for timeout + if local_files.len() % 1000 == 0 { + tokio::task::yield_now().await; + } + } + } + + // Update the shared files collection + let mut files_guard = files_clone.lock().await; + *files_guard = local_files; + }; + + // Run the scan with a timeout + if let Err(_) = tokio::time::timeout(timeout, scan_task).await { + eprintln!("File indexing timed out after {:?}", timeout); + } + } } fn skip_hidden(entry: &DirEntry) -> bool { @@ -106,32 +180,21 @@ fn skip_dir(entry: &DirEntry, dirs: &Vec) -> bool { impl LauncherPlugin for FileSearchPlugin { fn init(&self) { - // let home = env::home_dir().unwrap(); - if let Some(ud) = UserDirs::new() { - let scan = [ - ud.document_dir(), - ud.picture_dir(), - ud.audio_dir(), - ud.video_dir(), - ]; + // Start async file scanning with 500ms timeout + let self_clone = FileSearchPlugin { + search_paths: self.search_paths.clone(), + skip_dirs: self.skip_dirs.clone(), + files: Arc::clone(&self.files), + }; - for p in scan { - match p { - Some(path) => { - let walker = WalkDir::new(path).into_iter(); - for entry in walker - .filter_entry(|e| !skip_hidden(e) && !skip_dir(e, &self.skip_dirs)) - .filter_map(|e| e.ok()) - { - if entry.path().is_file() { - self.files.borrow_mut().push(FileEntry::from(entry)); - } - } - } - None => continue, - } - } - } + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + self_clone + .init_with_timeout(Duration::from_millis(2000)) + .await; + }); + }); } fn name(&self) -> String { return String::from("File search"); @@ -163,12 +226,15 @@ impl LauncherPlugin for FileSearchPlugin { } let mut entries: Vec> = Vec::new(); - let files = self.files.borrow(); - for f in files.iter() { - if let Some(file_name) = f.path.file_name() { - let cmp = file_name.to_string_lossy().to_lowercase(); - if cmp.contains(&query.to_lowercase()) { - entries.push(Box::new(f.clone())); + + // Try to get files without blocking - if indexing is still in progress, return empty + if let Ok(files) = self.files.try_lock() { + for f in files.iter() { + if let Some(file_name) = f.path.file_name() { + let cmp = file_name.to_string_lossy().to_lowercase(); + if cmp.contains(&query.to_lowercase()) { + entries.push(Box::new(f.clone())); + } } } }