diff --git a/man_card/cli.py b/man_card/cli.py new file mode 100644 index 0000000..8ecda8c --- /dev/null +++ b/man_card/cli.py @@ -0,0 +1,212 @@ +"""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()