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",
"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",
]

View File

@ -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"

View File

@ -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

View File

@ -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<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 {
search_paths: Vec<PathBuf>,
skip_dirs: Vec<String>,
// Running list of files in memory
files: RefCell<Vec<FileEntry>>,
files: Arc<Mutex<Vec<FileEntry>>>,
}
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<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 {
@ -106,32 +180,21 @@ fn skip_dir(entry: &DirEntry, dirs: &Vec<String>) -> 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,7 +226,9 @@ impl LauncherPlugin for FileSearchPlugin {
}
let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new();
let files = self.files.borrow();
// 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();
@ -172,6 +237,7 @@ impl LauncherPlugin for FileSearchPlugin {
}
}
}
}
entries
}