use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use clap::CommandFactory;
use crate::cli::Cli;
const MANUAL: &str = "tsafe Manual";
pub fn render_to_dir(output_dir: &Path) -> io::Result<Vec<PathBuf>> {
fs::create_dir_all(output_dir)?;
remove_stale_generated_pages(output_dir)?;
let root = Cli::command();
let mut written = Vec::new();
render_command(&root, output_dir, "tsafe", &mut written)?;
written.sort();
Ok(written)
}
fn remove_stale_generated_pages(output_dir: &Path) -> io::Result<()> {
for entry in fs::read_dir(output_dir)? {
let entry = entry?;
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if name.starts_with("tsafe") && name.ends_with(".1") {
fs::remove_file(path)?;
}
}
Ok(())
}
fn render_command(
cmd: &clap::Command,
output_dir: &Path,
page_name: &str,
written: &mut Vec<PathBuf>,
) -> io::Result<()> {
let mut rendered = Vec::new();
clap_mangen::Man::new(cmd.clone())
.title(page_name.to_ascii_uppercase())
.section("1")
.manual(MANUAL)
.source(format!("tsafe {}", env!("CARGO_PKG_VERSION")))
.render(&mut rendered)?;
let output_path = output_dir.join(format!("{page_name}.1"));
let text = String::from_utf8(rendered)
.map(|text| {
text.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned());
fs::write(&output_path, text)?;
written.push(output_path);
for subcommand in cmd.get_subcommands() {
let child_name = format!("{page_name}-{}", subcommand.get_name());
render_command(subcommand, output_dir, &child_name, written)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn renders_root_and_nested_manpages() {
let temp = tempdir().unwrap();
let written = render_to_dir(temp.path()).unwrap();
let root = temp.path().join("tsafe.1");
assert!(written.contains(&root));
assert!(
written.iter().any(|path| {
path != &root
&& path.parent() == Some(temp.path())
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("tsafe-"))
}),
"expected at least one nested manpage, got {written:?}"
);
let root_contents = fs::read_to_string(root).unwrap();
assert!(root_contents.contains(".TH TSAFE 1"));
assert!(root_contents.contains("tsafe Manual"));
}
#[test]
fn removes_stale_generated_manpages_before_writing_current_set() {
let temp = tempdir().unwrap();
let stale = temp.path().join("tsafe-stale-command.1");
let unrelated = temp.path().join("README.md");
fs::write(&stale, "stale").unwrap();
fs::write(&unrelated, "keep me").unwrap();
let written = render_to_dir(temp.path()).unwrap();
assert!(
!stale.exists(),
"stale generated manpage should be removed before writing fresh output"
);
assert_eq!(fs::read_to_string(unrelated).unwrap(), "keep me");
assert!(!written.contains(&stale));
}
}