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| {
// 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();

View File

@ -261,7 +261,8 @@ impl GtkLauncherUI {
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 || {
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
@ -277,14 +278,17 @@ impl GtkLauncherUI {
let items: Vec<LauncherItemObject> = {
let mut launcher_ref = launcher_clone.borrow_mut();
let results = launcher_ref.search(&query);
results.iter().map(|entry| {
results
.iter()
.map(|entry| {
LauncherItemObject::new(
entry.title(),
entry.description(),
entry.icon(),
entry.id(),
)
}).collect()
})
.collect()
};
// Update UI on main thread
@ -464,6 +468,7 @@ fn find_icon_file(
size: &str,
icon_theme: &IconTheme,
) -> Option<std::path::PathBuf> {
println!("Icon: {}", icon_name);
let cache_key = format!("icon:{}:{}", icon_name, size);
let cache = waycast_core::cache::get();

View File

@ -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<Project> = 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<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,
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| {

View File

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

View File

@ -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<TypeScanner> = LazyLock::new(TypeScanner::new);
static FRAMEWORK_DETECTOR: LazyLock<FrameworkDetector> = LazyLock::new(FrameworkDetector::new);
#[derive(Clone)]
pub struct ProjectEntry {
path: PathBuf,
exec_command: Arc<str>,
project_type: Option<String>,
}
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<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>> {
if query.is_empty() {
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 {
let 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())),
}
}
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)
}