From 47dd0c752a83737ca83ea477088de61cf400a860 Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Thu, 4 Sep 2025 18:00:05 -0400 Subject: [PATCH] File search on the way --- Cargo.lock | 98 +++++++++++++++++++++++++++ Cargo.toml | 2 + src/lib.rs | 1 + src/main.rs | 7 +- src/plugins/drun.rs | 4 ++ src/plugins/file_search.rs | 131 ++++++++++++++++++++++++++++++++++--- src/ui/mod.rs | 9 ++- 7 files changed, 242 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35df3f8..a3fe152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,27 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -583,6 +604,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -658,6 +689,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "pango" version = "0.21.1" @@ -727,6 +764,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "relm4" version = "0.10.0" @@ -787,6 +835,15 @@ dependencies = [ "semver", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -879,6 +936,26 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.47.1" @@ -990,6 +1067,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1057,6 +1144,7 @@ dependencies = [ name = "waycast" version = "0.1.0" dependencies = [ + "directories", "gio", "glib", "gtk4", @@ -1064,6 +1152,16 @@ dependencies = [ "relm4", "relm4-components", "tracker", + "walkdir", +] + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 57f4fa4..ccc6fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +directories = "6.0.0" gio = "0.21.1" glib = "0.21.1" gtk = { version = "0.10.0", package = "gtk4" } @@ -11,3 +12,4 @@ gtk4-layer-shell = "0.6.1" relm4 = "0.10.0" relm4-components = "0.10.0" tracker = "0.2.2" +walkdir = "2.5.0" diff --git a/src/lib.rs b/src/lib.rs index 73bec28..1c44be6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub trait LauncherListItem { } pub trait LauncherPlugin { + fn init(&self); fn name(&self) -> String; fn priority(&self) -> i32; fn description(&self) -> Option; diff --git a/src/main.rs b/src/main.rs index e51c886..8ab88e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ use gtk::Application; use gtk::prelude::*; +use std::env; use waycast::plugins; use waycast::ui::WaycastLauncher; +// TODO: Add an init() function to the launcher plugin spec +// that will get called when loaded. That way plugins like +// file search can index the file system on init instead +// of on the fly fn main() { let app = Application::builder() .application_id("dev.thegrind.waycast") @@ -11,7 +16,7 @@ fn main() { app.connect_activate(|app| { let launcher = WaycastLauncher::new() .add_plugin(plugins::drun::DrunPlugin {}) - .add_plugin(plugins::file_search::FileSearchPlugin {}) + .add_plugin(plugins::file_search::FileSearchPlugin::new()) .initialize(app); launcher.borrow().show(); diff --git a/src/plugins/drun.rs b/src/plugins/drun.rs index 31fe328..7d5a220 100644 --- a/src/plugins/drun.rs +++ b/src/plugins/drun.rs @@ -85,6 +85,10 @@ pub fn get_desktop_entries() -> Vec { pub struct DrunPlugin {} impl LauncherPlugin for DrunPlugin { + fn init(&self) { + // TODO: Load apps into memory + // TODO: Find and cache Icons + } fn name(&self) -> String { return String::from("drun"); } diff --git a/src/plugins/file_search.rs b/src/plugins/file_search.rs index 1780bfc..fd36cba 100644 --- a/src/plugins/file_search.rs +++ b/src/plugins/file_search.rs @@ -1,28 +1,128 @@ +use directories::UserDirs; +use gio::prelude::FileExt; +use glib::object::Cast; +use std::path::PathBuf; +use std::{cell::RefCell, env}; +use walkdir::{DirEntry, WalkDir}; + use crate::{LaunchError, LauncherListItem, LauncherPlugin}; +#[derive(Clone)] struct FileEntry { - title: String, + path: PathBuf, } -impl LauncherListItem for ExampleEntry { +impl FileEntry { + fn from(entry: DirEntry) -> Self { + return FileEntry { + path: entry.into_path(), + }; + } +} + +impl LauncherListItem for FileEntry { fn title(&self) -> String { - return self.title.to_string(); + return String::from(self.path.file_name().unwrap().to_string_lossy()); } fn description(&self) -> Option { return None; } fn execute(&self) -> Result<(), LaunchError> { - println!("Sample item clicked: {}", self.title); - Ok(()) + let file_uri = gio::File::for_path(&self.path); + match gio::AppInfo::launch_default_for_uri( + file_uri.uri().as_str(), + None::<&gio::AppLaunchContext>, + ) { + Err(_) => Err(LaunchError::CouldNotLaunch( + "Error opening file".to_string(), + )), + Ok(()) => Ok(()), + } } + fn icon(&self) -> String { - return String::from("vscode"); + let (content_type, _) = gio::content_type_guess(Some(&self.path), None); + + let icon = gio::content_type_get_icon(&content_type); + + if let Some(themed_icon) = icon.downcast_ref::() { + if let Some(icon_name) = themed_icon.names().first() { + return icon_name.to_string(); + } + } + + String::from("text-x-generic") } } -pub struct FileSearchPlugin {} +pub struct FileSearchPlugin { + search_paths: Vec, + skip_dirs: Vec, + // Running list of files in memory + files: RefCell>, +} + +impl FileSearchPlugin { + pub fn new() -> Self { + return FileSearchPlugin { + search_paths: Vec::new(), + skip_dirs: vec![ + String::from("vendor"), + String::from("node_modules"), + String::from("cache"), + String::from("zig-cache"), + ], + files: RefCell::new(Vec::new()), + }; + } +} + +fn skip_hidden(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with(".")) + .unwrap_or(false) +} + +fn skip_dir(entry: &DirEntry, dirs: &Vec) -> bool { + entry + .file_name() + .to_str() + .map(|n| dirs.contains(&String::from(n))) + .unwrap_or(false) +} + 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(), + ]; + + 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, + } + } + } + } fn name(&self) -> String { return String::from("File search"); } @@ -48,6 +148,21 @@ impl LauncherPlugin for FileSearchPlugin { } fn filter(&self, query: &str) -> Vec> { - self.default_list() + if query.is_empty() { + return self.default_list(); + } + + 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())); + } + } + } + + entries } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a8e5777..0868a07 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ use gtk4_layer_shell as layerShell; use layerShell::LayerShell; use std::cell::RefCell; use std::collections::HashMap; +use std::ops::Deref; use std::path::PathBuf; use std::rc::Rc; mod launcher_builder; @@ -155,6 +156,7 @@ impl WaycastLauncher { plugins_by_prefix, })); + model.borrow().init_plugins(); // Populate the list model.borrow_mut().populate_list(); @@ -165,7 +167,6 @@ impl WaycastLauncher { let model_clone = model.clone(); search_input.connect_changed(move |entry| { let query = entry.text().to_string(); - println!("query: {query}"); model_clone.borrow_mut().filter_list(&query); }); @@ -263,6 +264,12 @@ impl WaycastLauncher { model } + fn init_plugins(&self) { + for plugin in &self.plugins { + plugin.init(); + } + } + pub fn clear_list_ui(&self) { while let Some(child) = self.list_box.first_child() { self.list_box.remove(&child);