diff --git a/README.md b/README.md new file mode 100644 index 0000000..d12b57c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# rp + +Rich print files — markdown, JSON, code, and more — beautifully in your terminal. diff --git a/pyproject.toml b/pyproject.toml index a3580cd..98f5a7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/rp/__init__.py b/src/rp/__init__.py index cf2ac46..a59d46a 100644 --- a/src/rp/__init__.py +++ b/src/rp/__init__.py @@ -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" diff --git a/src/rp/cli.py b/src/rp/cli.py new file mode 100644 index 0000000..440c2a0 --- /dev/null +++ b/src/rp/cli.py @@ -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() diff --git a/src/rp/detect.py b/src/rp/detect.py new file mode 100644 index 0000000..d199b5d --- /dev/null +++ b/src/rp/detect.py @@ -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" diff --git a/src/rp/render.py b/src/rp/render.py new file mode 100644 index 0000000..158e925 --- /dev/null +++ b/src/rp/render.py @@ -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) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..991e348 --- /dev/null +++ b/uv.lock @@ -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" }, +]