#![doc = include_str!("../README.md")]
use std::io::{self, IsTerminal, Read, Write};
use std::path::PathBuf;
use std::process::{Command as ProcessCommand, Stdio};
use bpaf::Bpaf;
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options, version, generate(cli))]
struct Opts {
#[bpaf(external)]
command: Command,
}
#[derive(Debug, Clone, Bpaf)]
enum Command {
#[bpaf(command("man"), hide)]
Man,
Render {
#[bpaf(short, long)]
no_highlight: bool,
#[bpaf(long)]
no_code_background_color: bool,
#[bpaf(long)]
no_pager: bool,
#[bpaf(short, long, argument("COLS"))]
width: Option<usize>,
#[bpaf(positional("FILE"))]
file: Option<PathBuf>,
},
}
fn main() {
let opts = cli().run();
match opts.command {
Command::Man => {
let roff = cli().render_manpage(
"mad",
bpaf::doc::Section::General,
None,
None,
Some("Mad Manual"),
);
print!("{roff}");
}
Command::Render {
no_highlight,
no_code_background_color,
no_pager,
width,
file,
} => {
let input = read_input(file);
let term_size = terminal_size::terminal_size();
let width =
width.or_else(|| term_size.map(|(terminal_size::Width(w), _)| usize::from(w)));
let term_height = term_size.map(|(_, terminal_size::Height(h))| usize::from(h));
let render_opts = markdown_to_ansi::Options {
syntax_highlight: !no_highlight,
width,
code_bg: !no_code_background_color,
};
let output = markdown_to_ansi::render(&input, &render_opts);
print_or_page(&output, no_pager, term_height);
}
}
}
fn read_input(file: Option<PathBuf>) -> String {
if let Some(path) = file {
match std::fs::read_to_string(&path) {
Ok(contents) => contents,
Err(e) => {
eprintln!("mad: {}: {e}", path.display());
std::process::exit(1);
}
}
} else {
if io::stdin().is_terminal() {
eprintln!("mad: reading from stdin (use --help for usage)");
}
let mut buf = String::new();
if let Err(e) = io::stdin().read_to_string(&mut buf) {
eprintln!("mad: failed to read stdin: {e}");
std::process::exit(1);
}
buf
}
}
fn print_or_page(output: &str, no_pager: bool, term_height: Option<usize>) {
if no_pager || !io::stdout().is_terminal() {
print!("{output}");
return;
}
if let Some(height) = term_height {
let line_count = output.lines().count();
if line_count <= height {
print!("{output}");
return;
}
}
if spawn_pager(output).is_err() {
print!("{output}");
}
}
fn spawn_pager(output: &str) -> io::Result<()> {
let pager_env = std::env::var("PAGER").unwrap_or_else(|_| "less".to_string());
let mut parts = pager_env.split_whitespace();
let cmd = parts.next().unwrap_or("less");
let mut args: Vec<&str> = parts.collect();
if cmd.ends_with("less") && !args.iter().any(|a| a.contains('R')) {
args.push("-R");
}
let mut child = ProcessCommand::new(cmd)
.args(&args)
.stdin(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(output.as_bytes());
}
let _ = child.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_parse_no_args() {
let opts = cli().run_inner(&[]).expect("should parse empty args");
match opts.command {
Command::Render {
no_highlight,
no_code_background_color,
no_pager,
width,
file,
} => {
assert!(!no_highlight);
assert!(!no_code_background_color);
assert!(!no_pager);
assert!(width.is_none());
assert!(file.is_none());
}
Command::Man => panic!("expected Render"),
}
}
#[test]
fn cli_parse_file_arg() {
let opts = cli()
.run_inner(&["README.md"])
.expect("should parse file arg");
match opts.command {
Command::Render { file, .. } => {
assert_eq!(file, Some(PathBuf::from("README.md")));
}
Command::Man => panic!("expected Render"),
}
}
#[test]
fn cli_parse_all_flags() {
let opts = cli()
.run_inner(&[
"--no-highlight",
"--no-code-background-color",
"--no-pager",
"--width",
"120",
"test.md",
])
.expect("should parse all flags");
match opts.command {
Command::Render {
no_highlight,
no_code_background_color,
no_pager,
width,
file,
} => {
assert!(no_highlight);
assert!(no_code_background_color);
assert!(no_pager);
assert_eq!(width, Some(120));
assert_eq!(file, Some(PathBuf::from("test.md")));
}
Command::Man => panic!("expected Render"),
}
}
#[test]
fn cli_parse_short_flags() {
let opts = cli()
.run_inner(&["-n", "-w", "80"])
.expect("should parse short flags");
match opts.command {
Command::Render {
no_highlight,
no_code_background_color,
width,
..
} => {
assert!(no_highlight);
assert!(!no_code_background_color);
assert_eq!(width, Some(80));
}
Command::Man => panic!("expected Render"),
}
}
#[test]
fn cli_parse_man_subcommand() {
let opts = cli()
.run_inner(&["man"])
.expect("should parse man subcommand");
assert!(matches!(opts.command, Command::Man));
}
}