use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process;
use clap::{CommandFactory, Parser};
use clap_complete::Shell;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[derive(Parser)]
#[command(name = "srcwalk", version, about)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
query: Option<String>,
#[arg(long, default_value = ".")]
scope: PathBuf,
#[arg(long)]
section: Option<String>,
#[arg(long)]
budget: Option<u64>,
#[arg(long)]
no_budget: bool,
#[arg(long)]
full: bool,
#[arg(long, conflicts_with_all = ["callers", "callees", "deps", "map", "expand", "glob"])]
path_exact: bool,
#[arg(long, conflicts_with = "map")]
json: bool,
#[arg(long, num_args = 0..=1, default_missing_value = "2", require_equals = true)]
expand: Option<usize>,
#[arg(long)]
glob: Option<String>,
#[arg(long, conflicts_with_all = ["callees", "deps", "map", "flow", "impact"])]
callers: bool,
#[arg(long, value_name = "QUALIFIERS", conflicts_with = "map")]
filter: Option<String>,
#[arg(long, requires = "callers", value_name = "FIELD")]
count_by: Option<String>,
#[arg(long, conflicts_with_all = ["callers", "deps", "map", "flow", "impact"])]
callees: bool,
#[arg(long, requires = "callees")]
detailed: bool,
#[arg(long, value_name = "N")]
depth: Option<usize>,
#[arg(long, value_name = "K", requires = "callers")]
max_frontier: Option<usize>,
#[arg(long, value_name = "M", requires = "callers")]
max_edges: Option<usize>,
#[arg(long, value_name = "CSV", requires = "callers")]
skip_hubs: Option<String>,
#[arg(long, conflicts_with_all = ["callers", "callees", "map", "flow", "impact"])]
deps: bool,
#[arg(long, conflicts_with_all = ["callers", "callees", "deps", "map", "impact", "expand", "section", "full"])]
flow: bool,
#[arg(long, conflicts_with_all = ["callers", "callees", "deps", "map", "flow", "expand", "section", "full"])]
impact: bool,
#[arg(long, conflicts_with_all = ["callers", "callees", "deps", "flow", "impact", "expand", "section", "full"])]
map: bool,
#[arg(long, requires = "map")]
symbols: bool,
#[arg(long, value_name = "N")]
limit: Option<usize>,
#[arg(long, value_name = "N", default_value = "0")]
offset: usize,
#[arg(long, value_name = "SHELL")]
completions: Option<Shell>,
}
#[derive(clap::Subcommand)]
enum Command {
Overview,
}
#[cfg(unix)]
fn reset_sigpipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn reset_sigpipe() {}
fn main() {
reset_sigpipe();
configure_thread_pools();
let cli = Cli::parse();
if let Some(shell) = cli.completions {
clap_complete::generate(shell, &mut Cli::command(), "srcwalk", &mut io::stdout());
return;
}
if let Some(cmd) = cli.command {
match cmd {
Command::Overview => {
let cwd = std::env::current_dir().unwrap_or_default();
let output = srcwalk::overview::fingerprint(&cwd);
if output.is_empty() {
eprintln!("No project fingerprint could be generated.");
process::exit(1);
}
println!("{output}");
}
}
return;
}
let is_tty = io::stdout().is_terminal();
let effective_budget = if cli.no_budget {
None
} else if cli.budget.is_some() {
cli.budget
} else if !is_tty {
Some(5_000)
} else {
None
};
if cli.map {
let cache = srcwalk::cache::OutlineCache::new();
let scope = cli.scope.canonicalize().unwrap_or(cli.scope);
let depth = cli.depth.unwrap_or(3);
match srcwalk::map::generate(
&scope,
depth,
effective_budget,
&cache,
cli.symbols,
cli.glob.as_deref(),
) {
Ok(output) => emit_output(&output, is_tty),
Err(e) => {
eprintln!("{e}");
process::exit(e.exit_code());
}
}
return;
}
let query = if let Some(q) = cli.query {
q
} else {
eprintln!("usage: srcwalk <query> [--scope DIR] [--section N-M] [--budget N]");
process::exit(3);
};
let cache = srcwalk::cache::OutlineCache::new();
let scope = cli.scope.canonicalize().unwrap_or(cli.scope);
let full = cli.full;
let expand = cli.expand.unwrap_or(0);
let effective_limit = cli.limit.or({
if is_tty && !full {
Some(50)
} else {
None
}
});
if cli.path_exact {
let result = srcwalk::run_path_exact(
&query,
&scope,
cli.section.as_deref(),
effective_budget,
full,
&cache,
);
emit_result(result, &query, cli.json, is_tty);
return;
}
if cli.filter.is_some()
&& (cli.deps || cli.map || cli.path_exact || cli.impact || (cli.callees && !cli.detailed))
{
eprintln!("error: --filter applies to search results, direct --callers, --flow, and --callees --detailed");
process::exit(2);
}
if cli.flow {
let result = srcwalk::run_flow(
&query,
&scope,
effective_budget,
&cache,
cli.depth,
cli.filter.as_deref(),
);
emit_result(result, &query, cli.json, is_tty);
return;
}
if cli.impact {
let result = srcwalk::run_impact(&query, &scope, effective_budget, &cache);
emit_result(result, &query, cli.json, is_tty);
return;
}
if cli.callers {
let bfs_json = cli.json && matches!(cli.depth, Some(d) if d >= 2);
let result = srcwalk::run_callers(
&query,
&scope,
expand,
effective_budget,
effective_limit,
cli.offset,
cli.glob.as_deref(),
&cache,
cli.depth,
cli.max_frontier,
cli.max_edges,
cli.skip_hubs.as_deref(),
cli.filter.as_deref(),
cli.count_by.as_deref(),
bfs_json,
);
if bfs_json {
match result {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("error: {e}");
process::exit(e.exit_code());
}
}
return;
}
emit_result(result, &query, cli.json, is_tty);
return;
}
if cli.callees {
let result = srcwalk::run_callees(
&query,
&scope,
effective_budget,
&cache,
cli.depth,
cli.detailed,
cli.filter.as_deref(),
);
emit_result(result, &query, cli.json, is_tty);
return;
}
if cli.deps {
let path = if Path::new(&query).is_absolute() {
PathBuf::from(&query)
} else {
let scope_path = scope.join(&query);
if scope_path.exists() {
scope_path
} else {
let cwd_path = std::env::current_dir().unwrap_or_default().join(&query);
if cwd_path.exists() {
cwd_path
} else {
scope_path }
}
};
let result = srcwalk::run_deps(
&path,
&scope,
effective_budget,
&cache,
cli.limit,
cli.offset,
);
emit_result(result, &query, cli.json, is_tty);
return;
}
let result = if expand > 0 {
srcwalk::run_expanded_filtered(
&query,
&scope,
cli.section.as_deref(),
effective_budget,
full,
expand,
effective_limit,
cli.offset,
cli.glob.as_deref(),
cli.filter.as_deref(),
&cache,
)
} else if full {
srcwalk::run_full_filtered(
&query,
&scope,
cli.section.as_deref(),
effective_budget,
effective_limit,
cli.offset,
cli.glob.as_deref(),
cli.filter.as_deref(),
&cache,
)
} else {
srcwalk::run_filtered(
&query,
&scope,
cli.section.as_deref(),
effective_budget,
effective_limit,
cli.offset,
cli.glob.as_deref(),
cli.filter.as_deref(),
&cache,
)
};
emit_result(result, &query, cli.json, is_tty);
}
fn emit_result(
result: Result<String, srcwalk::error::SrcwalkError>,
query: &str,
json: bool,
is_tty: bool,
) {
match result {
Ok(output) => {
if json {
let json = serde_json::json!({
"query": query,
"output": output,
});
println!(
"{}",
serde_json::to_string_pretty(&json)
.expect("serde_json::Value is always serializable")
);
} else {
emit_output(&output, is_tty);
}
}
Err(e) => {
eprintln!("{e}");
process::exit(e.exit_code());
}
}
}
fn emit_output(output: &str, is_tty: bool) {
let line_count = output.lines().count();
let term_height = terminal_height();
if is_tty && line_count > term_height {
let pager = std::env::var("PAGER").unwrap_or_else(|_| "less".into());
if let Ok(mut child) = process::Command::new(&pager)
.arg("-R")
.stdin(process::Stdio::piped())
.spawn()
{
if let Some(ref mut stdin) = child.stdin.take() {
let _ = stdin.write_all(output.as_bytes());
}
let _ = child.wait();
return;
}
}
print!("{output}");
let _ = io::stdout().flush();
}
fn terminal_height() -> usize {
if let Ok(lines) = std::env::var("LINES") {
if let Ok(h) = lines.parse::<usize>() {
return h;
}
}
24
}
fn configure_thread_pools() {
let num_threads = std::env::var("SRCWALK_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or_else(|| {
std::thread::available_parallelism().map_or(4, |n| (n.get() / 2).clamp(2, 6))
});
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()
.ok();
}