#![allow(rustdoc::invalid_rust_codeblocks)]
use camino::Utf8PathBuf as PathBuf;
use clap::{Parser, Subcommand};
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use miette::{IntoDiagnostic, WrapErr};
use std::io::{self, BufWriter, Write};
use yadr::{Language, YAdr};
#[derive(Parser, Debug)]
#[command(author, version, verbatim_doc_comment)]
struct Args {
#[command(subcommand)]
mode: Mode,
}
#[derive(clap::Args, Debug)]
struct Scope {
#[arg(long)]
exclude: Vec<String>,
#[arg(default_value_t = PathBuf::from("."))]
root: PathBuf,
}
#[derive(Subcommand, Debug)]
enum Mode {
#[clap(alias = "ls")]
List {
#[clap(short = 'L')]
only_files: bool,
#[command(flatten)]
scope: Scope,
},
Check {
#[command(flatten)]
scope: Scope,
},
Show {
id: String,
#[command(flatten)]
scope: Scope,
},
}
impl Mode {
fn scope(&self) -> &Scope {
match self {
Mode::List { scope, .. } | Mode::Check { scope } | Mode::Show { scope, .. } => scope,
}
}
}
fn main() -> miette::Result<()> {
let args = Args::parse();
let scope = args.mode.scope();
let mut walk = WalkBuilder::new(&scope.root);
walk.hidden(false);
let mut excludes = OverrideBuilder::new(".");
for exclude in &scope.exclude {
excludes
.add(&format!("!{exclude}"))
.into_diagnostic()
.wrap_err_with(|| format!("bad exclude rule {exclude}"))?;
}
walk.overrides(
excludes
.build()
.into_diagnostic()
.wrap_err("assemble exclude list")?,
);
let stdout = std::io::stdout();
let mut out = BufWriter::new(stdout.lock());
'walk: for entry in walk.build() {
let entry = entry.into_diagnostic()?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(language) = path
.extension()
.and_then(|ext| ext.to_str())
.and_then(Language::from_extension)
else {
continue;
};
let source = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| format!("read {}", path.display()))?;
let mut printed_file_title = false;
let mut pipe_closed = false;
yadr::find_all(&source, language, |line: usize, mut yadr: YAdr<'_>| {
if let Mode::Show { id, .. } = &args.mode {
if !yadr.title.starts_with(id) {
return Ok(true);
}
}
let wrote = match &args.mode {
Mode::Check { .. } => Ok(()),
Mode::Show { .. } => {
yadr.tidy();
writeln!(out, "# {}", yadr.title)
.and_then(|()| writeln!(out, "## from {}:{line}", path.display()))
.and_then(|()| writeln!(out, "{yadr}"))
}
Mode::List { only_files, .. } => {
if *only_files {
return stop_on_broken_pipe(
writeln!(out, "{}", path.display()),
&mut pipe_closed,
)
.map(|_| false);
}
if !printed_file_title {
printed_file_title = true;
if let Err(e) = writeln!(out, "==> {}", path.display()) {
return stop_on_broken_pipe(Err(e), &mut pipe_closed);
}
}
if let Some((last_changed, _)) = yadr.changes.last() {
writeln!(out, " -> {} (last changed: {})", yadr.title, last_changed)
} else {
writeln!(out, " -> {}", yadr.title)
}
}
};
stop_on_broken_pipe(wrote, &mut pipe_closed)
})
.with_context(|| format!("in {}", path.display()))?;
if pipe_closed {
break 'walk;
}
}
match out.flush() {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
other => other.into_diagnostic().wrap_err("flush stdout"),
}
}
fn stop_on_broken_pipe(wrote: io::Result<()>, pipe_closed: &mut bool) -> miette::Result<bool> {
match wrote {
Ok(()) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
*pipe_closed = true;
Ok(false)
}
Err(e) => Err(e).into_diagnostic().wrap_err("write to stdout"),
}
}