use clap::CommandFactory;
use clap_complete::{Shell, generate_to};
use clap_mangen::Man;
use std::env;
use std::fs;
use std::io::Result;
use std::path::PathBuf;
#[path = "src/cli.rs"]
mod cli;
use cli::Cli;
fn generate_completions(outdir: &std::ffi::OsString) -> Result<()> {
let mut cmd = Cli::command();
for shell in [
Shell::Bash,
Shell::Fish,
Shell::Zsh,
Shell::PowerShell,
Shell::Elvish,
] {
generate_to(shell, &mut cmd, "panache", outdir)?;
}
let completions_dir = PathBuf::from("target/completions");
fs::create_dir_all(&completions_dir)?;
let outdir_path = PathBuf::from(outdir);
let bash_src = outdir_path.join("panache.bash");
let fish_src = outdir_path.join("panache.fish");
let zsh_src = outdir_path.join("_panache");
if bash_src.exists() {
fs::copy(&bash_src, completions_dir.join("panache.bash"))?;
}
if fish_src.exists() {
fs::copy(&fish_src, completions_dir.join("panache.fish"))?;
}
if zsh_src.exists() {
fs::copy(&zsh_src, completions_dir.join("_panache"))?;
}
Ok(())
}
fn generate_cli_markdown() -> Result<()> {
let is_packaging = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.contains("/target/package/")))
.unwrap_or(false);
if is_packaging {
return Ok(());
}
let cmd = Cli::command();
let docs_dir = PathBuf::from("docs/reference");
if !docs_dir.exists() {
return Ok(());
}
let opts = clap_markdown::MarkdownOptions::default()
.show_footer(false)
.show_table_of_contents(false);
let markdown = clap_markdown::help_markdown_command_custom(&cmd, &opts);
let mut document = String::new();
document.push_str("---\n");
document.push_str("title: CLI Reference\n");
document.push_str("description: >-\n Comprehensive reference for the Panache CLI, including all commands, options, and usage examples.\n");
document.push_str("---\n\n");
document.push_str(&markdown);
let output_path = docs_dir.join("cli.qmd");
fs::write(&output_path, &document)?;
println!("Generated CLI markdown: {:?}", output_path);
Ok(())
}
#[derive(Debug)]
struct FormatterPresetDocRow {
preset_name: String,
description: String,
homepage: String,
supported_languages: Vec<String>,
cmd: String,
args: Vec<String>,
mode: &'static str,
}
#[derive(Debug)]
struct LinterDocRow {
name: String,
description: String,
homepage: String,
supported_languages: Vec<String>,
cmd: String,
args: Vec<String>,
}
fn extract_quoted_strings(src: &str) -> Vec<String> {
let mut out = Vec::new();
let mut rest = src;
while let Some(start) = rest.find('"') {
let after_start = &rest[start + 1..];
if let Some(end) = after_start.find('"') {
out.push(after_start[..end].to_string());
rest = &after_start[end + 1..];
} else {
break;
}
}
out
}
fn extract_field_string(block: &str, field: &str) -> Option<String> {
let marker = format!("{field}:");
let pos = block.find(&marker)?;
let after = &block[pos + marker.len()..];
let first = after.find('"')?;
let after_first = &after[first + 1..];
let end = after_first.find('"')?;
Some(after_first[..end].to_string())
}
fn extract_field_list(block: &str, field: &str) -> Option<Vec<String>> {
let marker_slice = format!("{field}: &[");
if let Some(start) = block.find(&marker_slice) {
let after = &block[start + marker_slice.len()..];
let end = after.find("],")?;
return Some(extract_quoted_strings(&after[..end]));
}
let marker_vec = format!("{field}: vec![");
let start = block.find(&marker_vec)?;
let after = &block[start + marker_vec.len()..];
let end = after.find("],")?;
Some(extract_quoted_strings(&after[..end]))
}
fn format_languages_for_docs(languages: &[String]) -> String {
let mut unique: Vec<String> = Vec::new();
for language in languages {
let language = language.trim();
if language.is_empty() {
continue;
}
if !unique.iter().any(|existing| existing == language) {
unique.push(language.to_string());
}
}
unique
.into_iter()
.map(|language| format!("`{}`", language))
.collect::<Vec<_>>()
.join(", ")
}
fn format_args(args: &[String]) -> String {
if args.is_empty() {
"[]".to_string()
} else {
format!("[{}]", args.join(", "))
}
}
fn generate_external_formatter_table() -> Result<()> {
let is_packaging = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.contains("/target/package/")))
.unwrap_or(false);
if is_packaging {
return Ok(());
}
let presets_path = PathBuf::from("src/config/formatter_presets.rs");
let docs_dir = PathBuf::from("docs/reference");
if !presets_path.exists() || !docs_dir.exists() {
return Ok(());
}
let source = fs::read_to_string(&presets_path)?;
let mut rows: Vec<FormatterPresetDocRow> = Vec::new();
for chunk in source.split("FormatterPresetMetadata {").skip(1) {
let Some(block_end) = chunk.find("\n },") else {
continue;
};
let block = &chunk[..block_end];
let Some(name) = extract_field_string(block, "name") else {
continue;
};
let Some(cmd) = extract_field_string(block, "cmd") else {
continue;
};
let Some(description) = extract_field_string(block, "description") else {
continue;
};
let Some(homepage) = extract_field_string(block, "url") else {
continue;
};
let Some(args) = extract_field_list(block, "args") else {
continue;
};
let mode = if block.contains("stdin: false") {
"File-based"
} else {
"Stdin"
};
let Some(supported_languages) = extract_field_list(block, "supported_languages") else {
continue;
};
rows.push(FormatterPresetDocRow {
preset_name: name,
description,
homepage,
supported_languages,
cmd,
args,
mode,
});
}
rows.sort_by(|a, b| a.preset_name.cmp(&b.preset_name));
let mut out = String::new();
out.push_str("<!-- AUTO-GENERATED by build.rs -->\n");
for row in rows {
out.push_str(&format!("## `{}`\n\n", row.preset_name));
out.push_str(&format!("{}\n\n", row.description));
out.push_str("Homepage\n");
out.push_str(&format!(": <{}>\n\n", row.homepage));
out.push_str("Supported Languages\n");
out.push_str(&format!(
": {}\n\n",
format_languages_for_docs(&row.supported_languages)
));
out.push_str("Command\n");
out.push_str(&format!(": `{}`\n\n", row.cmd));
out.push_str("`args`\n");
out.push_str(&format!(": `{}`\n\n", format_args(&row.args)));
out.push_str("Type\n");
out.push_str(&format!(": {}\n\n", row.mode));
}
let output_path = docs_dir.join("_formatter-presets-details.qmd");
fs::write(&output_path, out)?;
println!("Generated external formatter presets: {:?}", output_path);
Ok(())
}
fn generate_external_linter_table() -> Result<()> {
let is_packaging = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(|s| s.contains("/target/package/")))
.unwrap_or(false);
if is_packaging {
return Ok(());
}
let linters_path = PathBuf::from("src/linter/external_linters.rs");
let docs_dir = PathBuf::from("docs/reference");
if !linters_path.exists() || !docs_dir.exists() {
return Ok(());
}
let source = fs::read_to_string(&linters_path)?;
let mut rows: Vec<LinterDocRow> = Vec::new();
for chunk in source.split("LinterInfo {").skip(1) {
let Some(block_end) = chunk.find("\n );") else {
continue;
};
let block = &chunk[..block_end];
let Some(name) = extract_field_string(block, "name") else {
continue;
};
let Some(description) = extract_field_string(block, "description") else {
continue;
};
let Some(homepage) = extract_field_string(block, "url") else {
continue;
};
let Some(cmd) = extract_field_string(block, "command") else {
continue;
};
let Some(args) = extract_field_list(block, "args") else {
continue;
};
let Some(supported_languages) = extract_field_list(block, "supported_languages") else {
continue;
};
rows.push(LinterDocRow {
name,
description,
homepage,
supported_languages,
cmd,
args,
});
}
rows.sort_by(|a, b| a.name.cmp(&b.name));
let mut out = String::new();
out.push_str("<!-- AUTO-GENERATED by build.rs -->\n");
for row in rows {
out.push_str(&format!("## `{}`\n\n", row.name));
out.push_str(&format!("{}\n\n", row.description));
out.push_str("Homepage\n");
out.push_str(&format!(": <{}>\n\n", row.homepage));
out.push_str("Supported Languages\n");
out.push_str(&format!(
": {}\n\n",
format_languages_for_docs(&row.supported_languages)
));
out.push_str("Command\n");
out.push_str(&format!(": `{}`\n\n", row.cmd));
out.push_str("`args`\n");
out.push_str(&format!(": `{}`\n\n", format_args(&row.args)));
}
let output_path = docs_dir.join("_linter-presets-details.qmd");
fs::write(&output_path, out)?;
println!("Generated external linter presets: {:?}", output_path);
Ok(())
}
fn generate_man_pages() -> Result<()> {
let out_dir = PathBuf::from("target/man");
fs::create_dir_all(&out_dir)?;
let cmd = Cli::command();
let man = Man::new(cmd.clone());
let mut buffer = Vec::new();
man.render(&mut buffer)?;
fs::write(out_dir.join("panache.1"), buffer)?;
for subcommand in cmd.get_subcommands() {
let subcommand_name = subcommand.get_name();
if subcommand_name == "help" {
continue; }
let name = format!("panache-{}", subcommand_name);
let man = Man::new(subcommand.clone()).title(&name);
let mut buffer = Vec::new();
man.render(&mut buffer)?;
let content = String::from_utf8_lossy(&buffer);
let fixed_content = content.replace(
&format!("{}\\-", subcommand_name),
&format!("panache\\-{}\\-", subcommand_name),
);
fs::write(
out_dir.join(format!("{}.1", name)),
fixed_content.as_bytes(),
)?;
for nested in subcommand.get_subcommands() {
let nested_name = nested.get_name();
if nested_name == "help" {
continue;
}
let full_name = format!("panache-{}-{}", subcommand_name, nested_name);
let man = Man::new(nested.clone()).title(&full_name);
let mut buffer = Vec::new();
man.render(&mut buffer)?;
let content = String::from_utf8_lossy(&buffer);
let fixed_content = content
.replace(
&format!("{} \\-", nested_name),
&format!("{} \\-", full_name),
)
.replace(
&format!("\\fB{}\\fR", nested_name),
&format!("\\fBpanache {} {}\\fR", subcommand_name, nested_name),
);
fs::write(
out_dir.join(format!("{}.1", full_name)),
fixed_content.as_bytes(),
)?;
}
}
Ok(())
}
fn main() -> Result<()> {
if let Some(outdir) = env::var_os("OUT_DIR") {
generate_completions(&outdir)?;
}
generate_man_pages()?;
generate_cli_markdown()?;
generate_external_formatter_table()?;
generate_external_linter_table()?;
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=src/config/formatter_presets.rs");
println!("cargo:rerun-if-changed=src/linter/external_linters.rs");
println!("cargo:rerun-if-changed=build.rs");
Ok(())
}