seela 1.2.0

A simple tmux session manager.
Documentation
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(())
}

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;
                }

                let mut longest_rule_len = 0;
                let mut is_excluded = false;

                for ex in &exclude_paths {
                    if path.starts_with(ex) && ex.as_os_str().len() > longest_rule_len {
                        longest_rule_len = ex.as_os_str().len();
                        is_excluded = true;
                    }
                }

                for s in &search_dirs {
                    if path.starts_with(s) && s.as_os_str().len() >= longest_rule_len {
                        longest_rule_len = s.as_os_str().len();
                        is_excluded = false;
                    }
                }

                if is_excluded {
                    let is_parent_of_search = search_dirs.iter().any(|s| s.starts_with(path));
                    if !is_parent_of_search {
                        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, 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, debug)?;
    }

    Ok(())
}