use crate::config::{Config, expand_path};
use rayon::prelude::*;
use std::error::Error;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
pub fn run_confirm(cmd: &str) -> Result<(), Box<dyn Error>> {
print!("Run \"{cmd}\"? [Y/n] ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input.is_empty() || input == "y" || input == "yes" {
let status = Command::new("sh").arg("-c").arg(cmd).status()?;
if !status.success() {
eprintln!("Command exited with status: {}", status);
}
} else {
println!("Skipped.");
}
Ok(())
}
fn is_excluded(path: &Path, exclude_paths: &[PathBuf], search_dirs: &[PathBuf]) -> bool {
let excluded_by = exclude_paths
.iter()
.filter(|ex| path.starts_with(ex.as_path()))
.max_by_key(|ex| ex.as_os_str().len());
let Some(exclude_rule) = excluded_by else {
return false;
};
!search_dirs
.iter()
.any(|s| path.starts_with(s) && s.as_os_str().len() > exclude_rule.as_os_str().len())
}
pub fn find_projects(config: &Config) -> Vec<PathBuf> {
let search_dirs: Vec<PathBuf> = config
.folders
.search_dirs
.iter()
.map(|s| expand_path(s))
.collect();
let exclude_paths: Vec<PathBuf> = config
.folders
.exclude_paths
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|s| expand_path(s))
.collect();
let force_include: Vec<PathBuf> = config
.folders
.force_include
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|s| expand_path(s))
.collect();
let mut projects: Vec<PathBuf> = force_include.into_iter().filter(|p| p.exists()).collect();
let discovered: Vec<PathBuf> = search_dirs
.par_iter()
.filter(|root| root.exists())
.flat_map(|root| {
let mut it = WalkDir::new(root).into_iter();
let mut results = Vec::new();
loop {
let entry = match it.next() {
None => break,
Some(Ok(entry)) => entry,
Some(Err(_)) => continue,
};
let path = entry.path();
if !entry.file_type().is_dir() {
continue;
}
if is_excluded(path, &exclude_paths, &search_dirs) {
let has_search_dir_below = search_dirs.iter().any(|s| s.starts_with(path));
if !has_search_dir_below {
it.skip_current_dir();
}
continue;
}
if path.join(".git").exists() {
results.push(path.to_path_buf());
it.skip_current_dir();
continue;
}
}
results
})
.collect();
for p in discovered {
if !projects.contains(&p) {
projects.push(p);
}
}
projects
}
pub fn run(
config: &Config,
config_dir: &Path,
debug: bool,
headless: bool,
) -> Result<(), Box<dyn Error>> {
if debug {
println!("Loaded Config: {config:#?}");
}
let projects = find_projects(config);
let project_strings = projects
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>();
if headless {
println!("Headless mode enabled. Skipping fzf and tmux.");
if debug {
println!("Found {} projects", project_strings.len());
}
return Ok(());
}
if let Some(selected) = crate::fzf::select_project(&project_strings, &config.fzf)? {
crate::tmux::open_session(Path::new(&selected), config, config_dir, debug)?;
}
Ok(())
}