183 lines
5.8 KiB
Rust
183 lines
5.8 KiB
Rust
// 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.
|
|
// TODO: Project type detection and icon
|
|
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
sync::Arc,
|
|
};
|
|
|
|
use tokio::sync::Mutex;
|
|
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
|
use waycast_macros::{launcher_entry, plugin};
|
|
|
|
#[derive(Clone)]
|
|
pub struct ProjectEntry {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl LauncherListItem for ProjectEntry {
|
|
launcher_entry! {
|
|
id: self.path.to_string_lossy().to_string(),
|
|
title: String::from(self.path.file_name().unwrap().to_string_lossy()),
|
|
description: Some(self.path.to_string_lossy().to_string()),
|
|
icon: {
|
|
String::from("vscode")
|
|
},
|
|
execute: {
|
|
println!("Executing: {}", self.path.display());
|
|
|
|
// Use xdg-open directly since it works properly with music files
|
|
match Command::new("code").arg("-n").arg(&self.path).spawn() {
|
|
Ok(_) => {
|
|
println!("Successfully opened with code");
|
|
Ok(())
|
|
}
|
|
Err(_) => Err(LaunchError::CouldNotLaunch("Failed to open project folder".into())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ProjectsPlugin {
|
|
search_paths: Vec<PathBuf>,
|
|
skip_dirs: Vec<String>,
|
|
// Running list of files in memory
|
|
files: Arc<Mutex<Vec<ProjectEntry>>>,
|
|
}
|
|
|
|
impl ProjectsPlugin {
|
|
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
|
|
let p = path.as_ref();
|
|
|
|
if !p.exists() {
|
|
return Err(format!("Path does not exist: {}", p.display()));
|
|
}
|
|
|
|
if !p.is_dir() {
|
|
return Err(format!("Path is not a directory: {}", p.display()));
|
|
}
|
|
|
|
self.search_paths.push(p.to_path_buf());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn add_skip_dir(&mut self, directory_name: String) -> Result<(), String> {
|
|
self.skip_dirs.push(directory_name);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn should_skip_dir(dir_name: &str, skip_dirs: &[String]) -> bool {
|
|
skip_dirs.iter().any(|skip| skip == dir_name)
|
|
}
|
|
|
|
impl LauncherPlugin for ProjectsPlugin {
|
|
plugin! {
|
|
name: "Projects",
|
|
priority: 800,
|
|
description: "Search and open code projects",
|
|
prefix: "proj",
|
|
init: projects_init,
|
|
default_list: projects_default_list,
|
|
filter: projects_filter
|
|
}
|
|
}
|
|
|
|
fn projects_default_list(_plugin: &ProjectsPlugin) -> Vec<Box<dyn LauncherListItem>> {
|
|
Vec::new()
|
|
}
|
|
|
|
fn projects_filter(plugin: &ProjectsPlugin, query: &str) -> Vec<Box<dyn LauncherListItem>> {
|
|
if query.is_empty() {
|
|
return projects_default_list(plugin);
|
|
}
|
|
|
|
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) = plugin.files.try_lock() {
|
|
for f in files.iter() {
|
|
if let Some(file_name) = f.path.file_name() {
|
|
let cmp = file_name.to_string_lossy().to_lowercase();
|
|
if cmp.contains(&query.to_lowercase()) {
|
|
entries.push(Box::new(f.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
entries
|
|
}
|
|
|
|
fn projects_init(plugin: &ProjectsPlugin) {
|
|
let files_clone = Arc::clone(&plugin.files);
|
|
let search_paths = plugin.search_paths.clone();
|
|
let skip_dirs = plugin.skip_dirs.clone();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
let mut project_entries = Vec::new();
|
|
|
|
for search_path in &search_paths {
|
|
if let Ok(entries) = fs::read_dir(search_path) {
|
|
for entry in entries.flatten() {
|
|
if let Ok(file_type) = entry.file_type() {
|
|
if file_type.is_dir() {
|
|
let path = entry.path();
|
|
|
|
// Skip hidden directories (starting with .)
|
|
if let Some(file_name) = path.file_name() {
|
|
if let Some(name_str) = file_name.to_str() {
|
|
// Skip hidden directories
|
|
if name_str.starts_with('.') {
|
|
continue;
|
|
}
|
|
|
|
// Skip directories in skip list
|
|
if should_skip_dir(name_str, &skip_dirs) {
|
|
continue;
|
|
}
|
|
|
|
println!("{}", path.display());
|
|
|
|
project_entries.push(ProjectEntry { path });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the shared files collection
|
|
let mut files_guard = files_clone.lock().await;
|
|
*files_guard = project_entries;
|
|
|
|
println!("Projects plugin: Found {} projects", files_guard.len());
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn new() -> ProjectsPlugin {
|
|
ProjectsPlugin {
|
|
search_paths: Vec::new(),
|
|
skip_dirs: vec![
|
|
String::from("vendor"),
|
|
String::from("node_modules"),
|
|
String::from("cache"),
|
|
String::from("zig-cache"),
|
|
String::from(".git"),
|
|
String::from(".svn"),
|
|
],
|
|
files: Arc::new(Mutex::new(Vec::new())),
|
|
}
|
|
}
|