use crate::types::GlobOutput;
use crate::types::SortOrder;
use crate::walker::{self};
use agentic_tools_core::ToolError;
use globset::Glob;
use ignore::WalkBuilder;
use std::path::Path;
#[derive(Debug)]
pub struct GlobConfig {
pub root: String,
pub pattern: String,
pub ignore_globs: Vec<String>,
pub include_hidden: bool,
pub sort: SortOrder,
pub head_limit: usize,
pub offset: usize,
}
const MAX_HEAD_LIMIT: usize = 1000;
#[derive(Debug)]
struct GlobEntry {
rel_path: String,
mtime: Option<std::time::SystemTime>,
}
pub fn run(cfg: GlobConfig) -> Result<GlobOutput, ToolError> {
let root_path = Path::new(&cfg.root);
if !root_path.exists() {
return Err(ToolError::invalid_input(format!(
"Path does not exist: {}",
cfg.root
)));
}
let pattern_glob = Glob::new(&cfg.pattern).map_err(|e| {
ToolError::invalid_input(format!("Invalid glob pattern '{}': {}", cfg.pattern, e))
})?;
let pattern_matcher = pattern_glob.compile_matcher();
let ignore_gs = walker::build_ignore_globset(&cfg.ignore_globs)?;
let head_limit = cfg.head_limit.min(MAX_HEAD_LIMIT);
let mut warnings: Vec<String> = Vec::new();
let mut entries: Vec<GlobEntry> = Vec::new();
let mut builder = WalkBuilder::new(root_path);
builder.hidden(!cfg.include_hidden);
builder.git_ignore(true);
builder.git_global(true);
builder.git_exclude(true);
builder.parents(false);
builder.follow_links(false);
let root_clone = root_path.to_path_buf();
builder.filter_entry(move |entry| {
let rel = entry
.path()
.strip_prefix(&root_clone)
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
if rel.is_empty() {
return true;
}
!ignore_gs.is_match(&rel)
});
for result in builder.build() {
match result {
Ok(entry) => {
let path = entry.path();
if path == root_path {
continue;
}
let rel_path = path.strip_prefix(root_path).map_or_else(
|_| path.to_string_lossy().to_string(),
|p| p.to_string_lossy().replace('\\', "/"),
);
if !pattern_matcher.is_match(&rel_path) {
continue;
}
let mtime = if matches!(cfg.sort, SortOrder::Mtime) {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
} else {
None
};
entries.push(GlobEntry { rel_path, mtime });
}
Err(e) => {
warnings.push(format!("Walk error: {e}"));
}
}
}
match cfg.sort {
SortOrder::Name => {
entries.sort_by(|a, b| a.rel_path.to_lowercase().cmp(&b.rel_path.to_lowercase()));
}
SortOrder::Mtime => {
entries.sort_by(|a, b| {
match (&b.mtime, &a.mtime) {
(Some(b_time), Some(a_time)) => b_time.cmp(a_time),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => {
a.rel_path.to_lowercase().cmp(&b.rel_path.to_lowercase())
}
}
});
}
}
let total_count = entries.len();
let paginated: Vec<String> = entries
.into_iter()
.skip(cfg.offset)
.take(head_limit)
.map(|e| e.rel_path)
.collect();
let has_more = total_count > cfg.offset + paginated.len();
Ok(GlobOutput {
root: cfg.root,
entries: paginated,
has_more,
warnings,
})
}