Skip to main content

browser_automation_cli/
find_paths.rs

1//! One-shot filesystem path discovery (`find-paths`, fd-like UX; no Chrome).
2
3use std::path::{Path, PathBuf};
4
5use ignore::WalkBuilder;
6use regex::RegexBuilder;
7use serde_json::{json, Value};
8
9use crate::error::{CliError, ErrorKind};
10
11/// Options for `find-paths`.
12#[derive(Debug, Clone)]
13pub struct FindPathsOpts {
14    /// Regex pattern on file name (empty = match all).
15    pub pattern: String,
16    /// Root paths to walk.
17    pub roots: Vec<PathBuf>,
18    /// Extension filter without dot (e.g. `rs`).
19    pub extension: Option<String>,
20    /// Include hidden entries.
21    pub hidden: bool,
22    /// Do not respect .gitignore/.ignore.
23    pub no_ignore: bool,
24    /// Max walk depth.
25    pub max_depth: Option<usize>,
26    /// Entry type: `f` file, `d` dir, empty = both.
27    pub entry_type: Option<String>,
28    /// Max results (0 = unlimited).
29    pub limit: usize,
30}
31
32impl Default for FindPathsOpts {
33    fn default() -> Self {
34        Self {
35            pattern: String::new(),
36            roots: vec![PathBuf::from(".")],
37            extension: None,
38            hidden: false,
39            no_ignore: false,
40            max_depth: None,
41            entry_type: None,
42            limit: 10_000,
43        }
44    }
45}
46
47/// Walk roots and return matching paths (one-shot).
48pub fn find_paths(opts: &FindPathsOpts) -> Result<Value, CliError> {
49    let roots = if opts.roots.is_empty() {
50        vec![PathBuf::from(".")]
51    } else {
52        opts.roots.clone()
53    };
54    let re = if opts.pattern.is_empty() {
55        None
56    } else {
57        Some(
58            RegexBuilder::new(&opts.pattern)
59                .case_insensitive(true)
60                .build()
61                .map_err(|e| {
62                    CliError::new(ErrorKind::Usage, format!("invalid find-paths pattern: {e}"))
63                })?,
64        )
65    };
66    let mut paths = Vec::new();
67    for root in &roots {
68        let mut builder = WalkBuilder::new(root);
69        builder.hidden(!opts.hidden);
70        builder.git_ignore(!opts.no_ignore);
71        builder.git_global(!opts.no_ignore);
72        builder.git_exclude(!opts.no_ignore);
73        builder.ignore(!opts.no_ignore);
74        if let Some(d) = opts.max_depth {
75            builder.max_depth(Some(d));
76        }
77        builder.threads(
78            std::thread::available_parallelism()
79                .map(|n| n.get())
80                .unwrap_or(2),
81        );
82        for entry in builder.build() {
83            let entry = match entry {
84                Ok(e) => e,
85                Err(_) => continue,
86            };
87            let ft = entry.file_type();
88            let is_file = ft.map(|t| t.is_file()).unwrap_or(false);
89            let is_dir = ft.map(|t| t.is_dir()).unwrap_or(false);
90            if let Some(ref t) = opts.entry_type {
91                match t.as_str() {
92                    "f" | "file" if !is_file => continue,
93                    "d" | "dir" | "directory" if !is_dir => continue,
94                    _ => {}
95                }
96            }
97            let path = entry.path();
98            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
99            if let Some(ref ext) = opts.extension {
100                let e = path.extension().and_then(|x| x.to_str()).unwrap_or("");
101                if !e.eq_ignore_ascii_case(ext.trim_start_matches('.')) {
102                    continue;
103                }
104            }
105            if let Some(ref re) = re {
106                if !re.is_match(name) && !re.is_match(&path.to_string_lossy()) {
107                    continue;
108                }
109            }
110            paths.push(path.display().to_string());
111            if opts.limit > 0 && paths.len() >= opts.limit {
112                break;
113            }
114        }
115        if opts.limit > 0 && paths.len() >= opts.limit {
116            break;
117        }
118    }
119    Ok(json!({
120        "count": paths.len(),
121        "paths": paths,
122        "pattern": opts.pattern,
123        "engine": "ignore",
124        "chrome": false,
125    }))
126}
127
128/// Normalize roots from CLI paths.
129pub fn roots_from(paths: &[String]) -> Vec<PathBuf> {
130    if paths.is_empty() {
131        vec![PathBuf::from(".")]
132    } else {
133        paths.iter().map(|p| Path::new(p).to_path_buf()).collect()
134    }
135}