This commit is contained in:
Javier Feliz 2025-09-11 19:56:10 -04:00
parent 9fcd0269a8
commit 919e6a44f6
6 changed files with 241 additions and 68 deletions

View File

@ -14,8 +14,8 @@ fn main() {
app.connect_activate(|app| { app.connect_activate(|app| {
// Create the core launcher // Create the core launcher
let launcher = WaycastLauncher::new() let launcher = WaycastLauncher::new()
.add_plugin(Box::new(waycast_plugins::drun::new())) // .add_plugin(Box::new(waycast_plugins::drun::new()))
.add_plugin(Box::new(waycast_plugins::file_search::new())) // .add_plugin(Box::new(waycast_plugins::file_search::new()))
.add_plugin(Box::new(waycast_plugins::projects::new())) .add_plugin(Box::new(waycast_plugins::projects::new()))
.init(); .init();

View File

@ -261,7 +261,8 @@ impl GtkLauncherUI {
let current_generation = *search_generation.borrow(); let current_generation = *search_generation.borrow();
let generation_check = search_generation.clone(); let generation_check = search_generation.clone();
let _timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(150), move || { let _timeout_id =
glib::timeout_add_local(std::time::Duration::from_millis(150), move || {
// Check if this search is still the current one // Check if this search is still the current one
if *generation_check.borrow() != current_generation { if *generation_check.borrow() != current_generation {
return glib::ControlFlow::Break; // This search was superseded return glib::ControlFlow::Break; // This search was superseded
@ -277,14 +278,17 @@ impl GtkLauncherUI {
let items: Vec<LauncherItemObject> = { let items: Vec<LauncherItemObject> = {
let mut launcher_ref = launcher_clone.borrow_mut(); let mut launcher_ref = launcher_clone.borrow_mut();
let results = launcher_ref.search(&query); let results = launcher_ref.search(&query);
results.iter().map(|entry| { results
.iter()
.map(|entry| {
LauncherItemObject::new( LauncherItemObject::new(
entry.title(), entry.title(),
entry.description(), entry.description(),
entry.icon(), entry.icon(),
entry.id(), entry.id(),
) )
}).collect() })
.collect()
}; };
// Update UI on main thread // Update UI on main thread
@ -464,6 +468,7 @@ fn find_icon_file(
size: &str, size: &str,
icon_theme: &IconTheme, icon_theme: &IconTheme,
) -> Option<std::path::PathBuf> { ) -> Option<std::path::PathBuf> {
println!("Icon: {}", icon_name);
let cache_key = format!("icon:{}:{}", icon_name, size); let cache_key = format!("icon:{}:{}", icon_name, size);
let cache = waycast_core::cache::get(); let cache = waycast_core::cache::get();

View File

@ -5,7 +5,12 @@ use waycast_plugins::projects::{
type_scanner::TypeScanner, type_scanner::TypeScanner,
}; };
struct Project {
project_type: String,
path: PathBuf,
}
pub fn main() { pub fn main() {
let mut projects: Vec<Project> = Vec::new();
let scanner = TypeScanner::new(); let scanner = TypeScanner::new();
let framework_detector = FrameworkDetector::new(); let framework_detector = FrameworkDetector::new();
if let Ok(entries) = std::fs::read_dir(PathBuf::from("/home/javi/projects")) { 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 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 { if let Some(name) = fw {
println!("{}: {}", e.path().display(), name); project_type = name;
} else { } 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<String> = langs.iter().map(|l| l.name.to_owned()).collect(); projects.push(Project {
project_type,
path: e.path().to_path_buf(),
});
}
}
// println!("{}: {:?}", e.path().display(), top); for p in projects {
} println!("{}: {}", p.path.display(), p.project_type);
} }
} }

View File

@ -5,6 +5,10 @@ pub enum Framework {
Rails, Rails,
Vue, Vue,
NextJS, NextJS,
Svelte,
Django,
Flask,
Fiber,
Ansible, Ansible,
} }
@ -13,6 +17,98 @@ crate::frameworks! {
files: ["composer.json"], files: ["composer.json"],
json_checks: [("composer.json", "require.laravel/framework")], 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 { Ansible {
directories: ["playbooks"], directories: ["playbooks"],
custom: |project_path: &str| { custom: |project_path: &str| {

View File

@ -95,15 +95,19 @@ macro_rules! frameworks {
} }
)? )?
// If we have files specified but no other checks, files existing means match // If we reach here and ONLY files were specified (no other validation),
#[allow(unreachable_code)] // then files existing means it's a match
{ {
$( let has_other_checks = false
$( $(|| { $(let _ = $dir;)* true })? // has directories
let _ = $file; // Use the file variable to indicate files were specified $(|| { $(let _ = ($json_file, $json_path);)* true })? // has json_checks
return true; $(|| { 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 false
} }
} }

View File

@ -1,7 +1,6 @@
pub mod framework_detector; pub mod framework_detector;
pub mod framework_macro; pub mod framework_macro;
pub mod type_scanner; pub mod type_scanner;
// TODO: Project type detection and icon
use std::{ use std::{
collections::HashSet, collections::HashSet,
fs, fs,
@ -10,14 +9,24 @@ use std::{
sync::Arc, sync::Arc,
}; };
use std::sync::LazyLock;
use tokio::sync::Mutex; 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 waycast_macros::{launcher_entry, plugin};
use crate::projects::{framework_detector::FrameworkDetector, type_scanner::TypeScanner};
static TOKEI_SCANNER: LazyLock<TypeScanner> = LazyLock::new(TypeScanner::new);
static FRAMEWORK_DETECTOR: LazyLock<FrameworkDetector> = LazyLock::new(FrameworkDetector::new);
#[derive(Clone)] #[derive(Clone)]
pub struct ProjectEntry { pub struct ProjectEntry {
path: PathBuf, path: PathBuf,
exec_command: Arc<str>, exec_command: Arc<str>,
project_type: Option<String>,
} }
impl LauncherListItem for ProjectEntry { impl LauncherListItem for ProjectEntry {
@ -26,6 +35,11 @@ impl LauncherListItem for ProjectEntry {
title: String::from(self.path.file_name().unwrap().to_string_lossy()), title: String::from(self.path.file_name().unwrap().to_string_lossy()),
description: Some(self.path.to_string_lossy().to_string()), description: Some(self.path.to_string_lossy().to_string()),
icon: { 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") String::from("vscode")
}, },
execute: { execute: {
@ -118,9 +132,13 @@ impl LauncherPlugin for ProjectsPlugin {
continue; continue;
} }
let project_type = detect_project_type(
path.to_string_lossy().to_string().as_str(),
);
project_entries.push(ProjectEntry { project_entries.push(ProjectEntry {
path, path,
exec_command: Arc::clone(&exec_command), exec_command: Arc::clone(&exec_command),
project_type,
}); });
} }
} }
@ -139,6 +157,19 @@ impl LauncherPlugin for ProjectsPlugin {
}); });
} }
fn default_list(&self) -> Vec<Box<dyn LauncherListItem>> {
let mut entries: Vec<Box<dyn LauncherListItem>> = 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<Box<dyn LauncherListItem>> { fn filter(&self, query: &str) -> Vec<Box<dyn LauncherListItem>> {
if query.is_empty() { if query.is_empty() {
return self.default_list(); return self.default_list();
@ -162,10 +193,6 @@ impl LauncherPlugin for ProjectsPlugin {
} }
} }
// fn get_config_value<T>(key: &str) -> Result<T> {
// waycast_config::config_file().get::<T>(format!("plugins.projects.{}", key))
// }
pub fn new() -> ProjectsPlugin { pub fn new() -> ProjectsPlugin {
let search_paths = let search_paths =
match waycast_config::get::<HashSet<PathBuf>>("plugins.projects.search_paths") { match waycast_config::get::<HashSet<PathBuf>>("plugins.projects.search_paths") {
@ -190,3 +217,31 @@ pub fn new() -> ProjectsPlugin {
files: Arc::new(Mutex::new(Vec::new())), files: Arc::new(Mutex::new(Vec::new())),
} }
} }
fn detect_project_type(path: &str) -> Option<String> {
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<Option<String>, 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)
}