Initial upload: man-card CLI tool with PDF/PNG generation, templates, and tests
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
212
man_card/cli.py
Normal file
212
man_card/cli.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user