This commit is contained in:
Javier Feliz 2025-09-05 21:15:35 -04:00
parent f81acdc962
commit b5de72e9bc
4 changed files with 96 additions and 141 deletions

View File

@ -13,9 +13,9 @@ fn main() {
.build(); .build();
app.connect_activate(|app| { app.connect_activate(|app| {
let file_search_plugin = waycast_plugins::file_search::new(); let mut file_search_plugin = waycast_plugins::file_search::new();
match waycast_plugins::file_search::add_search_path("/home/javi/working-files/DJ Music/") { match file_search_plugin.add_search_path("/home/javi/working-files/DJ Music/") {
Err(e) => eprintln!("{}", e), Err(e) => eprintln!("{}", e),
_ => (), _ => (),
} }

View File

@ -9,7 +9,6 @@ use syn::{
/// Plugin configuration parsed from the macro input /// Plugin configuration parsed from the macro input
struct PluginConfig { struct PluginConfig {
struct_name: Ident,
name: LitStr, name: LitStr,
priority: Option<LitInt>, priority: Option<LitInt>,
description: Option<LitStr>, description: Option<LitStr>,
@ -22,10 +21,6 @@ struct PluginConfig {
impl Parse for PluginConfig { impl Parse for PluginConfig {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
// Parse "struct StructName;"
input.parse::<Token![struct]>()?;
let struct_name = input.parse::<Ident>()?;
input.parse::<Token![;]>()?;
let mut name = None; let mut name = None;
let mut priority = None; let mut priority = None;
@ -98,11 +93,10 @@ impl Parse for PluginConfig {
// Validate required fields // Validate required fields
let name = name.ok_or_else(|| { let name = name.ok_or_else(|| {
syn::Error::new_spanned(&struct_name, "Plugin must have a 'name' field") syn::Error::new(input.span(), "Plugin must have a 'name' field")
})?; })?;
Ok(PluginConfig { Ok(PluginConfig {
struct_name,
name, name,
priority, priority,
description, description,
@ -116,15 +110,8 @@ impl Parse for PluginConfig {
} }
impl PluginConfig { impl PluginConfig {
/// Generate the full plugin struct name with "Plugin" suffix /// Generate the implementation of LauncherPlugin trait methods
fn plugin_struct_name(&self) -> Ident { fn generate(&self) -> proc_macro2::TokenStream {
let name_str = format!("{}Plugin", self.struct_name);
Ident::new(&name_str, self.struct_name.span())
}
/// Generate the implementation of LauncherPlugin trait
fn generate_plugin_impl(&self) -> proc_macro2::TokenStream {
let plugin_struct_name = self.plugin_struct_name();
let name_str = &self.name; let name_str = &self.name;
// Generate priority method // Generate priority method
@ -201,7 +188,6 @@ impl PluginConfig {
}; };
quote! { quote! {
impl waycast_core::LauncherPlugin for #plugin_struct_name {
#init_method #init_method
fn name(&self) -> String { fn name(&self) -> String {
@ -231,36 +217,18 @@ impl PluginConfig {
} }
} }
/// Generate the complete plugin code
fn generate(&self) -> proc_macro2::TokenStream {
let plugin_struct_name = self.plugin_struct_name();
let plugin_impl = self.generate_plugin_impl();
quote! {
pub struct #plugin_struct_name {}
impl #plugin_struct_name {
pub fn new() -> Self {
#plugin_struct_name {}
}
}
#plugin_impl
}
}
}
/// The main plugin! proc macro /// The main plugin! proc macro
/// ///
/// Usage: /// Usage inside impl LauncherPlugin block:
/// ```rust /// ```rust
/// impl LauncherPlugin for MyPlugin {
/// plugin! { /// plugin! {
/// struct Calculator;
/// name: "calculator", /// name: "calculator",
/// priority: 500, /// priority: 500,
/// prefix: "calc", /// prefix: "calc",
/// filter: calc_filter, /// filter: calc_filter,
/// } /// }
/// }
/// ``` /// ```
#[proc_macro] #[proc_macro]
pub fn plugin(input: TokenStream) -> TokenStream { pub fn plugin(input: TokenStream) -> TokenStream {

View File

@ -1,6 +1,6 @@
use gio::{prelude::*, AppInfo, DesktopAppInfo, Icon}; use gio::{prelude::*, AppInfo, DesktopAppInfo, Icon};
use waycast_core::{LaunchError, LauncherListItem}; use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
use crate::plugin; use waycast_macros::plugin;
#[derive(Debug)] #[derive(Debug)]
pub struct DesktopEntry { pub struct DesktopEntry {
@ -113,14 +113,23 @@ fn drun_filter(plugin: &DrunPlugin, query: &str) -> Vec<Box<dyn LauncherListItem
entries entries
} }
pub struct DrunPlugin;
impl DrunPlugin {
pub fn new() -> Self {
DrunPlugin
}
}
impl LauncherPlugin for DrunPlugin {
plugin! { plugin! {
struct Drun;
name: "drun", name: "drun",
priority: 1000, priority: 1000,
description: "List and launch an installed application", description: "List and launch an installed application",
prefix: "app", prefix: "app",
default_list: drun_default_list, default_list: drun_default_list,
filter: drun_filter, filter: drun_filter
}
} }
pub fn new() -> DrunPlugin { pub fn new() -> DrunPlugin {

View File

@ -9,7 +9,7 @@ use tokio::sync::Mutex;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use waycast_macros::plugin; use waycast_macros::plugin;
use waycast_core::{LaunchError, LauncherListItem}; use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
#[derive(Clone)] #[derive(Clone)]
struct FileEntry { struct FileEntry {
@ -104,16 +104,27 @@ pub fn default_search_list() -> Vec<PathBuf> {
Vec::new() Vec::new()
} }
// Global state for file search pub struct FileSearchPlugin {
static mut FILE_SEARCH_DATA: Option<FileSearchData> = None;
struct FileSearchData {
search_paths: Vec<PathBuf>, search_paths: Vec<PathBuf>,
skip_dirs: Vec<String>, skip_dirs: Vec<String>,
// Running list of files in memory
files: Arc<Mutex<Vec<FileEntry>>>, files: Arc<Mutex<Vec<FileEntry>>>,
} }
impl FileSearchData { impl FileSearchPlugin {
pub fn new() -> Self {
FileSearchPlugin {
search_paths: default_search_list(),
skip_dirs: vec![
String::from("vendor"),
String::from("node_modules"),
String::from("cache"),
String::from("zig-cache"),
],
files: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> { pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
let p = path.as_ref(); let p = path.as_ref();
@ -170,43 +181,6 @@ impl FileSearchData {
} }
} }
fn get_file_search_data() -> &'static FileSearchData {
unsafe {
FILE_SEARCH_DATA.as_ref().unwrap()
}
}
fn get_file_search_data_mut() -> &'static mut FileSearchData {
unsafe {
FILE_SEARCH_DATA.as_mut().unwrap()
}
}
pub fn new() -> FileSearchPlugin {
unsafe {
FILE_SEARCH_DATA = Some(FileSearchData {
search_paths: default_search_list(),
skip_dirs: vec![
String::from("vendor"),
String::from("node_modules"),
String::from("cache"),
String::from("zig-cache"),
],
files: Arc::new(Mutex::new(Vec::new())),
});
}
FileSearchPlugin::new()
}
pub fn add_search_path<P: AsRef<Path>>(path: P) -> Result<(), String> {
get_file_search_data_mut().add_search_path(path)
}
pub fn add_skip_dir(directory_name: String) -> Result<(), String> {
get_file_search_data_mut().add_skip_dir(directory_name)
}
fn skip_hidden(entry: &DirEntry) -> bool { fn skip_hidden(entry: &DirEntry) -> bool {
entry entry
.file_name() .file_name()
@ -223,20 +197,31 @@ fn skip_dir(entry: &DirEntry, dirs: &Vec<String>) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
impl LauncherPlugin for FileSearchPlugin {
plugin! {
name: "Files",
priority: 500,
description: "Search and open files",
prefix: "f",
init: file_search_init,
default_list: file_search_default_list,
filter: file_search_filter
}
}
fn file_search_default_list(_plugin: &FileSearchPlugin) -> Vec<Box<dyn LauncherListItem>> { fn file_search_default_list(_plugin: &FileSearchPlugin) -> Vec<Box<dyn LauncherListItem>> {
Vec::new() Vec::new()
} }
fn file_search_filter(_plugin: &FileSearchPlugin, query: &str) -> Vec<Box<dyn LauncherListItem>> { fn file_search_filter(plugin: &FileSearchPlugin, query: &str) -> Vec<Box<dyn LauncherListItem>> {
if query.is_empty() { if query.is_empty() {
return file_search_default_list(_plugin); return file_search_default_list(plugin);
} }
let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new(); let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new();
let data = get_file_search_data();
// Try to get files without blocking - if indexing is still in progress, return empty // Try to get files without blocking - if indexing is still in progress, return empty
if let Ok(files) = data.files.try_lock() { if let Ok(files) = plugin.files.try_lock() {
for f in files.iter() { for f in files.iter() {
if let Some(file_name) = f.path.file_name() { if let Some(file_name) = f.path.file_name() {
let cmp = file_name.to_string_lossy().to_lowercase(); let cmp = file_name.to_string_lossy().to_lowercase();
@ -250,31 +235,24 @@ fn file_search_filter(_plugin: &FileSearchPlugin, query: &str) -> Vec<Box<dyn La
entries entries
} }
fn file_search_init(_plugin: &FileSearchPlugin) { fn file_search_init(plugin: &FileSearchPlugin) {
let data = get_file_search_data(); // Start async file scanning with 500ms timeout
let data_clone = FileSearchData { let self_clone = FileSearchPlugin {
search_paths: data.search_paths.clone(), search_paths: plugin.search_paths.clone(),
skip_dirs: data.skip_dirs.clone(), skip_dirs: plugin.skip_dirs.clone(),
files: Arc::clone(&data.files), files: Arc::clone(&plugin.files),
}; };
std::thread::spawn(move || { std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
data_clone self_clone
.init_with_timeout(Duration::from_millis(2000)) .init_with_timeout(Duration::from_millis(2000))
.await; .await;
}); });
}); });
} }
plugin! { pub fn new() -> FileSearchPlugin {
struct FileSearch; FileSearchPlugin::new()
name: "Files",
priority: 500,
description: "Search and open files",
prefix: "f",
init: file_search_init,
default_list: file_search_default_list,
filter: file_search_filter
} }