Progress
This commit is contained in:
parent
9fcd0269a8
commit
919e6a44f6
@ -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();
|
||||
|
||||
|
@ -261,46 +261,50 @@ 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 || {
|
||||
// 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<LauncherItemObject> = {
|
||||
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);
|
||||
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
|
||||
}
|
||||
|
||||
// Select first item
|
||||
if list_store_clone.n_items() > 0 {
|
||||
selection_clone.set_selected(0);
|
||||
}
|
||||
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<LauncherItemObject> = {
|
||||
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<std::path::PathBuf> {
|
||||
println!("Icon: {}", icon_name);
|
||||
let cache_key = format!("icon:{}:{}", icon_name, size);
|
||||
let cache = waycast_core::cache::get();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -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| {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user