Async file scan and adding additional paths

This commit is contained in:
Javier Feliz 2025-09-05 19:20:46 -04:00
parent d8aa3cf267
commit e5efca4689
4 changed files with 124 additions and 37 deletions

13
Cargo.lock generated
View File

@ -968,6 +968,18 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"slab", "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]] [[package]]
@ -1151,6 +1163,7 @@ dependencies = [
"gtk4-layer-shell", "gtk4-layer-shell",
"relm4", "relm4",
"relm4-components", "relm4-components",
"tokio",
"tracker", "tracker",
"walkdir", "walkdir",
] ]

View File

@ -11,5 +11,6 @@ gtk = { version = "0.10.0", package = "gtk4" }
gtk4-layer-shell = "0.6.1" gtk4-layer-shell = "0.6.1"
relm4 = "0.10.0" relm4 = "0.10.0"
relm4-components = "0.10.0" relm4-components = "0.10.0"
tokio = { version = "1.0", features = ["rt", "time", "macros"] }
tracker = "0.2.2" tracker = "0.2.2"
walkdir = "2.5.0" walkdir = "2.5.0"

View File

@ -10,10 +10,17 @@ fn main() {
.build(); .build();
app.connect_activate(|app| { 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 // Create the core launcher
let launcher = WaycastLauncher::new() let launcher = WaycastLauncher::new()
.add_plugin(Box::new(plugins::drun::DrunPlugin {})) .add_plugin(Box::new(plugins::drun::DrunPlugin {}))
.add_plugin(Box::new(plugins::file_search::FileSearchPlugin::new())) .add_plugin(Box::new(file_search_plugin))
.init(); .init();
// Create and show the GTK UI // Create and show the GTK UI

View File

@ -1,8 +1,10 @@
use directories::UserDirs; use directories::UserDirs;
use gio::prelude::FileExt; use gio::prelude::FileExt;
use glib::object::Cast; use glib::object::Cast;
use std::cell::RefCell; use std::path::{Path, PathBuf};
use std::path::PathBuf; use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use crate::{LaunchError, LauncherListItem, LauncherPlugin}; use crate::{LaunchError, LauncherListItem, LauncherPlugin};
@ -66,26 +68,98 @@ impl LauncherListItem for FileEntry {
// $HOME/Documents/wallpapers // $HOME/Documents/wallpapers
// Then we should just keep $HOME/Documents since wallpapers // Then we should just keep $HOME/Documents since wallpapers
// will be included in it anyways // will be included in it anyways
pub fn default_search_list() -> Vec<PathBuf> {
if let Some(ud) = UserDirs::new() {
let mut paths: Vec<PathBuf> = 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 { pub struct FileSearchPlugin {
search_paths: Vec<PathBuf>, search_paths: Vec<PathBuf>,
skip_dirs: Vec<String>, skip_dirs: Vec<String>,
// Running list of files in memory // Running list of files in memory
files: RefCell<Vec<FileEntry>>, files: Arc<Mutex<Vec<FileEntry>>>,
} }
impl FileSearchPlugin { impl FileSearchPlugin {
pub fn new() -> Self { pub fn new() -> Self {
return FileSearchPlugin { return FileSearchPlugin {
search_paths: Vec::new(), search_paths: default_search_list(),
skip_dirs: vec![ skip_dirs: vec![
String::from("vendor"), String::from("vendor"),
String::from("node_modules"), String::from("node_modules"),
String::from("cache"), String::from("cache"),
String::from("zig-cache"), String::from("zig-cache"),
], ],
files: RefCell::new(Vec::new()), files: Arc::new(Mutex::new(Vec::new())),
}; };
} }
pub fn add_search_path<P: AsRef<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 { fn skip_hidden(entry: &DirEntry) -> bool {
@ -106,32 +180,21 @@ fn skip_dir(entry: &DirEntry, dirs: &Vec<String>) -> bool {
impl LauncherPlugin for FileSearchPlugin { impl LauncherPlugin for FileSearchPlugin {
fn init(&self) { fn init(&self) {
// let home = env::home_dir().unwrap(); // Start async file scanning with 500ms timeout
if let Some(ud) = UserDirs::new() { let self_clone = FileSearchPlugin {
let scan = [ search_paths: self.search_paths.clone(),
ud.document_dir(), skip_dirs: self.skip_dirs.clone(),
ud.picture_dir(), files: Arc::clone(&self.files),
ud.audio_dir(), };
ud.video_dir(),
];
for p in scan { std::thread::spawn(move || {
match p { let rt = tokio::runtime::Runtime::new().unwrap();
Some(path) => { rt.block_on(async {
let walker = WalkDir::new(path).into_iter(); self_clone
for entry in walker .init_with_timeout(Duration::from_millis(2000))
.filter_entry(|e| !skip_hidden(e) && !skip_dir(e, &self.skip_dirs)) .await;
.filter_map(|e| e.ok()) });
{ });
if entry.path().is_file() {
self.files.borrow_mut().push(FileEntry::from(entry));
}
}
}
None => continue,
}
}
}
} }
fn name(&self) -> String { fn name(&self) -> String {
return String::from("File search"); return String::from("File search");
@ -163,12 +226,15 @@ impl LauncherPlugin for FileSearchPlugin {
} }
let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new(); let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new();
let files = self.files.borrow();
for f in files.iter() { // Try to get files without blocking - if indexing is still in progress, return empty
if let Some(file_name) = f.path.file_name() { if let Ok(files) = self.files.try_lock() {
let cmp = file_name.to_string_lossy().to_lowercase(); for f in files.iter() {
if cmp.contains(&query.to_lowercase()) { if let Some(file_name) = f.path.file_name() {
entries.push(Box::new(f.clone())); let cmp = file_name.to_string_lossy().to_lowercase();
if cmp.contains(&query.to_lowercase()) {
entries.push(Box::new(f.clone()));
}
} }
} }
} }