"""CLI interface for man-card.""" import os from pathlib import Path from typing import Optional import click from rich.console import Console from rich.panel import Panel from rich.text import Text from .cli_utils import parse_commands_from_file from .man_parser import ManPageParser from .card_generator import PDFCardGenerator, PNGCardGenerator from .templates import TemplateLoader from .config import Config console = Console() @click.group() @click.option('--verbose', '-v', is_flag=True, help='Enable verbose output') @click.option('--quiet', '-q', is_flag=True, help='Suppress non-essential output') @click.pass_context def cli(ctx: click.Context, verbose: bool, quiet: bool) -> None: """man-card: Generate beautiful command reference cards from man pages.""" ctx.ensure_object(dict) ctx.obj['verbose'] = verbose ctx.obj['quiet'] = quiet @cli.command() @click.argument('command', nargs=-1, required=False) @click.option('--output', '-o', type=click.Path(), help='Output file path') @click.option('--format', 'output_format', type=click.Choice(['pdf', 'png']), help='Output format') @click.option('--template', '-t', help='Template to use') @click.option('--section', '-s', default='1', help='Man page section (default: 1)') @click.option('--dpi', type=int, help='DPI for PNG output (default: 150)') @click.option('--page-size', type=click.Choice(['a4', 'letter']), help='Page size for PDF') @click.option('--input-file', '-i', type=click.Path(exists=True), help='Input file with commands (one per line)') @click.option('--output-dir', '-d', type=click.Path(), help='Output directory for batch generation') @click.pass_context def generate( ctx: click.Context, command: tuple, output: Optional[str], output_format: Optional[str], template: Optional[str], section: str, dpi: Optional[int], page_size: Optional[str], input_file: Optional[str], output_dir: Optional[str] ) -> None: """Generate a reference card for a command.""" ctx_obj = ctx.obj if ctx.obj is not None else {} verbose = ctx_obj.get('verbose', False) quiet = ctx_obj.get('quiet', False) config = Config.from_dotenv() template = template or config.default_template output_format = output_format or config.default_format dpi = dpi or config.default_dpi page_size = page_size or config.page_size if input_file: commands = parse_commands_from_file(input_file) if not commands: console.print("[red]Error: No commands found in input file[/red]") ctx.exit(1) elif command: commands = list(command) else: console.print("[red]Error: Either COMMAND or --input-file must be provided[/red]") ctx.exit(1) template_loader = TemplateLoader() try: template_obj = template_loader.load(template) except ValueError as e: console.print(f"[red]Error: {e}[/red]") return parser = ManPageParser() pdf_gen = PDFCardGenerator(template_obj) png_gen = PNGCardGenerator(template_obj) for cmd in commands: if not quiet: console.print(f"Processing: [cyan]{cmd}[/cyan]") try: cmd_info = parser.parse(cmd, section) except ValueError as e: console.print(f"[red]Error: {e}[/red]") continue if output_dir: base_name = f"{cmd}_card" elif output: base_name = Path(output).stem Path(output).parent.mkdir(parents=True, exist_ok=True) else: base_name = f"{cmd}_card" try: if output_format == 'pdf': pdf_path = f"{base_name}.pdf" if not output or output_dir else output if output_dir: pdf_path = os.path.join(output_dir, f"{cmd}_card.pdf") pdf_gen.generate(cmd_info, pdf_path, page_size) if not quiet: console.print(f" PDF: [green]{pdf_path}[/green]") else: png_path = f"{base_name}.png" if not output or output_dir else output if output_dir: png_path = os.path.join(output_dir, f"{cmd}_card.png") png_gen.generate(cmd_info, png_path, dpi) if not quiet: console.print(f" PNG: [green]{png_path}[/green]") except Exception as e: console.print(f"[red]Error generating card for {cmd}: {e}[/red]") if verbose: raise @cli.command() def list_templates() -> None: """List available templates.""" template_loader = TemplateLoader() templates = template_loader.list_templates() console.print(Panel( "\n".join(f" [cyan]{t}[/cyan]" for t in templates), title="Available Templates", subtitle=f"Total: {len(templates)} templates" )) @cli.command() @click.argument('command', nargs=-1, required=True) @click.option('--section', '-s', default='1', help='Man page section') @click.pass_context def preview(ctx: click.Context, command: tuple, section: str) -> None: """Preview a command's man page in the terminal.""" parser = ManPageParser() for cmd in command: try: cmd_info = parser.parse(cmd, section) except ValueError as e: console.print(f"[red]Error: {e}[/red]") continue text = Text() text.append(f"{cmd_info.name}\n", style="bold cyan") text.append(f"SYNOPSIS\n{cmd_info.synopsis}\n\n", style="yellow") if cmd_info.description: text.append(f"DESCRIPTION\n{cmd_info.description}\n\n", style="yellow") if cmd_info.options: text.append("OPTIONS\n", style="yellow") for opt in cmd_info.options[:10]: text.append(f" {opt.flag}\n {opt.description}\n") console.print(Panel(text, title=f"Preview: {cmd_info.name}", expand=False)) @cli.command() @click.argument('command', nargs=-1, required=True) @click.option('--section', '-s', default='1', help='Man page section') @click.option('--format', 'output_format', type=click.Choice(['json', 'yaml']), default='json', help='Output format') @click.pass_context def parse(ctx: click.Context, command: tuple, section: str, output_format: str) -> None: """Parse a man page and output structured data.""" import json parser = ManPageParser() for cmd in command: try: cmd_info = parser.parse(cmd, section) except ValueError as e: console.print(f"[red]Error: {e}[/red]") continue if output_format == 'json': data = { "name": cmd_info.name, "synopsis": cmd_info.synopsis, "description": cmd_info.description, "options": [{"flag": o.flag, "description": o.description} for o in cmd_info.options], "examples": cmd_info.examples } console.print(json.dumps(data, indent=2)) else: console.print(f"Name: {cmd_info.name}") console.print(f"Synopsis: {cmd_info.synopsis}") console.print(f"Description: {cmd_info.description}") console.print("Options:") for opt in cmd_info.options: console.print(f" {opt.flag}: {opt.description}") def main(): """Entry point for the CLI.""" cli(obj={}) if __name__ == '__main__': main()