From 919e6a44f6499be362b6431b4dd45116da2d70fb Mon Sep 17 00:00:00 2001 From: Javier Feliz Date: Thu, 11 Sep 2025 19:56:10 -0400 Subject: [PATCH] Progress --- waycast-gtk/src/main.rs | 4 +- waycast-gtk/src/ui/gtk/mod.rs | 95 +++++++++--------- waycast-plugins/src/main.rs | 27 ++++-- .../src/projects/framework_detector.rs | 96 +++++++++++++++++++ .../src/projects/framework_macro.rs | 20 ++-- waycast-plugins/src/projects/mod.rs | 67 +++++++++++-- 6 files changed, 241 insertions(+), 68 deletions(-) diff --git a/waycast-gtk/src/main.rs b/waycast-gtk/src/main.rs index b5fa06b..7853733 100644 --- a/waycast-gtk/src/main.rs +++ b/waycast-gtk/src/main.rs @@ -14,8 +14,8 @@ fn main() { app.connect_activate(|app| { // Create the core launcher let launcher = WaycastLauncher::new() - .add_plugin(Box::new(waycast_plugins::drun::new())) - .add_plugin(Box::new(waycast_plugins::file_search::new())) + // .add_plugin(Box::new(waycast_plugins::drun::new())) + // .add_plugin(Box::new(waycast_plugins::file_search::new())) .add_plugin(Box::new(waycast_plugins::projects::new())) .init(); diff --git a/waycast-gtk/src/ui/gtk/mod.rs b/waycast-gtk/src/ui/gtk/mod.rs index a381f81..0f3c607 100644 --- a/waycast-gtk/src/ui/gtk/mod.rs +++ b/waycast-gtk/src/ui/gtk/mod.rs @@ -223,21 +223,21 @@ impl GtkLauncherUI { let launcher_for_search = launcher.clone(); let list_store_for_search = list_store.clone(); let selection_for_search = selection.clone(); - + // Add debouncing to avoid excessive searches with generation counter let search_generation = Rc::new(RefCell::new(0u64)); - + search_input.connect_changed(move |entry| { let query = entry.text().to_string(); - + // Increment generation to cancel any pending searches *search_generation.borrow_mut() += 1; - + if query.trim().is_empty() { // Handle empty query synchronously for immediate response let mut launcher_ref = launcher_for_search.borrow_mut(); let results = launcher_ref.get_default_results(); - + list_store_for_search.remove_all(); for entry in results.iter() { let item_obj = LauncherItemObject::new( @@ -248,7 +248,7 @@ impl GtkLauncherUI { ); list_store_for_search.append(&item_obj); } - + // Select first item if list_store_for_search.n_items() > 0 { selection_for_search.set_selected(0); @@ -258,49 +258,53 @@ impl GtkLauncherUI { let launcher_clone = launcher_for_search.clone(); let list_store_clone = list_store_for_search.clone(); let selection_clone = selection_for_search.clone(); - + let current_generation = *search_generation.borrow(); let generation_check = search_generation.clone(); - let _timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(150), move || { - // Check if this search is still the current one - if *generation_check.borrow() != current_generation { - return glib::ControlFlow::Break; // This search was superseded - } - - let launcher_clone = launcher_clone.clone(); - let list_store_clone = list_store_clone.clone(); - let selection_clone = selection_clone.clone(); - let query = query.clone(); - - glib::spawn_future_local(async move { - // Run search and collect items immediately - let items: Vec = { - let mut launcher_ref = launcher_clone.borrow_mut(); - let results = launcher_ref.search(&query); - results.iter().map(|entry| { - LauncherItemObject::new( - entry.title(), - entry.description(), - entry.icon(), - entry.id(), - ) - }).collect() - }; - - // Update UI on main thread - list_store_clone.remove_all(); - for item_obj in items { - list_store_clone.append(&item_obj); - } - - // Select first item - if list_store_clone.n_items() > 0 { - selection_clone.set_selected(0); + let _timeout_id = + glib::timeout_add_local(std::time::Duration::from_millis(150), move || { + // Check if this search is still the current one + if *generation_check.borrow() != current_generation { + return glib::ControlFlow::Break; // This search was superseded } + + let launcher_clone = launcher_clone.clone(); + let list_store_clone = list_store_clone.clone(); + let selection_clone = selection_clone.clone(); + let query = query.clone(); + + glib::spawn_future_local(async move { + // Run search and collect items immediately + let items: Vec = { + let mut launcher_ref = launcher_clone.borrow_mut(); + let results = launcher_ref.search(&query); + results + .iter() + .map(|entry| { + LauncherItemObject::new( + entry.title(), + entry.description(), + entry.icon(), + entry.id(), + ) + }) + .collect() + }; + + // Update UI on main thread + list_store_clone.remove_all(); + for item_obj in items { + list_store_clone.append(&item_obj); + } + + // Select first item + if list_store_clone.n_items() > 0 { + selection_clone.set_selected(0); + } + }); + + glib::ControlFlow::Break }); - - glib::ControlFlow::Break - }); } }); @@ -464,6 +468,7 @@ fn find_icon_file( size: &str, icon_theme: &IconTheme, ) -> Option { + println!("Icon: {}", icon_name); let cache_key = format!("icon:{}:{}", icon_name, size); let cache = waycast_core::cache::get(); diff --git a/waycast-plugins/src/main.rs b/waycast-plugins/src/main.rs index d1f63bb..2955633 100644 --- a/waycast-plugins/src/main.rs +++ b/waycast-plugins/src/main.rs @@ -5,7 +5,12 @@ use waycast_plugins::projects::{ type_scanner::TypeScanner, }; +struct Project { + project_type: String, + path: PathBuf, +} pub fn main() { + let mut projects: Vec = Vec::new(); let scanner = TypeScanner::new(); let framework_detector = FrameworkDetector::new(); if let Ok(entries) = std::fs::read_dir(PathBuf::from("/home/javi/projects")) { @@ -17,17 +22,25 @@ pub fn main() { { let fw = framework_detector.detect(e.path().to_string_lossy().to_string().as_str()); + let mut project_type: String = String::from("NONE"); + if let Some(name) = fw { - println!("{}: {}", e.path().display(), name); + project_type = name; } else { - println!("{}: {}", e.path().display(), "NONE"); + let langs = scanner.scan(e.path(), Some(1)); + if let Some(l) = langs.first() { + project_type = l.name.to_owned() + } } - // let langs = scanner.scan(e.path(), Some(3)); - // // let langs = lang_breakdown(&[e.path().to_str().unwrap()], &[]); - // let top: Vec = langs.iter().map(|l| l.name.to_owned()).collect(); - - // println!("{}: {:?}", e.path().display(), top); + projects.push(Project { + project_type, + path: e.path().to_path_buf(), + }); } } + + for p in projects { + println!("{}: {}", p.path.display(), p.project_type); + } } diff --git a/waycast-plugins/src/projects/framework_detector.rs b/waycast-plugins/src/projects/framework_detector.rs index d2514b0..63f003b 100644 --- a/waycast-plugins/src/projects/framework_detector.rs +++ b/waycast-plugins/src/projects/framework_detector.rs @@ -5,6 +5,10 @@ pub enum Framework { Rails, Vue, NextJS, + Svelte, + Django, + Flask, + Fiber, Ansible, } @@ -13,6 +17,98 @@ crate::frameworks! { files: ["composer.json"], json_checks: [("composer.json", "require.laravel/framework")], }, + Rails { + files: ["Gemfile"], + json_checks: [("package.json", "dependencies.rails"), ("package.json", "devDependencies.rails")], + custom: |project_path: &str| { + use crate::projects::framework_macro::{has_file, read_json_config}; + + // Check for Gemfile with rails gem + if let Ok(content) = std::fs::read_to_string(format!("{}/Gemfile", project_path)) { + if content.contains("gem 'rails'") || content.contains("gem \"rails\"") { + return true; + } + } + + // Check for Rails-specific directories + has_file(project_path, "config/application.rb") || + has_file(project_path, "app/controllers") || + has_file(project_path, "config/routes.rb") + }, + }, + NextJS { + files: ["package.json"], + json_checks: [("package.json", "dependencies.next"), ("package.json", "devDependencies.next")], + custom: |project_path: &str| { + use crate::projects::framework_macro::has_file; + has_file(project_path, "next.config.js") || has_file(project_path, "next.config.mjs") + }, + }, + Vue { + files: ["package.json"], + json_checks: [("package.json", "dependencies.vue"), ("package.json", "devDependencies.vue")], + custom: |project_path: &str| { + use crate::projects::framework_macro::has_file; + has_file(project_path, "vue.config.js") || + has_file(project_path, "src/App.vue") + }, + }, + Svelte { + files: ["package.json"], + json_checks: [("package.json", "dependencies.svelte"), ("package.json", "devDependencies.svelte")], + custom: |project_path: &str| { + use crate::projects::framework_macro::has_file; + has_file(project_path, "svelte.config.js") || + has_file(project_path, "src/App.svelte") + }, + }, + Django { + files: ["manage.py"], + custom: |project_path: &str| { + use crate::projects::framework_macro::has_file; + + // Check for requirements.txt with Django + if let Ok(content) = std::fs::read_to_string(format!("{}/requirements.txt", project_path)) { + if content.contains("Django") || content.contains("django") { + return true; + } + } + + // Check for Django-specific files + has_file(project_path, "settings.py") || + has_file(project_path, "wsgi.py") || + has_file(project_path, "urls.py") + }, + }, + Flask { + custom: |project_path: &str| { + use crate::projects::framework_macro::has_file; + + // Check for requirements.txt with Flask + if let Ok(content) = std::fs::read_to_string(format!("{}/requirements.txt", project_path)) { + if content.contains("Flask") || content.contains("flask") { + return true; + } + } + + // Check for common Flask files + has_file(project_path, "app.py") || + has_file(project_path, "main.py") || + has_file(project_path, "run.py") + }, + }, + Fiber { + files: ["go.mod"], + custom: |project_path: &str| { + // Check for go.mod with fiber dependency + if let Ok(content) = std::fs::read_to_string(format!("{}/go.mod", project_path)) { + if content.contains("github.com/gofiber/fiber") { + return true; + } + } + false + }, + }, Ansible { directories: ["playbooks"], custom: |project_path: &str| { diff --git a/waycast-plugins/src/projects/framework_macro.rs b/waycast-plugins/src/projects/framework_macro.rs index fd73826..bd8e5f6 100644 --- a/waycast-plugins/src/projects/framework_macro.rs +++ b/waycast-plugins/src/projects/framework_macro.rs @@ -95,15 +95,19 @@ macro_rules! frameworks { } )? - // If we have files specified but no other checks, files existing means match - #[allow(unreachable_code)] + // If we reach here and ONLY files were specified (no other validation), + // then files existing means it's a match { - $( - $( - let _ = $file; // Use the file variable to indicate files were specified - return true; - )* - )? + let has_other_checks = false + $(|| { $(let _ = $dir;)* true })? // has directories + $(|| { $(let _ = ($json_file, $json_path);)* true })? // has json_checks + $(|| { let _ = $custom_fn; true })?; // has custom + + if !has_other_checks { + // Only files specified - if we got this far, files exist so it's a match + $($(let _ = $file; return true;)*)? + } + false } } diff --git a/waycast-plugins/src/projects/mod.rs b/waycast-plugins/src/projects/mod.rs index 8acba09..9487030 100644 --- a/waycast-plugins/src/projects/mod.rs +++ b/waycast-plugins/src/projects/mod.rs @@ -1,7 +1,6 @@ pub mod framework_detector; pub mod framework_macro; pub mod type_scanner; -// TODO: Project type detection and icon use std::{ collections::HashSet, fs, @@ -10,14 +9,24 @@ use std::{ sync::Arc, }; +use std::sync::LazyLock; use tokio::sync::Mutex; -use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin}; +use waycast_core::{ + cache::{Cache, CacheTTL}, + LaunchError, LauncherListItem, LauncherPlugin, +}; use waycast_macros::{launcher_entry, plugin}; +use crate::projects::{framework_detector::FrameworkDetector, type_scanner::TypeScanner}; + +static TOKEI_SCANNER: LazyLock = LazyLock::new(TypeScanner::new); +static FRAMEWORK_DETECTOR: LazyLock = LazyLock::new(FrameworkDetector::new); + #[derive(Clone)] pub struct ProjectEntry { path: PathBuf, exec_command: Arc, + project_type: Option, } impl LauncherListItem for ProjectEntry { @@ -26,6 +35,11 @@ impl LauncherListItem for ProjectEntry { title: String::from(self.path.file_name().unwrap().to_string_lossy()), description: Some(self.path.to_string_lossy().to_string()), icon: { + if let Some(t) = &self.project_type { + let icon_path = PathBuf::from("./devicons"); + return icon_path.join(format!("{}.svg", t.to_lowercase())).to_string_lossy().to_string(); + } + String::from("vscode") }, execute: { @@ -118,9 +132,13 @@ impl LauncherPlugin for ProjectsPlugin { continue; } + let project_type = detect_project_type( + path.to_string_lossy().to_string().as_str(), + ); project_entries.push(ProjectEntry { path, exec_command: Arc::clone(&exec_command), + project_type, }); } } @@ -139,6 +157,19 @@ impl LauncherPlugin for ProjectsPlugin { }); } + fn default_list(&self) -> Vec> { + let mut entries: Vec> = Vec::new(); + + // 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() { + entries.push(Box::new(f.clone())); + } + } + + entries + } + fn filter(&self, query: &str) -> Vec> { if query.is_empty() { return self.default_list(); @@ -162,10 +193,6 @@ impl LauncherPlugin for ProjectsPlugin { } } -// fn get_config_value(key: &str) -> Result { -// waycast_config::config_file().get::(format!("plugins.projects.{}", key)) -// } - pub fn new() -> ProjectsPlugin { let search_paths = match waycast_config::get::>("plugins.projects.search_paths") { @@ -190,3 +217,31 @@ pub fn new() -> ProjectsPlugin { files: Arc::new(Mutex::new(Vec::new())), } } + +fn detect_project_type(path: &str) -> Option { + let cache_key = format!("project_type:{}", path); + let cache = waycast_core::cache::get(); + + let detect_fn = |path| { + let fw = FRAMEWORK_DETECTOR.detect(path); + if let Some(name) = fw { + return Some(name); + } else { + let langs = TOKEI_SCANNER.scan(path, Some(1)); + if let Some(l) = langs.first() { + return Some(l.name.to_owned()); + } + } + + None + }; + + let result: Result, waycast_core::cache::errors::CacheError> = + cache.remember_with_ttl(&cache_key, CacheTTL::hours(24), || detect_fn(path)); + + if let Ok(project_type) = result { + return project_type; + } + + detect_fn(path) +}