Started framework detection

This commit is contained in:
Javier Feliz 2025-09-11 18:45:34 -04:00
parent 9d19cdd5f8
commit 16e9c422a8
7 changed files with 172 additions and 45 deletions

2
Cargo.lock generated
View File

@ -2390,6 +2390,8 @@ dependencies = [
"directories",
"gio",
"glib",
"serde",
"serde_json",
"tokei",
"tokio",
"walkdir",

View File

@ -156,10 +156,4 @@ docs: ## Build and open documentation
# Development tools installation
tools: ## Install useful development tools
cargo install cargo-watch cargo-audit cargo-machete cargo-flamegraph cargo-deb cargo-outdated
@echo "Development tools installed!"
nix-install:
nix profile install .
nix-reinstall:
nix profile install --reinstall .
@echo "Development tools installed!"

View File

@ -19,6 +19,8 @@ tokio = { version = "1.0", features = [
] }
walkdir = "2.5.0"
tokei = "12.1.2"
serde = "1.0.219"
serde_json = "1.0.143"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(rust_analyzer)'] }

View File

@ -1,46 +1,33 @@
use std::{collections::BTreeMap, path::PathBuf};
use tokei::{Config, LanguageType, Languages};
fn lang_breakdown(paths: &[&str], excluded: &[&str]) -> Vec<(LanguageType, usize, f64)> {
let mut langs = Languages::new();
let cfg = Config::default();
langs.get_statistics(paths, excluded, &cfg);
let total_code: usize = langs.iter().map(|(_, l)| l.code).sum();
let mut rows: Vec<_> = langs
.iter()
.map(|(lt, l)| {
(
*lt,
l.code,
if total_code > 0 {
(l.code as f64) * 100.0 / (total_code as f64)
} else {
0.0
},
)
})
.collect();
rows.sort_by_key(|(_, lines, _)| std::cmp::Reverse(*lines));
rows
}
use waycast_plugins::projects::{
framework_detector::{self, FrameworkDetector},
type_scanner::TypeScanner,
};
pub fn main() {
let scanner = TypeScanner::new();
let framework_detector = FrameworkDetector::new();
if let Ok(entries) = std::fs::read_dir(PathBuf::from("/home/javi/projects")) {
for e in entries
.into_iter()
.filter(|e| e.is_ok())
.map(|e| e.unwrap())
.filter(|e| e.path().is_dir())
{
let langs = lang_breakdown(&[e.path().to_str().unwrap()], &[]);
let fw = framework_detector.detect(e.path().to_string_lossy().to_string().as_str());
let top = langs
.iter()
.map(|(l, _, _)| l.to_owned())
.take(3)
.collect::<Vec<LanguageType>>();
if let Some(name) = fw {
println!("{}: {}", e.path().display(), name);
} else {
println!("{}: {}", e.path().display(), "NONE");
}
// let langs = scanner.scan(e.path(), Some(3));
// // let langs = lang_breakdown(&[e.path().to_str().unwrap()], &[]);
println!("{}: {:?}", e.path().display(), top);
// let top: Vec<String> = langs.iter().map(|l| l.name.to_owned()).collect();
// println!("{}: {:?}", e.path().display(), top);
}
}
}

View File

@ -0,0 +1,84 @@
use std::path::{Path, PathBuf};
pub enum Framework {
Laravel,
Rails,
Vue,
NextJS,
Ansible,
}
fn has_file<P: AsRef<Path>>(project_path: P, file: P) -> bool {
PathBuf::from(project_path.as_ref()).join(file).exists()
}
fn read_json_config<P: AsRef<Path>>(project_path: P, file: P) -> Option<serde_json::Value> {
let pb = PathBuf::from(project_path.as_ref());
if let Ok(text) = std::fs::read_to_string(pb.join(file)) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
return Some(v);
}
} else {
return None;
}
None
}
trait FrameworkHeuristics: Sync {
fn name(&self) -> &'static str;
fn matches(&self, project_path: &str) -> bool;
}
struct Laravel;
impl FrameworkHeuristics for Laravel {
fn name(&self) -> &'static str {
"Laravel"
}
fn matches(&self, project_path: &str) -> bool {
// Check for composer.json
if !has_file(project_path, "composer.json") {
return false;
}
// If composer.json has "laravel/framework"
// we can say yes immediately
if let Some(cfg) = read_json_config(project_path, "composer.json") {
let requires_laravel = cfg
.get("require")
.and_then(|r| r.get("laravel/framework"))
.is_some();
if requires_laravel {
return true;
}
}
false
}
}
pub struct FrameworkDetector {
heuristics: &'static [&'static dyn FrameworkHeuristics],
}
static LARAVEL: Laravel = Laravel;
static HEURISTICS: &[&dyn FrameworkHeuristics] = &[&LARAVEL];
impl FrameworkDetector {
pub fn new() -> FrameworkDetector {
FrameworkDetector {
heuristics: HEURISTICS,
}
}
pub fn detect(&self, project_path: &str) -> Option<String> {
for h in self.heuristics {
if h.matches(project_path) {
return Some(String::from(h.name()));
}
}
None
}
}

View File

@ -1,10 +1,5 @@
// TODO: Use the user's preferred editor.
// This should just be in the config when I implement
// that eventually since figuring out every editor's
// launch option would be a pain. The user can just
// configure launch_command and pass a parameter.
// Example: code -n {path}
// and I'll just regex in the path.
pub mod framework_detector;
pub mod type_scanner;
// TODO: Project type detection and icon
use std::{
collections::HashSet,

View File

@ -0,0 +1,63 @@
use std::path::Path;
use tokei::{Config, Language, LanguageType, Languages};
pub struct ProjectLanguage {
pub name: String,
pub percentage: f64,
}
pub struct TypeScanner {
tokei_config: Config,
ignore_langs: [LanguageType; 5],
}
impl TypeScanner {
pub fn new() -> TypeScanner {
TypeScanner {
tokei_config: Config::default(),
ignore_langs: [
LanguageType::Css,
LanguageType::Json,
LanguageType::Markdown,
LanguageType::CppHeader,
LanguageType::CHeader,
],
}
}
// Scan a project for languages used. Limit gives the
// top [limit] entries
pub fn scan<P: AsRef<Path>>(&self, path: P, limit: Option<usize>) -> Vec<ProjectLanguage> {
let mut langs = Languages::new();
langs.get_statistics(&[path], &[], &self.tokei_config);
let total_code: usize = langs.iter().map(|(_, l)| l.code).sum();
let mut rows: Vec<ProjectLanguage> = langs
.iter()
.map(|(lt, l)| {
(
*lt,
l.code,
if total_code > 0 {
(l.code as f64) * 100.0 / (total_code as f64)
} else {
0.0
},
)
})
.filter(|(l, _, _)| !self.ignore_langs.contains(l))
.map(|(lang, _, percent)| ProjectLanguage {
name: String::from(lang.name()),
percentage: percent,
})
.collect();
rows.sort_by(|a, b| b.percentage.partial_cmp(&a.percentage).unwrap());
if let Some(l) = limit {
rows.truncate(l);
}
rows
}
}