Implement rp CLI: rich-print markdown, JSON, code, and more
- Add typer CLI with auto file-type detection by extension - Support markdown (rich.Markdown), JSON (rich.JSON), and 30+ code languages via Pygments syntax highlighting - Features: line numbers toggle, pager support, stdin input, configurable color themes - Project metadata: MIT license, author, classifiers, entry point
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
def hello() -> str:
|
||||
return "Hello from rp!"
|
||||
"""rp — Rich print files beautifully in your terminal."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Yunxiao Xu"
|
||||
|
||||
119
src/rp/cli.py
Normal file
119
src/rp/cli.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""CLI entry point for rp — rich print files beautifully."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from rp import __version__
|
||||
from rp.detect import detect_type
|
||||
from rp.render import RenderOptions, render
|
||||
|
||||
app = typer.Typer(
|
||||
name="rp",
|
||||
help="Rich print files — markdown, JSON, code, and more — beautifully in your terminal.",
|
||||
no_args_is_help=False,
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
_THEMES = [
|
||||
"monokai",
|
||||
"github-dark",
|
||||
"one-dark",
|
||||
"vim",
|
||||
"vs",
|
||||
"nord",
|
||||
"dracula",
|
||||
"solarized-dark",
|
||||
"solarized-light",
|
||||
"default",
|
||||
"friendly",
|
||||
"native",
|
||||
"emacs",
|
||||
]
|
||||
|
||||
|
||||
def version_callback(value: bool) -> None:
|
||||
if value:
|
||||
console = Console()
|
||||
console.print(f"rp {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
file: Annotated[
|
||||
Optional[Path],
|
||||
typer.Argument(help="File to rich-print. Use '-' or omit to read from stdin."),
|
||||
] = None,
|
||||
type: Annotated[
|
||||
Optional[str],
|
||||
typer.Option(
|
||||
"--type", "-t", help="Force file type (e.g. json, python, markdown)."
|
||||
),
|
||||
] = None,
|
||||
theme: Annotated[
|
||||
str,
|
||||
typer.Option("--theme", help="Pygments color theme."),
|
||||
] = "monokai",
|
||||
line_numbers: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--line-numbers/--no-line-numbers",
|
||||
help="Show line numbers. Default: auto.",
|
||||
),
|
||||
] = None,
|
||||
pager: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--pager/--no-pager",
|
||||
help="Use pager for output. Default: auto-detect.",
|
||||
),
|
||||
] = None,
|
||||
version: Annotated[
|
||||
Optional[bool],
|
||||
typer.Option(
|
||||
"--version",
|
||||
"-v",
|
||||
help="Show version.",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Rich print files beautifully in your terminal."""
|
||||
console = Console()
|
||||
|
||||
if file is None or str(file) == "-":
|
||||
content = sys.stdin.read()
|
||||
file_path = None
|
||||
else:
|
||||
if not file.exists():
|
||||
console.print(f"[red]Error:[/red] File not found: {file}")
|
||||
raise typer.Exit(code=1)
|
||||
if file.is_dir():
|
||||
console.print(f"[red]Error:[/red] Is a directory: {file}")
|
||||
raise typer.Exit(code=1)
|
||||
content = file.read_text(encoding="utf-8")
|
||||
file_path = str(file)
|
||||
|
||||
if not content:
|
||||
console.print("[dim]Empty input, nothing to display.[/dim]")
|
||||
raise typer.Exit()
|
||||
|
||||
file_type = detect_type(path=file_path, content=content, explicit_type=type)
|
||||
|
||||
options = RenderOptions(
|
||||
theme=theme,
|
||||
line_numbers=line_numbers,
|
||||
pager=pager,
|
||||
)
|
||||
render(content, file_type, console, options)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
83
src/rp/detect.py
Normal file
83
src/rp/detect.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""File type detection for rp."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
EXTENSION_MAP: dict[str, str] = {
|
||||
".md": "markdown",
|
||||
".json": "json",
|
||||
".py": "python",
|
||||
".pyw": "python",
|
||||
".js": "javascript",
|
||||
".mjs": "javascript",
|
||||
".ts": "typescript",
|
||||
".tsx": "typescript",
|
||||
".jsx": "jsx",
|
||||
".css": "css",
|
||||
".scss": "scss",
|
||||
".html": "html",
|
||||
".htm": "html",
|
||||
".xml": "xml",
|
||||
".yaml": "yaml",
|
||||
".yml": "yaml",
|
||||
".toml": "toml",
|
||||
".sh": "bash",
|
||||
".bash": "bash",
|
||||
".zsh": "bash",
|
||||
".sql": "sql",
|
||||
".rs": "rust",
|
||||
".go": "go",
|
||||
".java": "java",
|
||||
".c": "c",
|
||||
".h": "c",
|
||||
".cpp": "cpp",
|
||||
".hpp": "cpp",
|
||||
".cc": "cpp",
|
||||
".rb": "ruby",
|
||||
".php": "php",
|
||||
".swift": "swift",
|
||||
".kt": "kotlin",
|
||||
".r": "r",
|
||||
".R": "r",
|
||||
".lua": "lua",
|
||||
".dockerfile": "docker",
|
||||
".ini": "ini",
|
||||
".cfg": "ini",
|
||||
".diff": "diff",
|
||||
".patch": "diff",
|
||||
".tex": "latex",
|
||||
}
|
||||
|
||||
_FILENAME_MAP: dict[str, str] = {
|
||||
"Dockerfile": "docker",
|
||||
"Makefile": "make",
|
||||
}
|
||||
|
||||
|
||||
def detect_type(
|
||||
path: str | None = None,
|
||||
content: str | None = None,
|
||||
explicit_type: str | None = None,
|
||||
) -> str:
|
||||
if explicit_type is not None:
|
||||
return explicit_type
|
||||
|
||||
if path is not None:
|
||||
p = Path(path)
|
||||
name = p.name
|
||||
if name in _FILENAME_MAP:
|
||||
return _FILENAME_MAP[name]
|
||||
suffix = p.suffix.lower()
|
||||
if suffix in EXTENSION_MAP:
|
||||
return EXTENSION_MAP[suffix]
|
||||
|
||||
if content is not None:
|
||||
try:
|
||||
json.loads(content[:1000])
|
||||
return "json"
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
return "text"
|
||||
55
src/rp/render.py
Normal file
55
src/rp/render.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Core rendering module for rp."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from rich.console import Console
|
||||
from rich.json import JSON as RichJSON
|
||||
from rich.markdown import Markdown
|
||||
from rich.syntax import Syntax
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderOptions:
|
||||
theme: str = "monokai"
|
||||
line_numbers: bool | None = None
|
||||
pager: bool | None = None
|
||||
|
||||
|
||||
_MARKDOWN_TYPES = {"markdown", "md"}
|
||||
_JSON_TYPES = {"json"}
|
||||
|
||||
|
||||
def render(
|
||||
content: str, file_type: str, console: Console, options: RenderOptions
|
||||
) -> None:
|
||||
line_numbers = options.line_numbers
|
||||
if line_numbers is None:
|
||||
line_numbers = file_type not in _MARKDOWN_TYPES and file_type not in _JSON_TYPES
|
||||
|
||||
use_pager = options.pager
|
||||
if use_pager is None:
|
||||
import sys
|
||||
|
||||
use_pager = sys.stdout.isatty()
|
||||
|
||||
if file_type in _MARKDOWN_TYPES:
|
||||
renderable = Markdown(content)
|
||||
elif file_type in _JSON_TYPES:
|
||||
try:
|
||||
renderable = RichJSON(content)
|
||||
except Exception:
|
||||
renderable = Syntax(
|
||||
content, "json", theme=options.theme, line_numbers=line_numbers
|
||||
)
|
||||
else:
|
||||
renderable = Syntax(
|
||||
content, file_type, theme=options.theme, line_numbers=line_numbers
|
||||
)
|
||||
|
||||
if use_pager:
|
||||
with console.pager(styles=True):
|
||||
console.print(renderable)
|
||||
else:
|
||||
console.print(renderable)
|
||||
Reference in New Issue
Block a user