a73x

3a60ac00

Add auto-generated man pages via clap_mangen

a73x   2026-03-21 09:40

Man pages are generated at build time from clap derive definitions,
so they stay in sync with the CLI automatically. Adds make targets:
make man, make install-man, make clean-man.

Resolves: 31543ca5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/.gitignore b/.gitignore
index ea8c4bf..eaeddaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
/man
diff --git a/Cargo.lock b/Cargo.lock
index 537c260..e969d1d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -239,6 +239,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

[[package]]
name = "clap_mangen"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78"
dependencies = [
 "clap",
 "roff",
]

[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -574,6 +584,7 @@ version = "0.1.0"
dependencies = [
 "chrono",
 "clap",
 "clap_mangen",
 "crossterm",
 "git2",
 "ratatui",
@@ -1445,6 +1456,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"

[[package]]
name = "roff"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad"

[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 3f520d0..04364be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,5 +13,9 @@ thiserror = "2"
ratatui = "0.30.0"
crossterm = "0.29.0"

[build-dependencies]
clap = { version = "4", features = ["derive"] }
clap_mangen = "0.2"

[dev-dependencies]
tempfile = "3"
diff --git a/Makefile b/Makefile
index 11fc2dc..b614f8f 100644
--- a/Makefile
+++ b/Makefile
@@ -30,15 +30,31 @@ ci: lint test build

# --- Install ---
install: PROFILE := release
install: build
install: build install-man
	install -Dm755 target/release/$(BIN) $(PREFIX)/bin/$(BIN)

uninstall:
	rm -f $(PREFIX)/bin/$(BIN)

clean:
clean: clean-man
	$(CARGO) clean

# --- Man pages ---
MAN_DIR := man/man1

.PHONY: man install-man clean-man

man:
	MAN_OUT_DIR=$(MAN_DIR) $(CARGO) build
	@echo "Man pages written to $(MAN_DIR)/"

install-man: man
	install -d $(PREFIX)/share/man/man1
	install -m644 $(MAN_DIR)/*.1 $(PREFIX)/share/man/man1/

clean-man:
	rm -rf man/

# --- Dev helpers ---
.PHONY: run dev dashboard

diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..518588b
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,36 @@
use std::env;
use std::fs;
use std::path::PathBuf;

include!("src/cli.rs");

fn main() {
    let out = PathBuf::from(
        env::var("MAN_OUT_DIR").unwrap_or_else(|_| {
            env::var("OUT_DIR").expect("OUT_DIR not set")
        }),
    );

    let cmd = <Cli as clap::CommandFactory>::command();
    generate_manpages(&cmd, &out);
}

fn generate_manpages(cmd: &clap::Command, out: &PathBuf) {
    let man = clap_mangen::Man::new(cmd.clone());
    let name = cmd.get_name().to_string();
    let mut buf = Vec::new();
    man.render(&mut buf).expect("failed to render man page");
    fs::create_dir_all(out).expect("failed to create man output dir");
    fs::write(out.join(format!("{name}.1")), buf)
        .expect("failed to write man page");

    for sub in cmd.get_subcommands() {
        if sub.is_hide_set() {
            continue;
        }
        let sub_name = format!("{}-{}", cmd.get_name(), sub.get_name());
        let sub_name: &'static str = Box::leak(sub_name.into_boxed_str());
        let sub_cmd = sub.clone().name(sub_name);
        generate_manpages(&sub_cmd, out);
    }
}