use std::collections::BTreeMap;
use std::env;
use std::fmt::Write as _;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use clap::CommandFactory;
use clap_complete::{Shell, generate_to};
use clap_mangen::Man;
use serde::Deserialize;
#[path = "src/cli.rs"]
mod cli;
use cli::Cli;
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
enum RawArgKind {
Req,
Opt,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawArg {
Short(RawArgKind),
Full {
kind: RawArgKind,
#[serde(default)]
prose: bool,
#[serde(default)]
collapse: bool,
},
}
impl RawArg {
fn render(&self) -> String {
let (kind, prose, collapse) = match self {
RawArg::Short(kind) => (*kind, false, false),
RawArg::Full {
kind,
prose,
collapse,
} => (*kind, *prose, *collapse),
};
let (required, kind) = match kind {
RawArgKind::Req => (true, "ArgKind::Brace"),
RawArgKind::Opt => (false, "ArgKind::Bracket"),
};
format!("arg({required}, {kind}, {prose}, {collapse})")
}
}
fn render_args(args: &[RawArg]) -> String {
let mut out = String::from("&[");
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&a.render());
}
out.push(']');
out
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawCommand {
#[serde(default)]
args: Vec<RawArg>,
#[serde(default)]
sectioning: Option<u8>,
#[serde(default)]
verbatim: bool,
#[serde(default)]
rule: bool,
#[serde(default)]
inline: bool,
}
impl RawCommand {
fn render(&self) -> String {
let sectioning = match self.sectioning {
Some(n) => format!("Some({n}u8)"),
None => "None".to_string(),
};
format!(
"command({}, {}, {}, {}, {})",
render_args(&self.args),
sectioning,
self.verbatim,
self.rule,
self.inline,
)
}
}
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
enum RawOutlineKind {
Float,
Theorem,
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawEnvironment {
#[serde(default)]
args: Vec<RawArg>,
#[serde(default, rename = "verbatimBody")]
verbatim_body: bool,
#[serde(default)]
math: bool,
#[serde(default)]
code: bool,
#[serde(default)]
align: bool,
#[serde(default, rename = "noIndent")]
no_indent: bool,
#[serde(default)]
list: bool,
#[serde(default)]
block: bool,
#[serde(default)]
outline: Option<RawOutlineKind>,
}
impl RawEnvironment {
fn render(&self) -> String {
let outline = match self.outline {
Some(RawOutlineKind::Float) => "Some(OutlineKind::Float)",
Some(RawOutlineKind::Theorem) => "Some(OutlineKind::Theorem)",
None => "None",
};
format!(
"environment({}, {}, {}, {}, {}, {}, {}, {}, {})",
render_args(&self.args),
self.verbatim_body,
self.math,
self.code,
self.align,
self.no_indent,
self.list,
self.block,
outline,
)
}
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawDb {
#[serde(default, rename = "_comment")]
_comment: Option<serde::de::IgnoredAny>,
#[serde(default)]
commands: BTreeMap<String, RawCommand>,
#[serde(default)]
environments: BTreeMap<String, RawEnvironment>,
}
fn generate_cwl_signatures() {
let json = std::fs::read_to_string("data/cwl_signatures.json")
.expect("data/cwl_signatures.json must exist (run `task cwl:sync`)");
let db: RawDb = serde_json::from_str(&json).expect("data/cwl_signatures.json must be valid");
let mut commands = phf_codegen::Map::new();
for (name, sig) in &db.commands {
commands.entry(name.as_str(), sig.render());
}
let mut environments = phf_codegen::Map::new();
for (name, sig) in &db.environments {
environments.entry(name.as_str(), sig.render());
}
let mut out = String::new();
writeln!(
out,
"// @generated by build.rs from data/cwl_signatures.json — do not edit."
)
.unwrap();
writeln!(
out,
"static CWL_COMMANDS: CwlSigMap<CommandSig> = {};",
commands.build()
)
.unwrap();
writeln!(
out,
"static CWL_ENVIRONMENTS: CwlSigMap<EnvironmentSig> = {};",
environments.build()
)
.unwrap();
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("cwl_signatures.rs");
let mut file = BufWriter::new(File::create(&path).unwrap());
file.write_all(out.as_bytes()).unwrap();
}
fn generate_completions(outdir: &std::ffi::OsString) -> std::io::Result<()> {
let mut cmd = Cli::command();
for shell in [
Shell::Bash,
Shell::Fish,
Shell::Zsh,
Shell::PowerShell,
Shell::Elvish,
] {
generate_to(shell, &mut cmd, "badness", outdir)?;
}
let completions_dir = PathBuf::from("target/completions");
std::fs::create_dir_all(&completions_dir)?;
let outdir_path = PathBuf::from(outdir);
for (src, dst) in [
("badness.bash", "badness.bash"),
("badness.fish", "badness.fish"),
("_badness", "_badness"),
] {
let from = outdir_path.join(src);
if from.exists() {
std::fs::copy(&from, completions_dir.join(dst))?;
}
}
Ok(())
}
fn format_see_also(refs: &[String]) -> String {
let formatted: Vec<String> = refs.iter().map(|r| format!("\\fB{}\\fR(1)", r)).collect();
format!(".SH \"SEE ALSO\"\n{}\n", formatted.join(", "))
}
fn generate_man_pages() -> std::io::Result<()> {
let out_dir = PathBuf::from("target/man");
std::fs::create_dir_all(&out_dir)?;
let cmd = Cli::command();
let subcommand_names: Vec<String> = cmd
.get_subcommands()
.filter(|s| s.get_name() != "help")
.map(|s| format!("badness-{}", s.get_name()))
.collect();
let man = Man::new(cmd.clone());
let mut buffer = Vec::new();
man.render(&mut buffer)?;
let main_content =
String::from_utf8_lossy(&buffer).into_owned() + &format_see_also(&subcommand_names);
std::fs::write(out_dir.join("badness.1"), main_content.as_bytes())?;
for subcommand in cmd.get_subcommands() {
let subcommand_name = subcommand.get_name();
if subcommand_name == "help" {
continue;
}
let name = format!("badness-{}", subcommand_name);
let man = Man::new(subcommand.clone().version(env!("CARGO_PKG_VERSION"))).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!("{} \\-", name),
)
.replace(
&format!("\\fB{}\\fR", subcommand_name),
&format!("\\fBbadness {}\\fR", subcommand_name),
)
.replace(
&format!("{}\\-", subcommand_name),
&format!("badness\\-{}\\-", subcommand_name),
);
let mut see_also_refs: Vec<String> = vec!["badness".to_string()];
see_also_refs.extend(subcommand_names.iter().filter(|n| *n != &name).cloned());
let with_see_also = fixed_content + &format_see_also(&see_also_refs);
std::fs::write(
out_dir.join(format!("{}.1", name)),
with_see_also.as_bytes(),
)?;
}
Ok(())
}
fn generate_cli_markdown() -> std::io::Result<()> {
let is_packaging = 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 docs_dir = PathBuf::from("docs/src/reference");
if !docs_dir.exists() {
return Ok(());
}
let cmd = Cli::command();
let opts = clap_markdown::MarkdownOptions::default()
.show_footer(false)
.show_table_of_contents(false);
let markdown = clap_markdown::help_markdown_command_custom(&cmd, &opts);
std::fs::write(docs_dir.join("cli.md"), &markdown)?;
Ok(())
}
fn main() -> std::io::Result<()> {
println!("cargo:rerun-if-changed=data/cwl_signatures.json");
println!("cargo:rerun-if-changed=src/cli.rs");
println!("cargo:rerun-if-changed=build.rs");
generate_cwl_signatures();
if let Some(outdir) = env::var_os("OUT_DIR") {
generate_completions(&outdir)?;
}
generate_man_pages()?;
generate_cli_markdown()?;
Ok(())
}