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:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# rp
|
||||
|
||||
Rich print files — markdown, JSON, code, and more — beautifully in your terminal.
|
||||
@@ -1,13 +1,36 @@
|
||||
[project]
|
||||
name = "rp"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
description = "Rich print files — markdown, JSON, code, and more — beautifully in your terminal"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "Yunxiao Xu", email = "xuyunxiao2001@gmail.com" }
|
||||
]
|
||||
keywords = ["cli", "rich", "syntax-highlighting", "markdown", "terminal"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Utilities",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"rich>=13.0",
|
||||
"typer>=0.12",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
rp = "rp.cli:app"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yunxiaoxu/rich-viewer"
|
||||
Repository = "https://github.com/yunxiaoxu/rich-viewer"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.11.2,<0.12.0"]
|
||||
|
||||
@@ -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)
|
||||
115
uv.lock
generated
Normal file
115
uv.lock
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rp"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "rich", specifier = ">=13.0" },
|
||||
{ name = "typer", specifier = ">=0.12" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.24.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user