seela 1.5.1

A fast and customizable tmux session manager.
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;
    };

    // A search_dir more specific than the exclude rule re-opens the path.
    !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(())
}