Files
man-card/man_card/cli.py
2026-01-31 21:39:45 +00:00

213 lines
7.3 KiB
Python

"""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()