├── .flake8 ├── .github └── workflows │ ├── build-docs.yml │ ├── deploy-pypi.yml │ ├── docker.yml │ ├── pre-commit.yml │ └── update-tag.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── docs ├── cat.txt ├── config │ ├── cleaning.md │ ├── colours.md │ ├── command_setup.md │ ├── ignoring_changes.md │ ├── overview.md │ ├── time_limits.md │ ├── tweaks.md │ └── width.md ├── img │ ├── before_after_command.svg │ ├── cat.png │ ├── cat.svg │ ├── example-json-snippet.svg │ ├── example-python-snippet.svg │ ├── fake_command.svg │ ├── git-pull-rebase.svg │ ├── git-push-error.svg │ ├── git-push-success.svg │ ├── head-tail.svg │ ├── hide-command-default.svg │ ├── hide-command.svg │ ├── rich-codex-cli-envs.svg │ ├── rich-codex-cli-help.svg │ ├── rich-codex-help-min-pct.svg │ ├── rich-codex-help.svg │ ├── rich-codex-snippet-title.svg │ ├── rich-codex-snippet.svg │ ├── snippet-syntax.svg │ ├── snippet-theme-fruity.svg │ ├── snippet-theme-monokai.svg │ ├── snippet-theme-sas.svg │ ├── taste-the-rainbow.svg │ ├── theme-default_terminal_theme.svg │ ├── theme-dimmed_monokai.svg │ ├── theme-monokai.svg │ ├── theme-night_owlish.svg │ ├── theme-svg_export_theme.svg │ ├── title-command-default.svg │ ├── title-command.svg │ └── trim-after_truncated-text.svg ├── index.md ├── inputs │ ├── config_file.md │ ├── direct_inputs.md │ └── markdown.md ├── safety.md ├── troubleshooting.md └── usage │ ├── cli.md │ ├── docker_image.md │ └── github_action.md ├── examples └── action_command │ └── cowsay-colours.pdf ├── mkdocs.yml ├── pyproject.toml ├── src └── rich_codex │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── codex_search.py │ ├── config-schema.yml │ ├── rich_img.py │ └── utils.py └── uv.lock /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Default ignores that we can extend 3 | ignore=D100,D102,D205,E203,E231,E731,W504,I001,W503 4 | max-line-length=120 5 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build images / docs 2 | on: [push] 3 | 4 | # Cancel if a newer run is started 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | rich_codex: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.x 20 | 21 | - uses: actions/cache@v4 22 | with: 23 | key: ${{ github.ref }} 24 | path: .cache 25 | 26 | - name: Install requirements for docs 27 | run: pip install cowsay lolcat mkdocs-material pillow cairosvg rich-cli 28 | 29 | - name: Generate terminal images with rich-codex 30 | uses: ./ # Would normally be ewels/rich-codex@v1 31 | with: 32 | commit_changes: "true" 33 | clean_img_paths: docs/img/*.svg 34 | 35 | - name: Build and deploy docs 36 | run: mkdocs gh-deploy --force 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish rich-codex to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build-n-publish: 8 | runs-on: ubuntu-latest 9 | environment: 10 | name: pypi 11 | url: https://pypi.org/p/rich-codex 12 | permissions: 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | name: Check out source-code repository 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.12" 23 | 24 | - name: Install python dependencies 25 | run: python -m pip install --upgrade pip setuptools wheel build 26 | 27 | - name: Build the distribution 28 | run: python -m build 29 | 30 | - name: Publish to PyPI 31 | if: github.repository == 'ewels/rich-codex' 32 | uses: pypa/gh-action-pypi-publish@release/v1 33 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # Based on GitHub docs: 2 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images 3 | 4 | name: Publish Docker image 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | tags: 10 | - "v*" 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: ghcr.io/ewels/rich-codex 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@v6 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | pre-commit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-python@v5 12 | - uses: pre-commit/action@v3.0.1 13 | -------------------------------------------------------------------------------- /.github/workflows/update-tag.yml: -------------------------------------------------------------------------------- 1 | name: Update major version tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | update-major-ver: 10 | name: Update major version tag 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: nowactions/update-majorver@v1 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This file contains the [pre-commit](https://pre-commit.com/) configuration of this repository. 2 | # More on which specific pre-commit hooks we use can be found in README.md. 3 | --- 4 | minimum_pre_commit_version: "2.9.2" 5 | repos: 6 | - repo: meta 7 | hooks: 8 | - id: identity 9 | - id: check-hooks-apply 10 | - repo: https://github.com/pre-commit/mirrors-prettier 11 | rev: "v2.7.1" 12 | hooks: 13 | - id: prettier 14 | - repo: local 15 | hooks: 16 | - id: isort 17 | name: iSort - Sorts imports. 18 | description: Sorts your import for you. 19 | entry: isort 20 | language: python 21 | types: [python] 22 | require_serial: true 23 | additional_dependencies: 24 | - isort 25 | - id: black 26 | name: Black - Auto-formatter. 27 | description: Black is the uncompromising Python code formatter. Writing to files. 28 | entry: black 29 | language: python 30 | types: [python] 31 | require_serial: true 32 | additional_dependencies: 33 | - black 34 | - id: flake8 35 | name: Flake8 - Enforce code style and doc. 36 | description: A command-line utility for enforcing style consistency across Python projects. 37 | entry: flake8 38 | args: ["--config=.flake8"] 39 | language: python 40 | types: [python] 41 | exclude: ^examples/ 42 | require_serial: true 43 | additional_dependencies: 44 | - flake8 45 | - flake8-docstrings 46 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | examples/markdown_comments 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewels/rich-codex/80de9de011c994f32274bb4cffee140567621d8e/.prettierrc.yaml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog: rich-codex 2 | 3 | ## Version 1.2.11 (2025-04-22) 4 | 5 | - 🐛 Fix validation error ([#55](https://github.com/ewels/rich-codex/pull/55)) 6 | - ✨ In Github Action, added uv support (`use_uv`), skip Python setup support (`skip_python_setup`), and specific Python version support (`python_verison`). 7 | 8 | ## Version 1.2.10 (2025-03-14) 9 | 10 | - 🐛 Fix missing required argument ([#53](https://github.com/ewels/rich-codex/pull/53)) 11 | 12 | ## Version 1.2.9 (2025-03-12) 13 | 14 | - ✨ Add `working_dir` as new input to the GitHub Action, by @dwreeves in https://github.com/ewels/rich-codex/pull/47 15 | - ♻️ Update all GitHub actions used, by @ewels in https://github.com/ewels/rich-codex/pull/51 16 | 17 | ## Version 1.2.8 (2025-02-20) 18 | 19 | - ✨ Update upload-artifact action to v4 ([#49](https://github.com/ewels/rich-codex/pull/49)) 20 | 21 | ## Version 1.2.7 (2024-01-17) 22 | 23 | - 🐛 Remove `rich-cli` as a dependency ([#45](https://github.com/ewels/rich-codex/issues/45)) 24 | 25 | ## Version 1.2.6 (2022-10-03) 26 | 27 | - 🐛 Handle `OSError` when creating directories with a log message instead of crashing 28 | 29 | ## Version 1.2.5 (2022-08-25) 30 | 31 | - 🐛 Tweak output whitespace, fix use of `Path.absolute()` ([#39](https://github.com/ewels/rich-codex/pull/39)) 32 | - 🐛 Fix parsing of multiple config files ([#37](https://github.com/ewels/rich-codex/issues/37)) 33 | - 🐛 Fixed `KeyError` for top-level config options ([#35](https://github.com/ewels/rich-codex/issues/35)) 34 | 35 | ## Version 1.2.4 (2022-08-19) 36 | 37 | - ✨ Debug log `before_command` and `after_command` so that you can see return code, stderr, stdout 38 | 39 | ## Version 1.2.3 (2022-08-18) 40 | 41 | - ✨ Maintain order of commands in markdown, add alphabetical sort of the files to search 42 | - 🐛 Fix typo ([#30](https://github.com/ewels/rich-codex/pull/30)) 43 | - 🐛 Fix GitPod edit button ([#29](https://github.com/ewels/rich-codex/pull/29)) 44 | - 🐛 Set missing `hide_command` option in example ([#31](https://github.com/ewels/rich-codex/pull/31)) 45 | 46 | ## Version 1.2.2 (2022-08-15) 47 | 48 | - ✨ Log _which_ files have uncommitted changes in git ([#25](https://github.com/ewels/rich-codex/issues/25)) 49 | - 🐛 Close temp files before deleting (bugfix for Windows) ([#27](https://github.com/ewels/rich-codex/issues/27)) 50 | 51 | ## Version 1.2.1 (2022-08-14) 52 | 53 | - ✨ Drop minimum Python version to 3.7 54 | - 🐛 Handle logging error with relative paths ([#26](https://github.com/ewels/rich-codex/issues/26)) 55 | 56 | ## Version 1.2.0 (2022-08-01) 57 | 58 | - ✨ Log a warning if duplicate image paths are found ([#20](https://github.com/ewels/rich-codex/issues/20)) 59 | - 🐛 Fix `UnboundLocalError` if not cleaning an image path ([#24](https://github.com/ewels/rich-codex/issues/24)) 60 | 61 | ## Version 1.1.0 (2022-07-21) 62 | 63 | - ✨ Added CLI flags `--created-files` and `--deleted-files` to create a file with affected file paths 64 | - ✨ GitHub Action: only `git add` / `git rm` files that rich-codex itself created or deleted ([#21](https://github.com/ewels/rich-codex/issues/21)) 65 | 66 | ## Version 1.0.2 (2022-07-08) 67 | 68 | - 🐛 Don't use cache in action `actions/setup-python` step 69 | - 🐛 Bump minimum Python version to 3.9 (may try to drop this in the future) ([#19](https://github.com/ewels/rich-codex/issues/19)) 70 | - 🐳 Build + tag versioned labels of the Docker image on release 71 | - 📖 Improvements to docs 72 | 73 | ## Version 1.0.1 (2022-07-07) 74 | 75 | Patch release to add in a missing `pyyaml` dependency. 76 | 77 | ## Version 1.0.0 (2022-07-07) 78 | 79 | First public release of rich-codex. 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # 3 | # NOTE: This image does not contain your custom software! 4 | # 5 | # Screenshots generated from commands will likely not work. 6 | # Create your own Docker image based on this one or use this 7 | # purely for code snippets. 8 | # 9 | ################################################################## 10 | 11 | FROM python:3.10-alpine 12 | 13 | # Install Cairo for SVG -> PNG / PDF conversion 14 | # From: https://phauer.com/2018/install-cairo-cairosvg-alpine-docker/ 15 | RUN apk add --no-cache \ 16 | git build-base cairo-dev cairo cairo-tools \ 17 | # pillow dependencies 18 | jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev 19 | 20 | # Install requirements 21 | COPY . . 22 | RUN pip install . 23 | 24 | # Prepare GitHub Action 25 | ENTRYPOINT ["rich-codex"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Philip Ewels 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rich-codex ⚡️📖⚡️ 2 | 3 | A GitHub Action / command-line tool which generates screengrab images of a terminal window, containing _command outputs_ or _code snippets_. 4 | 5 | ## 📚 Documentation: https://ewels.github.io/rich-codex/ 📚 6 | 7 | [![PyPI Version](https://img.shields.io/pypi/v/rich-codex.svg?style=flat-square)](https://pypi.python.org/pypi/rich-codex/) 8 | 9 | ## Introduction 10 | 11 | Having code examples in your documentation is a fantastic way to help users understand what to expect from your tool. 12 | 13 | Using terminal screenshots is a good way to do this because: 14 | 15 | - 🌈 Coloured terminal output is supported 16 | - ↔️ You can fit in long lines without scrolling or cropping (images are auto-resized) 17 | - 😎 They look cool 18 | 19 | However, manually generating these screenshots is a pain 👎🏻 Remembering to update them every time you make a minor change means that they can easily get out of date. 20 | 21 | _**Rich-codex**_ automates this process for you. It searches markdown code for images with shell commands or code snippets. It runs these commands and saves a terminal screen-grab at the embedded path. 22 | 23 | Typical use cases: 24 | 25 | - 📷 Example CLI tool outputs that _automatically stay in sync with your package_ 26 | - ♻️ Syntax-highlighted code snippets that are always up to date with your `examples/` 27 | - 🤩 Fast and simple images for your docs with minimal setup 28 | 29 | ## Quickstart 30 | 31 | 1. 📖 Write some markdown docs, use an image tag with a backtick command inside: 32 | 33 | ```markdown 34 | ![`cat docs/cat.txt | lolcat -S 1`](docs/img/cat.png) 35 | ``` 36 | 2. 🤖 Add a GitHub Action to automatically run the command, generate the image and commit to the repo: 37 | 38 | ```yaml 39 | on: [push] 40 | jobs: 41 | rich_codex: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: Install your custom tools 47 | run: pip install lolcat 48 | 49 | - name: Generate terminal images with rich-codex 50 | uses: ewels/rich-codex@v1 51 | with: 52 | commit_changes: "true" 53 | ``` 54 | 55 | 3. 🌈 Enjoy reading your documentation ![My cat rainbow](https://raw.githubusercontent.com/ewels/rich-codex/main/docs/img/cat.png) 56 | 57 | ## How it works 58 | 59 | Rich-codex is a command-line tool that you can run [via a GitHub action](https://ewels.github.io/rich-codex/installation/github_action/) or as a [command line tool](https://ewels.github.io/rich-codex/installation/cli/). It works with any markdown (including GitHub READMEs). 60 | 61 | It collects either commands or code snippets, together with output filenames and configuration options. Commands are run in a subprocess and the standard output & standard error collected. These are then rendered as an image using [Textualize/rich](https://github.com/textualize/rich). 62 | 63 | > Rich-codex creates the images that your markdown docs expect. It doesn't require a HTML build-step and doesn't make any changes to your markdown or its output. As such, it's compatible with _**any documentation engine**_, including rendering markdown on [github.com](https://github.com). 64 | 65 | Rich-codex needs **inputs** (commands / snippets) and **output filenames** to work. These can be configured in four different ways: 66 | 67 | - 🖼 [Markdown images](https://ewels.github.io/rich-codex/inputs/markdown/) 68 | - Search markdown files for image tags with command alt text. eg: `` ![`rich-codex --help`](docs/img/rich-codex-help.svg) `` 69 | - 💬 [Markdown comments](https://ewels.github.io/rich-codex/inputs/markdown/#code-snippets) 70 | - Search markdown files for special HTML comments. 71 | - ➡️ [Command-line / action inputs](https://ewels.github.io/rich-codex/inputs/direct_inputs/) 72 | - Specify a command or snippet using the action `with` inputs. 73 | - ⚙️ [Config files](https://ewels.github.io/rich-codex/inputs/config_file/) 74 | - Use one or more YAML config files for multiple images and more complex customisation. 75 | 76 | Images can be generated as SVG, PNG or PDF (detected by filename extension). 77 | 78 | > **Keep reading!** 👉 https://ewels.github.io/rich-codex/ 79 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: rich-codex 2 | author: Phil Ewels 3 | 4 | description: Create rich code images for your docs 5 | 6 | branding: 7 | icon: "terminal" 8 | color: "blue" 9 | 10 | inputs: 11 | search_include: 12 | description: Glob patterns to files in which to search for rich-codex comments 13 | required: false 14 | search_exclude: 15 | description: Glob patterns to exclude from search for rich-codex comments 16 | required: false 17 | no_search: 18 | description: Set to 'true' to disable searching for rich-codex comments 19 | required: false 20 | command: 21 | description: Specify a command to run to capture output 22 | required: false 23 | timeout: 24 | description: Maximum run time for command (seconds) 25 | required: false 26 | working_dir: 27 | description: Working directory to run command in (relative to root of project) 28 | required: false 29 | before_command: 30 | description: Setup commands to run before running main output command 31 | required: false 32 | after_command: 33 | description: Setup commands to run after running main output command 34 | required: false 35 | snippet: 36 | description: Literal code snippet to render 37 | required: false 38 | snippet_syntax: 39 | description: Language to use for snippet sytax highlighting 40 | required: false 41 | img_paths: 42 | description: Path to image filenames if using 'command' or 'snippet' 43 | required: false 44 | clean_img_paths: 45 | description: Remove any matching files that are not generated 46 | required: false 47 | rc_configs: 48 | description: Paths to YAML config files 49 | required: false 50 | fake_command: 51 | description: Pretend command to show in the screenshot prompt 52 | required: false 53 | hide_command: 54 | description: Hide the terminal prompt with the command at the top of the output 55 | required: false 56 | title_command: 57 | description: Use the command as the terminal title if not set explicitly 58 | required: false 59 | head: 60 | description: Show only the first N lines of output 61 | required: false 62 | tail: 63 | description: Show only the last N lines of output 64 | required: false 65 | trim_after: 66 | description: Don't print any more lines after this string is found 67 | required: false 68 | truncated_text: 69 | description: Text to show when '--head' or '--tail' truncate content 70 | required: false 71 | skip_git_checks: 72 | description: Skip safety checks for git repos 73 | required: false 74 | min_pct_diff: 75 | description: Minimum file percentage change required to update image 76 | required: false 77 | skip_change_regex: 78 | description: Skip image update if file changes match regex 79 | required: false 80 | terminal_width: 81 | description: Width of the terminal 82 | required: false 83 | terminal_min_width: 84 | description: Minimum width of the terminal (use trimming) 85 | required: false 86 | notrim: 87 | description: Disable automatic trimming of terminal width 88 | required: false 89 | terminal_theme: 90 | description: Colour theme 91 | required: false 92 | snippet_theme: 93 | description: Snippet Pygments theme 94 | required: false 95 | use_pty: 96 | description: Use a pseudo-terminal for commands (may capture coloured output) 97 | required: false 98 | log_verbose: 99 | description: Print verbose output to the console. 100 | required: false 101 | commit_changes: 102 | description: Automatically commit changes to the repository 103 | required: false 104 | error_changes: 105 | description: Exit with an error if changes are found (Ignored if 'commit_changes' is true) 106 | default: "true" 107 | required: false 108 | use_uv: 109 | description: If true, use uv to manage python and install dependencies. 110 | default: "false" 111 | required: false 112 | skip_python_setup: 113 | description: If true, skip Python setup (or skip uv setup if `use_uv` is true). Only use this if you already set up Python in a previous step, e.g. to install additional dependencies. When combined with `use_uv`, this assumes you already ran the `astral-sh/setup-uv` action. 114 | default: "false" 115 | required: false 116 | python_verison: 117 | description: Which version of Python to use when setting up Python or uv. 118 | default: "3" 119 | required: false 120 | 121 | runs: 122 | using: "composite" 123 | steps: 124 | - name: Set up uv 125 | if: inputs.skip_python_setup != 'true' && inputs.use_uv == 'true' 126 | uses: astral-sh/setup-uv@v5 127 | with: 128 | python-version: 3.x 129 | 130 | - name: Set up Python 131 | if: inputs.skip_python_setup != 'true' && inputs.use_uv != 'true' 132 | uses: actions/setup-python@v5 133 | with: 134 | python-version: 3.x 135 | 136 | - name: Install rich-codex with uv 137 | if: inputs.use_uv == 'true' 138 | run: | 139 | echo "::group::Installing rich-codex" 140 | sudo apt install fonts-firacode 141 | uv pip install "rich-codex[cairo]" 142 | echo "::endgroup::" 143 | shell: bash 144 | 145 | - name: Install rich-codex with pip 146 | if: inputs.use_uv != 'true' 147 | run: | 148 | echo "::group::Installing rich-codex" 149 | sudo apt install fonts-firacode 150 | pip install "rich-codex[cairo]" 151 | echo "::endgroup::" 152 | shell: bash 153 | 154 | - name: Run rich-codex 155 | run: rich-codex 156 | shell: bash 157 | env: 158 | SEARCH_INCLUDE: ${{ inputs.search_include }} 159 | SEARCH_EXCLUDE: ${{ inputs.search_exclude }} 160 | NO_SEARCH: ${{ inputs.no_search }} 161 | COMMAND: ${{ inputs.command }} 162 | TIMEOUT: ${{ inputs.timeout }} 163 | WORKING_DIR: ${{ inputs.working_dir }} 164 | BEFORE_COMMAND: ${{ inputs.before_command }} 165 | AFTER_COMMAND: ${{ inputs.after_command }} 166 | SNIPPET: ${{ inputs.snippet }} 167 | SNIPPET_SYNTAX: ${{ inputs.snippet_syntax }} 168 | IMG_PATHS: ${{ inputs.img_paths }} 169 | CLEAN_IMG_PATHS: ${{ inputs.clean_img_paths }} 170 | RC_CONFIGS: ${{ inputs.rc_configs }} 171 | FAKE_COMMAND: ${{ inputs.fake_command }} 172 | HIDE_COMMAND: ${{ inputs.hide_command }} 173 | TITLE_COMMAND: ${{ inputs.title_command }} 174 | RC_HEAD: ${{ inputs.head }} 175 | RC_TAIL: ${{ inputs.tail }} 176 | TRIM_AFTER: ${{ inputs.trim_after }} 177 | TRUNCATED_TEXT: ${{ inputs.truncated_text }} 178 | SKIP_GIT_CHECKS: ${{ inputs.skip_git_checks }} 179 | MIN_PCT_DIFF: ${{ inputs.min_pct_diff }} 180 | SKIP_CHANGE_REGEX: ${{ inputs.skip_change_regex }} 181 | TERMINAL_WIDTH: ${{ inputs.terminal_width }} 182 | TERMINAL_MIN_WIDTH: ${{ inputs.terminal_min_width }} 183 | NOTRIM: ${{ inputs.notrim }} 184 | TERMINAL_THEME: ${{ inputs.terminal_theme }} 185 | SNIPPET_THEME: ${{ inputs.snippet_theme }} 186 | USE_PTY: ${{ inputs.use_pty }} 187 | CREATED_FILES: "created.txt" 188 | DELETED_FILES: "deleted.txt" 189 | LOG_VERBOSE: ${{ inputs.log_verbose }} 190 | LOG_SAVE: "true" 191 | NO_CONFIRM: "true" 192 | 193 | - name: Add and commit new images 194 | shell: bash 195 | run: | 196 | echo "::group::Pushing any changes found" 197 | git config --local user.name 'github-actions[bot]' 198 | git config --local user.email 'github-actions[bot]@users.noreply.github.com' 199 | echo "🕵️‍♀️ Git status after run:" 200 | git status -s 201 | if [[ -f created.txt || -f deleted.txt ]]; then 202 | echo "💥 Found some changes from running rich-codex!" 203 | if [[ ${{ inputs.commit_changes }} ]]; then 204 | [[ -f created.txt ]] && git add $(cat created.txt) 205 | [[ -f deleted.txt ]] && git rm $(cat deleted.txt) 206 | git commit -m "Generate new screengrabs with rich-codex" 207 | git push 208 | echo "🤖 Pushed commit with new changes!" 209 | elif [[ ${{ inputs.error_changes }} ]]; then 210 | echo "🤖 Tip: Use 'commit_changes' to automatically commit them next time." 211 | (exit 1) 212 | fi 213 | else 214 | echo "🤫 No changes from rich-codex found!" 215 | fi 216 | echo "::endgroup::" 217 | 218 | - name: Upload sync log file artifact 219 | if: ${{ always() }} 220 | uses: actions/upload-artifact@v4 221 | with: 222 | name: Rich-codex log file 223 | path: rich_codex_*.log 224 | -------------------------------------------------------------------------------- /docs/cat.txt: -------------------------------------------------------------------------------- 1 | WNNNWW WWNXXXNW 2 | NK0000KXW WNK000000N 3 | WKO0000000XNW WXK00000000KW 4 | N0O0KXXK0000XNW WX0000KKXXK00KN 5 | WX000XXXXXK0000XW WWWWWWWWWWWW WWNK000KKXXXXXK00N 6 | WX000XXXXXXK0000KXK000OO0000OO000KK0000KXXXXXXXK00N 7 | WX00KXXXXXXXK0000OOkkkkkO00OkkkkkO00000KXXXXXXXK00N 8 | X000XXXXXK0000000OOkkkkO00OkkkkOO0000000KXXXXX0OKN 9 | N0O0KXKK0000000000OkkkOO0OOOkkkO0000000000KKXK0OXW 10 | WK00000000000O00000OOOOO00OOOOOO0000000000000000X 11 | N000000000000O00000000000000000000000000000000KN 12 | WK00OO00O000000000000000000000000O000000O00000KW 13 | N00000000kdodkO0O00000000000000O0koc:cdO0000000XW 14 | WK000000Oo. .lO0O00000000000000k; .cO000000KN 15 | X0000000k, .x00000000000000O0o. ,k0O00000KXKKN 16 | NKKK0OO00000Oc 'x000O00OOOOO00000x, .lO000OOkkO0KXW 17 | WNXKOkkkO0000Ol,'':x0000xc,....';dO000kl:;cdO0000OkkOKWWWW 18 | WNNNXKOOkO000000OOO000000o. :O0000000000000OOkkkOKKKXW 19 | NKKKKK0kkkO000000000OkkO00Od;. 'ok00OxxO00000000OOkOOXWW 20 | WNKOOO000000000k,.:xkkxc. :xOOx:.,x0OO00000OkkkO0KKXW 21 | NKKK0OkOO00000000Od,......'::,......,oO000000000OO0XNNXXW 22 | WNWWWN0000000000000OkollodO00OxolloxO000000000000KNW 23 | WNK00O000000000000000000000000O000000O00000XW 24 | WNK000000000000000000000000000000000000XNW 25 | WNXK00000000000000000000000000000KXNW 26 | WNNXXKK00000000000000000KKKXNNW 27 | WWWWNNNNNNNNNNNNNWWWW 28 | -------------------------------------------------------------------------------- /docs/config/cleaning.md: -------------------------------------------------------------------------------- 1 | # Removing images 2 | 3 | If you change the output filename of an image, a new file will be created. 4 | However, the old one will remain which is probably not what you intended. 5 | 6 | To avoid this rich-codex can "clean" an image path, deleting any matching files that were not generated during a run. 7 | 8 | This is done using `--clean-img-paths` / `$CLEAN_IMG_PATHS` / `clean_img_paths` (CLI, env var, action/config). One or more filename glob patterns (separated by newlines) can be given. At the end of the run, all matching files that were not generated will be deleted. 9 | 10 | 11 | !!! warning 12 | Rich-codex will clean _all_ files matching your pattern. Including your source code. Handle with care 🔥 13 | 14 | -------------------------------------------------------------------------------- /docs/config/colours.md: -------------------------------------------------------------------------------- 1 | ## Using a pseudo-terminal 2 | 3 | By default, rich-codex runs commands in a Python `subprocess`. This is not an interactive terminal, and as such many command-line tools will disable coloured output. 4 | 5 | This is best solved at the tool level if possible, by telling the tool to force coloured output. However, if this is not possible then you can use `--use-pty` / `$USE_PTY` / `use_pty` (CLI, env var, action/config). This uses a [Python `pty` pseudo-terminal](https://docs.python.org/dev/library/pty.html) instead of [`subprocess`](https://docs.python.org/dev/library/subprocess.html) which may trick your tool into keeping coloured output. 6 | 7 | 8 | !!! warning 9 | Note that PTY almost certainly won't work on Windows and is generally more likely to do weird stuff / create poorly formatted outputs than the default subprocess shell. 10 | 11 | 12 | ## Colour theme 13 | 14 | You can customise the theme using `--terminal-theme` / `$TERMINAL_THEME` / `terminal_theme` (CLI, env, action/config). 15 | 16 | Themes are taken from [Rich](https://github.com/Textualize/rich/blob/master/rich/terminal_theme.py), at the time of writing the following are available: 17 | 18 | - `DEFAULT_TERMINAL_THEME` 19 | - `MONOKAI` 20 | - `DIMMED_MONOKAI` 21 | - `NIGHT_OWLISH` 22 | - `SVG_EXPORT_THEME` 23 | 24 | The terminal theme should be set as a string to one of these values. 25 | 26 | 27 | !!! note 28 | It's planned to add support for custom themes but not yet implemented. If you need this, please create a GitHub issue / pull-request. 29 | 30 | 31 | `DEFAULT_TERMINAL_THEME`: 32 | 33 | 34 | 35 | ![`rich ../../setup.cfg -h 5 --force-terminal`](../img/theme-default_terminal_theme.svg "DEFAULT_TERMINAL_THEME") 36 | 37 | `MONOKAI`: 38 | 39 | 40 | 41 | ![`rich ../../setup.cfg -h 5 --force-terminal`](../img/theme-monokai.svg "MONOKAI") 42 | 43 | `DIMMED_MONOKAI`: 44 | 45 | 46 | 47 | ![`rich ../../setup.cfg -h 5 --force-terminal`](../img/theme-dimmed_monokai.svg "DIMMED_MONOKAI") 48 | 49 | `NIGHT_OWLISH`: 50 | 51 | 52 | 53 | ![`rich ../../setup.cfg -h 5 --force-terminal`](../img/theme-night_owlish.svg "NIGHT_OWLISH") 54 | 55 | `SVG_EXPORT_THEME`: 56 | 57 | 58 | 59 | ![`rich ../../setup.cfg -h 5 --force-terminal`](../img/theme-svg_export_theme.svg "SVG_EXPORT_THEME") 60 | 61 | ## Snippet colours 62 | 63 | Snippets are formatted using [rich Syntax objects](https://rich.readthedocs.io/en/stable/syntax.html). 64 | These use Pygments to add code colouring, which has its own set of themes - separate to the terminal theme that the snippet is wrapped in. 65 | 66 | As such, if using snippets, you'll probably want to set both the terminal theme and the Pygments style. 67 | You can find available Pygments styles in the [Pygments docs](https://pygments.org/docs/styles/#getting-a-list-of-available-styles). 68 | 69 | `snippet_theme: xcode` + `terminal_theme: DEFAULT_TERMINAL_THEME`: 70 | 71 | 91 | 92 | ![DEFAULT_TERMINAL_THEME + sas](../img/snippet-theme-sas.svg "DEFAULT_TERMINAL_THEME + sas") 93 | 94 | `snippet_theme: monokai` + `terminal_theme: SVG_EXPORT_THEME`: 95 | 96 | 116 | 117 | ![SVG_EXPORT_THEME + monokai](../img/snippet-theme-monokai.svg "SVG_EXPORT_THEME + fruity") 118 | 119 | `snippet_theme: fruity` + `terminal_theme: MONOKAI`: 120 | 121 | 141 | 142 | ![MONOKAI + fruity](../img/snippet-theme-fruity.svg "MONOKAI + fruity") 143 | -------------------------------------------------------------------------------- /docs/config/command_setup.md: -------------------------------------------------------------------------------- 1 | ## Setting extra environment vars 2 | 3 | In some cases you may want to set environment variables for a single command only. 4 | In this case, you can use the `extra_env` config option, which adds YAML key:value pairs to the environment for just that command (and the before / after commands, see below). 5 | 6 | I use this method to set the terminal width for the rich-codex screenshots in this documentation: 7 | 8 | 9 | ```markdown 10 | 16 | ![`rich-codex --help`](../img/rich-codex-cli-envs.svg) 17 | ``` 18 | 19 | !!! tip 20 | It's probably easier to set these at run-level if that's an option, these are really only if you want to customise for a single output. 21 | 22 | 23 | ## Faking simple commands 24 | 25 | Sometimes you may need to have long complicated commands to get the screenshot you need, when the typical command for an end user would be much simpler. 26 | 27 | In this case, you can fake the command shown in the terminal prompt using `--fake-command` / `$FAKE_COMMAND` / `fake_command`. 28 | 29 | For example: 30 | 31 | 32 | 33 | ```markdown 34 | 35 | ![`echo "I won't tell if you don't 🤫" > temp_file.txt && echo "" && cat temp_file.txt && rm temp_file.txt`](../img/fake_command.svg) 36 | ``` 37 | ![](../img/fake_command.svg) 38 | 39 | 40 | 41 | ## Running commands before and after 42 | 43 | Chaining complex commands may not always work if the setup / cleanup commands generate output that you don't want to show in the screenshot. 44 | 45 | In these more complex scenarios, you can run additional commands before and after the one used for the screenshot. This is done with the following options: 46 | 47 | - `--before-command` / `$BEFORE_COMMAND` / `before_command` 48 | - `--after-command` / `$AFTER_COMMAND` / `after_command`. 49 | 50 | These run separate `subprocess` calls with the specified commands before and after the target command. 51 | This can be useful for initialising an environment and then cleaning up afterwards. 52 | 53 | For example: 54 | 55 | 56 | 57 | ```markdown 58 | 63 | ![`cat before_after_command_example.txt`](../img/before_after_command.svg) 64 | ``` 65 | ![](../img/before_after_command.svg) 66 | 67 | !!! note: 68 | Commands should be a single string, so remember to chain using `&&` and ideally use YAML multi-line strings that collapse newlines using `>`. 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/config/ignoring_changes.md: -------------------------------------------------------------------------------- 1 | You may find that your screenshots are changing every time you run rich-codex, even though no relevant changes have occured within your code. This could be because the screenshots include timestamps or some other live data. 2 | 3 | To avoid doubling your commit count with changes that you don't care about, rich-codex has two mechanisms which you can use to ignore changes: 4 | 5 | - ⚖️ Percentage change in file contents 6 | - 🔎 Regular expression matches 7 | 8 | ## Percentage change in file contents 9 | 10 | When you run rich-codex, any new images created will generate log messages that look like this: 11 | `Saved: 'docs/img/rich-codex-snippet-title.svg' (4.63% change)`. 12 | This percentage change is calculated using the [python-Levenshtein](https://github.com/ztane/python-Levenshtein) package, comparing the raw bytes of the two files. 13 | 14 | By default, any new files with 0.00% change will be ignored. If you find that you have screenshots changing by the same small percentage every time, you can raise this threshold by setting `--min-pct-diff` / `$MIN_PCT_DIFF` / `min_pct_diff` (CLI, env var, action/config). 15 | 16 | For example, if a timestamp caused this file to change by 4.34% on every commit, those changes could be ignored as follows: 17 | 18 | 19 | ```markdown 20 | 21 | ![`rich-codex --help`](../img/rich-codex-help-min-pct.svg) 22 | ``` 23 | 24 | 25 | ## Regular expression matches 26 | 27 | Percentage changes in files is quick and simple, but a little crude. If you prefer, you may be able to use regular expressions instead with `--skip-change-regex` / `$SKIP_CHANGE_REGEX` / `skip_change_regex` (CLI, env var, action/config). 28 | 29 | If there is a > 0% change in files, a rich diff will be generated. Any diff lines matching the supplied regexes will be removed and if none remain, the changeset will be ignored. 30 | 31 | Rich-codex ships with one default, applied for PDF files: if the only change is a line with `"/CreationDate"` then the changeset will be ignored. 32 | 33 | 34 | !!! warning 35 | Please note that generating diffs between file pairs can be _very_ slow. Use with caution. 36 | 37 | -------------------------------------------------------------------------------- /docs/config/overview.md: -------------------------------------------------------------------------------- 1 | ## Config locations 2 | 3 | Rich-codex can be run in several different ways and get configuration from multiple different locations. 4 | They are, in order of parsing / precidence (last location wins): 5 | 6 | 7 | 8 | - Global, for entire run: 9 | - GitHub Action arguments 10 | - Environment variables 11 | - Command-line flags (CLI) 12 | - Rich-codex config files (`.rich-codex.yml`) 13 | - Per-image: 14 | - Rich-codex config files (`.rich-codex.yml`) 15 | - Markdown config 16 | 17 | 18 | 19 | ## Overview of all options 20 | 21 | An overview of all available config options in all scopes is below: 22 | 23 | | CLI | Environment variable | GitHub Action / Rich-codex config | 24 | | ---------------------- | -------------------- | --------------------------------- | 25 | | `--search-include` | `SEARCH_INCLUDE` | `search_include` | 26 | | `--search-exclude` | `SEARCH_EXCLUDE` | `search_exclude` | 27 | | `--no-search` | `NO_SEARCH` | `no_search` | 28 | | `--command` | `COMMAND` | `command` | 29 | | `--timeout` | `TIMEOUT` | `timeout` | 30 | | `--working-dir` | `WORKING_DIR` | `working_dir` | 31 | | `--before-command` | `BEFORE_COMMAND` | `before_command` | 32 | | `--after-command` | `AFTER_COMMAND` | `after_command` | 33 | | `--snippet` | `SNIPPET` | `snippet` | 34 | | `--snippet-syntax` | `SNIPPET_SYNTAX` | `snippet_syntax` | 35 | | `--img-paths` | `IMG_PATHS` | `img_paths` | 36 | | `--clean-img-paths` | `CLEAN_IMG_PATHS` | `clean_img_paths` | 37 | | `--configs` | `RC_CONFIGS` | `rc_configs` | 38 | | `--fake-command` | `FAKE_COMMAND` | `fake_command` | 39 | | `--hide-command` | `HIDE_COMMAND` | `hide_command` | 40 | | `--title-command` | `TITLE_COMMAND` | `title_command` | 41 | | `--head` | `RC_HEAD` | `head` | 42 | | `--tail` | `RC_TAIL` | `tail` | 43 | | `--trim-after` | `TRIM_AFTER` | `trim_after` | 44 | | `--truncated-text` | `TRUNCATED_TEXT` | `truncated_text` | 45 | | `--skip-git-checks` | `SKIP_GIT_CHECKS` | `skip_git_checks` | 46 | | `--no-confirm` | `NO_CONFIRM` | - | 47 | | `--min-pct-diff` | `MIN_PCT_DIFF` | `min_pct_diff` | 48 | | `--skip-change-regex` | `SKIP_CHANGE_REGEX` | `skip_change_regex` | 49 | | `--terminal-width` | `TERMINAL_WIDTH` | `terminal_width` | 50 | | `--terminal-min-width` | `TERMINAL_MIN_WIDTH` | `terminal_min_width` | 51 | | `--notrim` | `NOTRIM` | `notrim` | 52 | | `--terminal-theme` | `TERMINAL_THEME` | `terminal_theme` | 53 | | `--snippet-theme` | `SNIPPET_THEME` | `snippet_theme` | 54 | | `--use-pty` | `USE_PTY` | `use_pty` | 55 | | `--created-files` | `CREATED_FILES` | - | 56 | | `--deleted-files` | `DELETED_FILES` | - | 57 | | `--verbose` | `LOG_VERBOSE` | `log_verbose` \* | 58 | | `--save-log` | `LOG_SAVE` | - | 59 | | `--log-file` | `LOG_FILENAME` | - | 60 | | - | - | `commit_changes` \* | 61 | | - | - | `error_changes` \* | 62 | | - | - | `title` † | 63 | | - | - | `extra_env` † | 64 | | - | - | `skip` † | 65 | 66 | - `*` - GitHub Action only 67 | - `†` - Markdown / config only (per-output, not global) 68 | 69 | ## Description of options 70 | 71 | A brief description of each option follows. 72 | 73 | 74 | !!! note 75 | Hopefully all config options will be either fairly self-explanitory and/or documented in more details elsewhere. 76 | If not, please open an issue on GitHub 77 | 78 | 79 | - `--search-include`: Glob patterns to search for rich-codex comments 80 | - `--search-exclude`: Glob patterns to exclude from search for rich-codex comments 81 | - `--no-search`: Set to disable searching for rich-codex comments 82 | - `--command`: Specify a command to run to capture output 83 | - `--timeout`: Maximum run time for command (seconds) 84 | - `--hide-command`: Hide the terminal prompt with the command at the top of the output 85 | - `--title-command`: Use the command as the terminal title if not set explicitly 86 | - `--head`: Show only the first N lines of output 87 | - `--tail`: Show only the last N lines of output 88 | - `--trim-after`: Don't print any more lines after this string is found 89 | - `--truncated-text`: Text to show when --head or --tail truncate content 90 | - `--snippet`: Literal code snippet to render 91 | - `--snippet-syntax`: Language to use for snippet sytax highlighting 92 | - `--img-paths`: Path to image filenames if using 'command' or 'snippet' 93 | - `--clean-img-paths`: Remove any matching files that are not generated 94 | - `--configs`: Paths to YAML config files 95 | - `--skip-git-checks`: Skip safety checks for git repos 96 | - `--no-confirm`: Set to skip confirmation prompt before running commands 97 | - `--min-pct-diff`: Minimum file percentage change required to update image 98 | - `--skip-change-regex`: Skip image update if file changes match regex 99 | - `--terminal-width`: Width of the terminal 100 | - `--terminal-min-width`: Minimum width of the terminal when trimming 101 | - `--notrim`: Disable automatic trimming of terminal width 102 | - `--terminal-theme`: Colour theme 103 | - `--snippet-theme`: Snippet Pygments theme 104 | - `--use-pty`: Use a pseudo-terminal for commands (may capture coloured output) 105 | - `--created-files`: Save a list of created files to this file 106 | - `--deleted-files`: Save a list of deleted files to this file 107 | - `--verbose`: Print verbose output to the console. 108 | - `--save-log`: Save a verbose log to a file (automatic filename). 109 | - `--log-file`: Save a verbose log to a file (specific filename). 110 | - `commit_changes`: Automatically commit changes to the repository 111 | - `error_changes`: Exit with an error if changes are found (Ignored if `commit_changes` is true) 112 | - `title`: Title for the terminal title bar 113 | - `skip`: Skip / ignore this image 114 | -------------------------------------------------------------------------------- /docs/config/time_limits.md: -------------------------------------------------------------------------------- 1 | As rich-codex runs commands within a non-interactive subshell, any command that requires input could cause the tool to hang forever. 2 | 3 | To avoid this, rich-codex sets a maximum time limit on all commands (default: `5 seconds`). Once a command runs for this time, it is killed and the screenshot is created with whatever output was captured up to that point. 4 | 5 | The amount of time that rich-codex waits for can be configured using `--timeout` / `$TIMEOUT` / `timeout` (CLI, env var, action/config). 6 | -------------------------------------------------------------------------------- /docs/config/tweaks.md: -------------------------------------------------------------------------------- 1 | ## Snippet syntax 2 | 3 | If snippets can be parsed as JSON, they will be automatically reformatted (pretty-printed) and set to use JSON code syntax colouring. Otherwise they will be printed as white text by default. 4 | 5 | To use coloured syntax highlighting on your non-JSON code snippets, you need to tell rich-codex what syntax to use with the `--snippet-syntax` / `$SNIPPET_SYNTAX` / `snippet_syntax` option (CLI, env var, action/config). 6 | 7 | Syntax highlighting is done using [rich](https://rich.readthedocs.io/en/latest/syntax.html) which uses [Pygments](https://pygments.org). Any language [supported by Pygments](https://pygments.org/languages/) should work. 8 | 9 | 10 | ```markdown 11 | 21 | ![c++ example](../img/snippet-syntax.svg) 22 | ``` 23 | ![c++ example](../img/snippet-syntax.svg) 24 | 25 | 26 | ## Hiding the command 27 | 28 | By default, rich-codex shows a command prompt with the command that was used to generate the iamge. 29 | This can be disabled by setting using `--hide-command` / `$HIDE_COMMAND` / `hide_command` (CLI, env var, action/config). 30 | 31 | 32 | Default: 33 | 34 | ```markdown 35 | ![`echo "See the command printed at the top?"`](../img/hide-command-default.svg) 36 | ``` 37 | ![echo "See the command printed at the top?"](../img/hide-command-default.svg) 38 | 39 | With `hide_command` set to `true`: 40 | 41 | ```markdown 42 | 43 | ![`echo "What command was used? It's a mystery.."`](../img/hide-command.svg) 44 | ``` 45 | 46 | ![echo "What command was used? It's a mystery.."](../img/hide-command.svg) 47 | 48 | 49 | ## Showing the command in the title 50 | 51 | Rich-codex sets the title in the screenshot only if you provide it (config or as title text in the markdown image). 52 | If you like you can tell rich-codex to always use a title, with the command (or fake command) used if the title is not explicitly set. 53 | 54 | Do this with the `--title-command` / `$TITLE_COMMAND` / `title_command` (CLI, env var, action/config). 55 | 56 | 57 | Default: 58 | 59 | ```markdown 60 | ![`echo "No custom title set"`](../img/title-command-default.svg) 61 | ``` 62 | ![echo "No custom title set"](../img/title-command-default.svg) 63 | 64 | With `title_command` set to `true`: 65 | 66 | ```markdown 67 | 68 | ![`echo "Look ma, a title"`](../img/title-command.svg) 69 | ``` 70 | ![echo "Look ma, a title"](../img/title-command.svg) 71 | 72 | 73 | ## Truncating content 74 | 75 | If your tool produces a lot of output you can show just the beginning or end of output. 76 | You can set the number of lines of output that you would like to show using: _(CLI, env var, action/config)_ 77 | 78 | - `--head` / `$RC_HEAD` / `head` 79 | - `--tail` / `$RC_TAIL` / `tail` 80 | 81 | 82 | ```markdown 83 | 84 | ![`rich ../../src/rich_codex/codex_search.py`](../img/head-tail.svg "codex_search.py") 85 | ``` 86 | ![rich ../../src/rich_codex/codex_search.py](../img/head-tail.svg "codex_search.py") 87 | 88 | If the number you set is larger than the amount of output then all output will be shown as usual. 89 | 90 | !!! tip 91 | Remember that you can set both head _and_ tail to remove just the middle section of output 🚀 92 | 93 | 94 | By default, if any output is truncated a line will be printed: `[..truncated..]`. 95 | You can customise this text using `--truncated-text` / `$TRUNCATED_TEXT` / `truncated_text`. 96 | Set it to `None` to omit the line completely. 97 | 98 | ## Trimming content 99 | 100 | You can clean off unwanted content based on a string pattern match using `--trim-after` / `$TRIM_AFTER` / `trim_after`. 101 | 102 | Set it to a string - if that string is found in the input, no more lines will be printed after that. 103 | 104 | No `truncated_text` is shown for this method currently (could be added if anyone wants it). 105 | 106 | 107 | ```markdown 108 | 109 | ![`rich ../../src/rich_codex/codex_search.py`](../img/trim-after_truncated-text.svg "codex_search.py") 110 | ``` 111 | ![rich ../../src/rich_codex/codex_search.py](../img/trim-after_truncated-text.svg "codex_search.py") 112 | 113 | -------------------------------------------------------------------------------- /docs/config/width.md: -------------------------------------------------------------------------------- 1 | # Terminal Width 2 | 3 | ## Trimming 4 | 5 | By default, rich-codex will run your command / parse your snippet and check the length of all output lines. The terminal width will be set to that of the longest line. 6 | 7 | A mimimum width is used to prevent very narrow images. The default for this is `80` characters and can be customised using `--terminal-min-width` / `$TERMINAL_MIN_WIDTH` / `terminal_min_width` (CLI, env var, action/config). 8 | 9 | To turn off trimming, use `--notrim` / `$NOTRIM` / `notrim`. 10 | 11 | 12 | !!! info 13 | Note that console output that is _padded_ with spaces will use the full terminal width available. Much of the output from the rich library is padded. 14 | 15 | In these cases, you will need to consult the upstream tool on how to set terminal width and match that in rich-codex. 16 | 17 | 18 | ## Fixing terminal width 19 | 20 | You can define a specific width to use for the terminal image using `--terminal-width` / `$TERMINAL_WIDTH` / `terminal_width` (CLI, env var, action/config). This is typically joined with `--notrim` to disable automatic trimming. 21 | 22 | If your console output doesn't match this width, you may get weird effects such as cropping or wrapping. You will probably want to try to match this width with upstream tools. 23 | 24 | 25 | !!! tip 26 | Some tools (such as [rich-click](https://github.com/ewels/rich-click)) also honour the environment variable `$TERMINAL_WIDTH` 27 | 28 | -------------------------------------------------------------------------------- /docs/img/before_after_command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $ cat before_after_command_example.txt 58 | This is a very simple example 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/img/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewels/rich-codex/80de9de011c994f32274bb4cffee140567621d8e/docs/img/cat.png -------------------------------------------------------------------------------- /docs/img/fake_command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | $ my_tool --is-simple 61 | 62 | I won't tell if you don't 🤫 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/img/git-pull-rebase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | $ git pull --rebase                                                              88 | 89 | remote: Enumerating objects: 19, done.                                           90 | remote: Counting objects: 100% (19/19), done.                                    91 | remote: Compressing objects: 100% (8/8), done.                                   92 | remote: Total 19 (delta 10), reused 18 (delta 9), pack-reused 0                  93 | Unpacking objects: 100% (19/19), 26.06 KiB | 1.45 MiB/s, done.                   94 | From github.com:ewels/rich-codex                                                 95 | 5bee980..5b092fa  main       -> origin/main                                      96 | + 501309f...9527197 gh-pages   -> origin/gh-pages  (forced update)               97 | Successfully rebased and updated refs/heads/main.                                98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/img/git-push-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | $ git push 87 | 88 | To github.com:ewels/rich-codex.git 89 | !         main -> main (fetch first) 90 | error: failed to push some refs to 'github.com:ewels/rich-codex.git' 91 | hint: Updates were rejected because the remote contains work that you do 92 | hint: not have locally. This is usually caused by another repository pushing 93 | hint: to the same ref. You may want to first integrate the remote changes 94 | hint: (e.g., 'git pull ...') before pushing again. 95 | hint: See the 'Note about fast-forwards' in 'git push --help' for details. 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/img/git-push-success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | $ git push                                                                       88 | 89 | Enumerating objects: 13, done.                                                   90 | Counting objects: 100% (13/13), done.                                            91 | Delta compression using up to 10 threads                                         92 | Compressing objects: 100% (6/6), done.                                           93 | Writing objects: 100% (7/7), 608 bytes | 608.00 KiB/s, done.                     94 | Total 7 (delta 5), reused 0 (delta 0), pack-reused 0                             95 | remote: Resolving deltas: 100% (5/5), completed with 5 local objects.            96 | To github.com:ewels/rich-codex.git                                               97 | fbbec47..482c318  main -> main                                                   98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/img/head-tail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | codex_search.py 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | $ rich ../../src/rich_codex/codex_search.py 113 | import logging 114 | import re 115 | from pathlib import Path 116 | 117 | import yaml 118 | from jsonschema.exceptions import ValidationError 119 | from rich import box 120 | from rich.console import Console 121 | from rich.prompt import Prompt 122 | from rich.table import Table 123 | [..truncated..] 124 |         """Save the images that we have collected.""" 125 |         for img_obj in self.rich_imgs: 126 |             img_obj.get_output() 127 |             img_obj.save_images() 128 |             self.saved_img_paths += img_obj.saved_img_paths 129 |             self.num_img_saved += img_obj.num_img_saved 130 |             self.num_img_skipped += img_obj.num_img_skipped 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/img/hide-command-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $ echo "See the command printed at the top?" 58 | See the command printed at the top? 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/img/hide-command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | What command was used? It's a mystery.. 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/img/rich-codex-snippet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |                     svg2pdf( 117 |                         file_obj=open(svg_tmp_filename, "rb"), 118 |                         write_to=tmp_filename, 119 |                     ) 120 | ifself._enough_image_difference(tmp_filename, filename): 121 |                         copyfile(tmp_filename, filename) 122 |                         pdf_img = filename 123 | 124 | # Delete temprary files 125 |             tmp_path = Path(tmp_filename) 126 | if Path(gettempdir()) in tmp_path.resolve().parents: 127 |                 os.close(tmp_file_handle) 128 |                 tmp_path.unlink() 129 | 130 | # Delete temporary SVG file - after loop as can be reused 131 |         tmp_svg_path = Path(svg_tmp_filename) 132 | if Path(gettempdir()) in tmp_svg_path.resolve().parents: 133 |             os.close(svg_tmp_file_handle) 134 |             tmp_svg_path.unlink() 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/img/snippet-syntax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | include<iostream> 78 | 79 | intmain(){ 80 | std::cout<<"Hello World!"; 81 | return0; 82 | } 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/img/theme-default_terminal_theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | DEFAULT_TERMINAL_THEME 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | $ rich ../../setup.cfg -h 5 --force-terminal 74 | [metadata] 75 | name=rich-codex 76 | version=attr: rich_codex.__version__ 77 | url=https://github.com/ewels/rich-codex 78 | project_urls= 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/img/theme-dimmed_monokai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | DIMMED_MONOKAI 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | $ rich ../../setup.cfg -h 5 --force-terminal 74 | [metadata] 75 | name=rich-codex 76 | version=attr: rich_codex.__version__ 77 | url=https://github.com/ewels/rich-codex 78 | project_urls= 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/img/theme-monokai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | MONOKAI 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | $ rich ../../setup.cfg -h 5 --force-terminal 74 | [metadata] 75 | name=rich-codex 76 | version=attr: rich_codex.__version__ 77 | url=https://github.com/ewels/rich-codex 78 | project_urls= 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/img/theme-night_owlish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | NIGHT_OWLISH 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | $ rich ../../setup.cfg -h 5 --force-terminal 74 | [metadata] 75 | name=rich-codex 76 | version=attr: rich_codex.__version__ 77 | url=https://github.com/ewels/rich-codex 78 | project_urls= 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/img/theme-svg_export_theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | SVG_EXPORT_THEME 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | $ rich ../../setup.cfg -h 5 --force-terminal 74 | [metadata] 75 | name=rich-codex 76 | version=attr: rich_codex.__version__ 77 | url=https://github.com/ewels/rich-codex 78 | project_urls= 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/img/title-command-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $ echo "No custom title set" 58 | No custom title set 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/img/title-command.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | echo "Look ma, a title" 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $ echo "Look ma, a title" 58 | Look ma, a title 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/img/trim-after_truncated-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | codex_search.py 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | $ rich ../../src/rich_codex/codex_search.py 109 | import logging 110 | import re 111 | from pathlib import Path 112 | 113 | import yaml 114 | from jsonschema.exceptions import ValidationError 115 | from rich import box 116 | from rich.console import Console 117 | from rich.prompt import Prompt 118 | from rich.table import Table 119 | 120 | from rich_codex import rich_img 121 | from rich_codex.utils import validate_config 122 | 123 | log = logging.getLogger("rich-codex") 124 | 125 | 126 | class CodexSearch: 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # rich-codex ⚡️📖⚡️ 2 | 3 | A GitHub Action / command-line tool which generates screengrab images of a terminal window, containing _command outputs_ or _code snippets_. 4 | 5 | [![PyPI Version](https://img.shields.io/pypi/v/rich-codex.svg?style=flat-square)](https://pypi.python.org/pypi/rich-codex/) 6 | 7 | ## Introduction 8 | 9 | Having code examples in your documentation is a fantastic way to help users understand what to expect from your tool. 10 | 11 | Using terminal screenshots is a good way to do this because: 12 | 13 | - 🌈 Coloured terminal output is supported 14 | - ↔️ You can fit in long lines without scrolling or cropping (images are auto-resized) 15 | - 😎 They look cool 16 | 17 | However, manually generating these screenshots is a pain 👎🏻 Remembering to update them every time you make a minor change means that they can easily get out of date. 18 | 19 | _**Rich-codex**_ automates this process for you. It searches markdown code for images with shell commands or code snippets. It runs these commands and saves a terminal screen-grab at the embedded path. 20 | 21 | Typical use cases: 22 | 23 | - 📷 Example CLI tool outputs that _automatically stay in sync with your package_ 24 | - ♻️ Syntax-highlighted code snippets that are always up to date with your `examples/` 25 | - 🤩 Fast and simple images for your docs with minimal setup 26 | 27 | ## Quickstart 28 | 29 | 1. 📖 Write some markdown docs, use an image tag with a backtick command inside: 30 | 31 | ```markdown 32 | ![`cat cat.txt | lolcat -S 1`](img/cat.svg) 33 | ``` 34 | 2. 🤖 Add a GitHub Action to automatically run the command, generate the image and commit to the repo: 35 | 36 | ```yaml 37 | on: [push] 38 | jobs: 39 | rich_codex: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Install your custom tools 45 | run: pip install lolcat 46 | 47 | - name: Generate terminal images with rich-codex 48 | uses: ewels/rich-codex@v1 49 | with: 50 | commit_changes: "true" 51 | ``` 52 | 53 | 3. 🌈 Enjoy reading your documentation ![My cat rainbow](img/cat.svg) 54 | 55 | ## How it works 56 | 57 | Rich-codex is a command-line tool that you can run [via a GitHub action](installation/github_action.md) or as a [command line tool](installation/cli.md). It works with any markdown (including GitHub READMEs). 58 | 59 | It collects either commands or code snippets, together with output filenames and configuration options. Commands are run in a subprocess and the standard output & standard error collected. These are then rendered as an image using [Textualize/rich](https://github.com/textualize/rich). 60 | 61 | 62 | !!! tip 63 | Rich-codex creates the images that your markdown docs expect. It doesn't require a HTML build-step and doesn't make any changes to your markdown or its output. As such, it's compatible with _**any documentation engine**_, including rendering markdown on [github.com](https://github.com). 64 | 65 | 66 | Rich-codex needs **inputs** (commands / snippets) and **output filenames** to work. These can be configured in four different ways: 67 | 68 | 69 | 70 | 71 | - 🖼 [Markdown images](inputs/markdown.md) 72 | - Search markdown files for image tags with command alt text. eg: `` ![`rich-codex --help`](img/rich-codex-help.svg) `` 73 | - 💬 [Markdown comments](inputs/markdown.md#code-snippets) 74 | - Search markdown files for special HTML comments. 75 | - ➡️ [Command-line / action inputs](inputs/direct_inputs.md) 76 | - Specify a command or snippet using the action `with` inputs. 77 | - ⚙️ [Config files](inputs/config_file.md) 78 | - Use one or more YAML config files for multiple images and more complex customisation. 79 | 80 | 81 | 82 | Images can be generated as SVG, PNG or PDF (detected by filename extension). 83 | -------------------------------------------------------------------------------- /docs/inputs/config_file.md: -------------------------------------------------------------------------------- 1 | ## YAML config files 2 | 3 | If you prefer, you can configure rich-codex outputs within YAML config files. 4 | 5 | ### Config file locations 6 | 7 | By default, rich-codex looks for files in the following locations (relative to where it runs): 8 | 9 | - `.rich-codex.yml` 10 | - `.github/rich-codex.yml` 11 | - `docs/img/rich-codex.yml` 12 | 13 | You can pass one or more additional config locations (separated with newlines) using `--configs` / `RC_CONFIGS` / `rc_configs` (command line / environment variable / GitHub action key). 14 | 15 | Any files that are not found (including those supplied in addition to the defaults) will be silently ignored. 16 | 17 | 18 | !!! note 19 | Strange things may happen if you have more than one config file, such as global config settings overwriting one another in unpredictable ways. 20 | So it's probably best not to use more than one. 21 | 22 | 23 | ### Validation 24 | 25 | When found, rich-codex will first parse the YAML and validate using the [bundled schema](https://github.com/ewels/rich-codex/blob/main/src/rich_codex/config-schema.yml). 26 | If any validation errors are found, rich-codex will provide a log and exit with an error. 27 | 28 | ### Structure 29 | 30 | Config files can have both top-level configuration options that apply to all files and also an `outputs` array of different things to create. 31 | 32 | Each `outputs` array item must contain an `img_paths` array of output filenames and either a `command` or a `snippet`. 33 | You can optionally add `title` to customise the terminal window title. 34 | 35 | For example: 36 | 37 | ```yaml 38 | outputs: 39 | - command: "cat docs/cat.txt | lolcat -S 1" 40 | title: Image from a config 41 | img_paths: 42 | - docs/img/cat.png 43 | - snippet: | 44 | #!/usr/bin/env python3 45 | # -*- coding: utf-8 -*- 46 | 47 | from rich_codex.cli import main 48 | 49 | if __name__ == "__main__": 50 | main() 51 | img_paths: 52 | - docs/img/main_header.svg 53 | ``` 54 | 55 | There are many other config keys also available. 56 | See the [configuration docs](../config/overview.md) for more details. 57 | 58 | ### Install IDE Validation 59 | 60 | You can validate your `.rich-codex.yml` files in your IDEs using JSONSchema. 61 | 62 | #### VSCode 63 | 64 | 1. Install the [VSCode-YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) 65 | 66 | 2. In your repo, create a `.vscode/settings.jsonc` or `.vscode/settings.template.jsonc` file containing the following data. This is what tells the extension which schema to associate with each file. 67 | 68 | ```json 69 | { 70 | "yaml.schemas": { 71 | "https://raw.githubusercontent.com/ewels/rich-codex/main/src/rich_codex/config-schema.yml": [ 72 | ".rich-codex.yml", 73 | ".rich-codex.yaml" 74 | ] 75 | } 76 | } 77 | ``` 78 | 79 | 3. To prompt other users to install the YAML extension, create a `.vscode/extensions.json` file containing the following data inside your repo: 80 | 81 | ```json 82 | { 83 | "recommendations": ["redhat.vscode-yaml"] 84 | } 85 | ``` 86 | 87 | #### JetBrains (PyCharm, IntelliJ, etc.) 88 | 89 | There are two ways to set this up. 90 | 91 | You can either add the following data to your `.idea/jsonSchemas.xml`: 92 | 93 | ```xml 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | Or you can do this manually in **Preferences > Languages & Frameworks > Schemas and DTDs > Json Schema Mappings**: 125 | 126 | - **Name**: `rich-codex` 127 | - **Schema File or URL**: `https://raw.githubusercontent.com/dbt-labs/dbt-jsonschema/main/schemas/dbt_project.json` 128 | - **Schema Version:** JSON schema version 4 129 | - **Mappings**: 130 | - `.rich-codex.yml` 131 | - `.rich-codex.yaml` 132 | -------------------------------------------------------------------------------- /docs/inputs/direct_inputs.md: -------------------------------------------------------------------------------- 1 | ## Command-line / action inputs 2 | 3 | You can generate images by providing a command or snippet directly to the CLI at run time. 4 | 5 | You need the following command line flags / environment variables / GitHub Action inputs: 6 | 7 | One of: 8 | 9 | - `--command` / `$COMMAND` / `command` 10 | - `--snippet` / `$SNIPPET` / `snippet` 11 | 12 | _And:_ 13 | 14 | - `--img-paths` / `$IMG_PATHS` / `img_paths` 15 | 16 | For example: 17 | 18 | ```bash 19 | rich-codex --command 'my-command --yay' --img-paths 'docs/example.svg' 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/inputs/markdown.md: -------------------------------------------------------------------------------- 1 | ## Running commands 2 | 3 | If you write markdown with images that contain _just_ a `backtick command` as the alt-text, rich-codex will find them. 4 | 5 | For example, the following markdown will generate `../img/rich-codex-help.svg` (the image shown above) based on the output of the command `rich-codex --help`: 6 | 7 | 13 | 14 | ```markdown 15 | ![`rich-codex --help`](../img/rich-codex-help.svg) 16 | ``` 17 | 18 | ![rich-codex --help](../img/rich-codex-help.svg) 19 | 20 | ## Printing files 21 | 22 | You can use any command-line tool to print files for screenshots. 23 | A good option is [rich-cli](https://github.com/Textualize/rich-cli), which you can use to easily produce screenshots of files with the `rich` command with nice syntax highlighting: 24 | 25 | 26 | !!! tip 27 | You probably want to hide the command with `hide_command` - see below for more about configuration. 28 | 29 | ```markdown 30 | 31 | ![`rich ../../src/rich_codex/rich_img.py --tail 20 --force-terminal`](../img/rich-codex-snippet.svg) 32 | ``` 33 | 34 | ![`rich ../../src/rich_codex/rich_img.py --tail 20 --force-terminal`](../img/rich-codex-snippet.svg) 35 | 36 | 37 | !!! tip 38 | 💡 Use the `--force-terminal` flag to keep colours in your screenshots 39 | 40 | !!! note 41 | `rich-codex` used to bundle `rich-cli` as a requirement, but this was dropped in v1.2.7 42 | 43 | 44 | ## Title text 45 | 46 | You can also add markdown image [title text](https://daringfireball.net/projects/markdown/syntax#img) in quotes after the filename, which will be used in the top menu bar of the screenshot terminal. (Alternatively, set the `title` config option in a comment). 47 | 48 | This can be useful when adding lots of command markup to get a good screenshot. 49 | You might also want to hide the command prompt. 50 | For example: 51 | 52 | 53 | ```markdown 54 | You don't always want people to see the exact command you used, after all. 55 | 56 | ![`rich ../../src/rich_codex/rich_img.py --tail 20 --force-terminal --guides --panel rounded --panel-style magenta --theme monokai`](../img/rich-codex-snippet-title.svg "rich_img.py") 57 | ``` 58 | 59 | 60 | ![long rich-cli command](../img/rich-codex-snippet-title.svg "rich_img.py") 61 | 62 | ## Config comments 63 | 64 | You can use a HTML comment in a line above the image to set config attributes for this image only. 65 | The comment should begin with `RICH-CODEX` and then have valid YAML after that. 66 | 67 | The parsed configuration will be validated - see an overview of available variables on the [config overview docs](../config/overview.md). 68 | 69 | For example: 70 | 71 | 72 | 73 | ```markdown 74 | 75 | ![`cowsay -t "Taste the rainbow" | lolcat -S 100`](../img/taste-the-rainbow.svg "Taste the rainbow") 76 | ``` 77 | 78 | 79 | ![`cowsay -t "Taste the rainbow" | lolcat -S 100`](../img/taste-the-rainbow.svg "Taste the rainbow") 80 | 81 | 82 | 83 | ## Code snippets 84 | 85 | In addition to running commands, you can format code blocks or "snippets". 86 | 87 | To do this, make the ` 91 | 92 | !!! info 93 | The alt-text for the markdown image embed doesn't matter for snippets. However, if it has a command in backticks then this will take priority over the snippet. 94 | 95 | If the snippet is valid JSON, it will be pretty-printed and coloured. Otherwise text will default to white. 96 | 97 | ```markdown 98 | 111 | ![my JSON snippet](../img/example-json-snippet.svg) 112 | ``` 113 | ![my snippet](../img/example-json-snippet.svg) 114 | 115 | For other code languages, use `snippet_syntax` to define which language to format in. For example: 116 | 117 | ```markdown 118 | 134 | ![](../img/example-python-snippet.svg) 135 | ``` 136 | ![](../img/example-python-snippet.svg) 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/safety.md: -------------------------------------------------------------------------------- 1 | 2 | !!! danger "💥⚠️ **Warning** ⚠️💥" 3 | Reminder: rich-codex runs arbitrary commands found in documentation on your host system. You are responsible for ensuring that it does not do any damage. 4 | 5 | 6 | ## Prompts for commands 7 | 8 | When rich-codex runs interactively, it collects all commands to be run and presents these to you, the user. You then need to choose whether to run all commands, choose some or to ignore all of them. 9 | 10 | You can disable these checks by using the `--no-confirm` CLI flag / setting env var `$NO_CONFIRM`. 11 | 12 | ## Banned commands 13 | 14 | As a very basic safety step, rich-codex attempts to ignore any commands that start with the following: `rm`, `cp`, `mv`, `sudo`. This is to avoid accidentally messing with your local system. 15 | 16 | Please note that this is only for rough protection against accidents and would be easy for a malicious user to circumvent _(for example, putting these commands in a bash script and running that)_. 17 | 18 | ## Git checks 19 | 20 | By default, rich-codex checks that: 21 | 22 | - You are running within a initialised git repository 23 | - You do not have any uncommitted changes 24 | 25 | This is because rich-codex overwrites local files. If you're running within a clean git repo you can easily see what has been changed and revert it. 26 | 27 | You can disable these checks by using the `--skip-git-checks` CLI flag / setting env var `$SKIP_GIT_CHECKS`. 28 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## No image generated 2 | 3 | First up: did you remember the `!`? It's `` ![`command`](path) `` - I've spent longer than I'd care to admit debugging only to realise I missed the leading `!` 🙈 4 | 5 | Next, check the verbose log - it's saved as an artefact with GitHub Actions or locally you can use the `-v`/`--verbose` flag. The verbose log tells you which files are being searched and gives you more insight into what rich-codex is doing. 6 | 7 | ## Can't push new commits 8 | 9 | If you're fairly new to using git, you might find this error message a bit intimidating when you first see it: 10 | 11 | 14 | 15 | ![`echo "\nTo github.com:ewels/rich-codex.git\n [red]! [rejected][/] main -> main (fetch first)\n[red]error: failed to push some refs to 'github.com:ewels/rich-codex.git'[/]\n[yellow]hint: Updates were rejected because the remote contains work that you do[/]\n[yellow]hint: not have locally. This is usually caused by another repository pushing[/]\n[yellow]hint: to the same ref. You may want to first integrate the remote changes[/]\n[yellow]hint: (e.g., 'git pull ...') before pushing again.[/]\n[yellow]hint: See the 'Note about fast-forwards' in 'git push --help' for details.[/]" | rich - -p --force-terminal`](img/git-push-error.svg) 16 | 17 | Fear not, what has likely happened is: 18 | 19 | - Your rich-codex GitHub Action has run since you last pushed 20 | - It created some new images and you have set it to automatically commit these new images to your repository 21 | - Because of this, your git remote now has new commits that you don't yet have locally 22 | - Git doesn't let you push your work until that's resolved 23 | 24 | The fix is usually simple - you need to pull the new commits and tell git to _rebase_ your new work on top of that. 25 | Git will shuffle the commits one in front of the other and then you'll be able to push without any issues: 26 | 27 | 41 | 42 | ![git pull --rebase](img/git-pull-rebase.svg) 43 | 44 | 58 | 59 | ![git push](img/git-push-success.svg) 60 | -------------------------------------------------------------------------------- /docs/usage/cli.md: -------------------------------------------------------------------------------- 1 | # Command-line 2 | 3 | In addition to the GitHub Action, rich-codex is also a stand-alone command line tool. 4 | 5 | You are welcome to use it locally, for example when first writing new documentation and generating initial images to check their output. 6 | 7 | !!! danger "💥⚠️ **Warning** ⚠️💥" 8 | 9 | Please remember that rich-codex is designed to _**run arbitrary commands**_ that it finds within documentation for your project. 10 | 11 | You alone are responsible for any damage you cause to your computer! 🙃 Running rich-codex entirely within GitHub Actions is recommended 👍🏻 12 | 13 | ## Local installation 14 | 15 | You can install `rich-codex` from the [Python Package Index (PyPI)](https://pypi.org/project/rich-codex/) with `pip` or equivalent. 16 | 17 | ```bash 18 | pip install rich-codex 19 | ``` 20 | 21 | At its simplest, the command-line tool runs without any arguments and recursively searches the current working directory for anything it recognises: 22 | 23 | ```bash 24 | rich-codex 25 | ``` 26 | 27 | Behaviour can be customised with command-line flags or by setting environment variables, see `rich-codex --help`: 28 | 29 | 35 | 36 | ![`rich-codex --help`](../img/rich-codex-cli-help.svg) 37 | 38 | ## Requirements for PNG / PDF outputs 39 | 40 | If you wish to generate `PNG` or `PDF` images (not just `SVG`) then there are a few additional requirements. Conversion is done using [CairoSVG](https://cairosvg.org/). First, install rich-codex with the `cairo` [extra](https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-setuptools-extras): 41 | 42 | ```bash 43 | pip install rich-codex[cairo] 44 | ``` 45 | 46 | You'll then probably need some additional libraries, see the [Cairo documentation](https://cairosvg.org/documentation/): 47 | 48 | > CairoSVG and its dependencies may require additional tools during the installation: a compiler, Python headers, Cairo, and FFI headers. These tools have different names depending on the OS you are using, but: 49 | > 50 | > - on Windows, you’ll have to install Visual C++ compiler for Python and Cairo; 51 | > - on macOS, you’ll have to install cairo and libffi (eg. with [Homebrew](https://brew.sh): `brew install cairo`); 52 | > - on Linux, you’ll have to install the cairo, python3-dev and libffi-dev packages (names may vary for your distribution). 53 | 54 | Installation can be messy, so be prepared to do a bit of googling to get things to work. Remember that running rich-codex with the `-v` flag to get verbose logging can give you more information about what's going wrong (if anything). 55 | 56 | You'll also need Fira Code installed, an open-licence font: [GitHub repo](https://github.com/tonsky/FiraCode) / [Google Fonts](https://fonts.google.com/specimen/Fira+Code). 57 | -------------------------------------------------------------------------------- /docs/usage/docker_image.md: -------------------------------------------------------------------------------- 1 | There is a docker image for running rich-codex, available [on GitHub](https://github.com/ewels/rich-codex/pkgs/container/rich-codex): 2 | 3 | ```bash 4 | docker pull ghcr.io/ewels/rich-codex:latest 5 | ``` 6 | 7 | - The label `latest` will pull the most recent release 8 | - The label `main` will pull the development version 9 | - Releases will have their own specific labels. 10 | 11 | 12 | !!! warning 13 | If you're trying to run commands, they will likely not be available in the container! 14 | So this image is best used for code snippets or common linux tools. 15 | Alternatively, you can build your own docker image using this as a base, with additional dependencies installed: `FROM ghcr.io/ewels/rich-codex:latest` 16 | 17 | 18 | To run, a typical command would be: 19 | 20 | ```bash 21 | docker run -i -v `pwd`:`pwd` -w `pwd` -u $(id -u):$(id -g) ghcr.io/ewels/rich-codex 22 | ``` 23 | 24 | - The `-i` flag enables stdin so that you can confirm running commands (alternatively, use `--no-confirm` at the end) 25 | - The `-v` argument tells Docker to bind your current working directory (`pwd`) to the same path inside the container, so that files created there will be saved to your local file system outside of the container. 26 | - `-w` sets the working directory in the container to this path, so that it's the same as your working directory outside of the container. 27 | - `-u` sets your local user account as the user inside the container, so that any files created have the correct ownership permissions. 28 | 29 | You can then pass environment variables with the `-e` flag to customise behaviour. See the usage instructions below for the available environment variables. 30 | -------------------------------------------------------------------------------- /docs/usage/github_action.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub Action 3 | --- 4 | 5 | Rich-codex was primarily designed to run automatically with GitHub actions, to keep your screenshots up to date for you. 6 | 7 | If there are changes to the images, the action can exit with an error (default) or automatically commit the updates. 8 | 9 | 10 | !!! info 11 | For GitHub Actions to push commits to your repository, you'll need to set _Workflow permissions_ to _Read and write permissions_ under _Actions_ -> _General_ in the repo settings. See the [GitHub docs](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#configuring-the-default-github_token-permissions). 12 | 13 | 14 | ## Example workflow 15 | 16 | This action looks for rich-codex content in the repo. It removes any SVG files found in `docs/img/` that don't match the outputs and generates the updated images. If there have been any changes, it pushes a new commit with the updated images. 17 | 18 | ```yaml title=".github/workflows/screenshots.yml" linenums="1" 19 | name: Rich-codex 20 | on: [push] 21 | jobs: 22 | rich_codex: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Check out the repo 26 | uses: actions/checkout@v3 27 | 28 | - name: Install your custom tools 29 | run: pip install . 30 | 31 | - name: Generate terminal images with rich-codex 32 | uses: ewels/rich-codex@v1 33 | with: 34 | commit_changes: "true" 35 | clean_img_paths: docs/img/*.svg 36 | ``` 37 | 38 | 39 | !!! tip 40 | The `@v1` installs the latest rich-codex release under the `v1.x.x` version. 41 | Rich-codex uses semantic versioning, so this means that your workflow will use the most up to date version without the risk of having breaking changes (which would warrant a `v2.x.x` release number). 42 | 43 | 44 | ## Triggers 45 | 46 | In the above example, the workflow is triggered by the line `on: [push]`. 47 | This means that new screenshots will be generated on every commit. 48 | 49 | For some people this may be a little excessive, in which case you might prefer some of the following suggestions. 50 | 51 | 52 | !!! warning 53 | If you have `commit_changes: "true"` set as in the example above, you should only run in environments where pushing a new commit is possible. 54 | For example, using this in a workflow triggered by a release will fail because the workflow will be running on a detached commit. 55 | 56 | 57 | Note that GitHub has [extensive documentation](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow) on the different ways to trigger actions workflows. 58 | 59 | 60 | !!! tip 61 | You can mix and match multiple different triggers! 62 | 63 | 64 | ### If specific files are edited 65 | 66 | If you only want to re-render screenshots if certain files (or filetypes) are edited, you can [filter the `push` event with the `paths` key](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#running-your-workflow-based-on-files-changed-in-a-pull-request-1): 67 | 68 | ```yaml 69 | on: 70 | push: 71 | paths: 72 | - "**.md" 73 | - .rich-codex.yml 74 | - src/myapp/cli-flags.py 75 | ``` 76 | 77 | ### Specific branches 78 | 79 | You can run on pushes to the `main` and `staging` branches only by using: 80 | 81 | ```yaml 82 | on: 83 | push: 84 | - main 85 | - staging 86 | ``` 87 | 88 | ### Manually running 89 | 90 | You can manually run the workflow by [pressing a button in the GitHub website](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow#running-a-workflow). Just use: 91 | 92 | ```yaml 93 | on: workflow_dispatch 94 | ``` 95 | 96 | ### Filtering for commit message 97 | 98 | You can filter by commit message by always running on every push, but then using an `if` statement on the job. 99 | 100 | For example, we can take the main example above and add the following to only run when the commit message includes the string `[screenshots]`: 101 | 102 | ```yaml title=".github/workflows/screenshots.yml" linenums="1" hl_lines="5" 103 | name: Rich-codex 104 | on: [push] 105 | jobs: 106 | rich_codex: 107 | if: "contains(github.event.head_commit.message, '[screenshots]')" 108 | runs-on: ubuntu-latest 109 | steps: 110 | - name: Check out the repo 111 | uses: actions/checkout@v3 112 | 113 | - name: Install your custom tools 114 | run: pip install . 115 | 116 | - name: Generate terminal images with rich-codex 117 | uses: ewels/rich-codex@v1 118 | with: 119 | commit_changes: "true" 120 | clean_img_paths: docs/img/*.svg 121 | ``` 122 | 123 | ## GitHub Action Inputs 124 | 125 | Basically everything that you can configure via the command line interface / config can also be configured within GitHub actions via the `with` key. 126 | 127 | For a full description of all available inputs, please see the [configuration overview docs](../config/overview.md). 128 | -------------------------------------------------------------------------------- /examples/action_command/cowsay-colours.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ewels/rich-codex/80de9de011c994f32274bb4cffee140567621d8e/examples/action_command/cowsay-colours.pdf -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: rich-codex 2 | repo_name: ewels/rich-codex 3 | repo_url: https://github.com/ewels/rich-codex 4 | site_url: https://ewels.github.io/rich-codex/ 5 | edit_uri: edit/main/docs/ 6 | 7 | theme: 8 | name: material 9 | icon: 10 | logo: octicons/book-16 11 | repo: fontawesome/brands/github 12 | palette: 13 | # Palette toggle for dark mode 14 | - media: "(prefers-color-scheme: dark)" 15 | scheme: slate 16 | primary: blue 17 | accent: blue 18 | toggle: 19 | icon: material/brightness-4 20 | name: Switch to light mode 21 | # Palette toggle for light mode 22 | - media: "(prefers-color-scheme: light)" 23 | scheme: default 24 | primary: blue 25 | accent: blue 26 | toggle: 27 | icon: material/brightness-7 28 | name: Switch to dark mode 29 | font: 30 | text: Noto Sans 31 | features: 32 | - navigation.instant 33 | 34 | nav: 35 | - Home: index.md 36 | - Usage: 37 | - usage/github_action.md 38 | - usage/cli.md 39 | - usage/docker_image.md 40 | - Inputs: 41 | - inputs/markdown.md 42 | - inputs/direct_inputs.md 43 | - inputs/config_file.md 44 | - Configuration: 45 | - config/overview.md 46 | - config/time_limits.md 47 | - config/tweaks.md 48 | - config/command_setup.md 49 | - config/width.md 50 | - config/colours.md 51 | - config/cleaning.md 52 | - config/ignoring_changes.md 53 | - safety.md 54 | - troubleshooting.md 55 | 56 | markdown_extensions: 57 | - admonition 58 | - pymdownx.highlight: 59 | anchor_linenums: true 60 | - pymdownx.inlinehilite 61 | - pymdownx.snippets 62 | - pymdownx.superfences 63 | - pymdownx.details 64 | plugins: 65 | - search 66 | - social 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rich-codex" 3 | description = "Generate rich images for the GitHub Action 'rich-codex'" 4 | readme = "README.md" 5 | license = "MIT" 6 | maintainers = [{ name = "Phil Ewels", email = "phil@ewels.co.uk" }] 7 | authors = [{ name = "Phil Ewels", email = "phil@ewels.co.uk" }] 8 | requires-python = ">=3.7" 9 | classifiers = [ 10 | "Development Status :: 3 - Alpha", 11 | "Intended Audience :: Developers", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3.7", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | ] 22 | dynamic = [ 23 | "version" 24 | ] 25 | dependencies = [ 26 | "rich>=12.4.3", 27 | "rich-click>=1.5", 28 | "importlib-metadata; python_version < '3.8'", 29 | "pyyaml", 30 | "levenshtein>=0.18.1", 31 | "jsonschema", 32 | "GitPython" 33 | ] 34 | [project.optional-dependencies] 35 | dev = [ 36 | "pre-commit", 37 | ] 38 | cairo = [ 39 | "CairoSVG>=2.5.2" 40 | ] 41 | 42 | [project.urls] 43 | Documentation = "https://github.com/ewels/rich-codex" 44 | "Source Code" = "https://github.com/ewels/rich-codex" 45 | "Issue Tracker" = "https://github.com/ewels/rich-codex/issues" 46 | [project.scripts] 47 | rich-codex = "rich_codex.cli:main" 48 | 49 | [build-system] 50 | build-backend = "setuptools.build_meta" 51 | requires = [ 52 | "setuptools>=45", 53 | ] 54 | 55 | [tool.setuptools.dynamic] 56 | version = { attr = "rich_codex.__version__" } 57 | 58 | [tool.setuptools.packages.find] 59 | where = ["src"] 60 | 61 | [tool.setuptools.package-data] 62 | rich_codex = ["config-schema.yml"] 63 | 64 | [tool.black] 65 | line-length=120 66 | target-version=['py37'] 67 | 68 | [tool.isort] 69 | line_length = 120 70 | multi_line_output = 3 71 | force_alphabetical_sort_within_sections = "True" 72 | force_sort_within_sections = "False" 73 | known_richclick = ["rich_click"] 74 | sections=["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER", "RICHCLICK"] 75 | profile = "black" 76 | 77 | [tool.mypy] 78 | python_version = "3.7" 79 | ignore_missing_imports = "True" 80 | scripts_are_modules = "True" 81 | -------------------------------------------------------------------------------- /src/rich_codex/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | rich-codex is a minimal Python module for generating terminal screenshots from shell commands and text snippets. 3 | 4 | It should work as a standalone command-line tool, however it is primarily intended 5 | for use with the rich-codex GitHub Action. 6 | """ 7 | 8 | __version__ = "1.2.11" 9 | 10 | from rich_codex import rich_img # noqa: F401 11 | -------------------------------------------------------------------------------- /src/rich_codex/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from rich_codex.cli import main 5 | 6 | if __name__ == "__main__": 7 | main() 8 | -------------------------------------------------------------------------------- /src/rich_codex/cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from os import getenv 4 | from sys import exit 5 | 6 | from jsonschema.exceptions import ValidationError 7 | from rich.console import Console 8 | from rich.logging import RichHandler 9 | 10 | from rich_codex import __version__, codex_search, rich_img, utils 11 | 12 | import rich_click as click 13 | 14 | click.rich_click.OPTION_ENVVAR_FIRST = True 15 | click.rich_click.ENVVAR_STRING = "[env: {}]" 16 | 17 | log = logging.getLogger() 18 | 19 | 20 | @click.command() 21 | @click.option( 22 | "--search-include", 23 | envvar="SEARCH_INCLUDE", 24 | show_envvar=True, 25 | help="Glob patterns to search for rich-codex comments", 26 | ) 27 | @click.option( 28 | "--search-exclude", 29 | envvar="SEARCH_EXCLUDE", 30 | show_envvar=True, 31 | help="Glob patterns to exclude from search for rich-codex comments", 32 | ) 33 | @click.option( 34 | "--no-search", 35 | is_flag=True, 36 | envvar="NO_SEARCH", 37 | show_envvar=True, 38 | help="Set to disable searching for rich-codex comments", 39 | ) 40 | @click.option( 41 | "--command", 42 | envvar="COMMAND", 43 | show_envvar=True, 44 | help="Specify a command to run to capture output", 45 | ) 46 | @click.option( 47 | "--timeout", 48 | type=int, 49 | default=5, 50 | envvar="TIMEOUT", 51 | show_envvar=True, 52 | show_default=True, 53 | help="Maximum run time for command (seconds)", 54 | ) 55 | @click.option( 56 | "--working-dir", 57 | envvar="WORKING_DIR", 58 | show_envvar=True, 59 | help="Working directory to run command in", 60 | ) 61 | @click.option( 62 | "--before-command", 63 | envvar="BEFORE_COMMAND", 64 | show_envvar=True, 65 | help="Setup commands to run before running main output command", 66 | ) 67 | @click.option( 68 | "--after-command", 69 | envvar="AFTER_COMMAND", 70 | show_envvar=True, 71 | help="Setup commands to run after running main output command", 72 | ) 73 | @click.option( 74 | "--snippet", 75 | envvar="SNIPPET", 76 | show_envvar=True, 77 | help="Literal code snippet to render", 78 | ) 79 | @click.option( 80 | "--snippet-syntax", 81 | envvar="SNIPPET_SYNTAX", 82 | show_envvar=True, 83 | help="Language to use for snippet sytax highlighting", 84 | ) 85 | @click.option( 86 | "--img-paths", 87 | envvar="IMG_PATHS", 88 | show_envvar=True, 89 | help="Path to image filenames if using 'command' or 'snippet'", 90 | ) 91 | @click.option( 92 | "--clean-img-paths", 93 | envvar="CLEAN_IMG_PATHS", 94 | show_envvar=True, 95 | help="Remove any matching files that are not generated", 96 | ) 97 | @click.option( 98 | "--configs", 99 | envvar="RC_CONFIGS", 100 | show_envvar=True, 101 | help="Paths to YAML config files", 102 | ) 103 | @click.option( 104 | "--fake-command", 105 | envvar="FAKE_COMMAND", 106 | show_envvar=True, 107 | help="Pretend command to show in the screenshot prompt", 108 | ) 109 | @click.option( 110 | "--hide-command", 111 | is_flag=True, 112 | envvar="HIDE_COMMAND", 113 | show_envvar=True, 114 | help="Hide the terminal prompt with the command at the top of the output", 115 | ) 116 | @click.option( 117 | "--title-command", 118 | is_flag=True, 119 | envvar="TITLE_COMMAND", 120 | show_envvar=True, 121 | help="Use the command as the terminal title if not set explicitly", 122 | ) 123 | @click.option( 124 | "--head", 125 | type=int, 126 | envvar="RC_HEAD", 127 | show_envvar=True, 128 | help="Show only the first N lines of output", 129 | ) 130 | @click.option( 131 | "--tail", 132 | type=int, 133 | envvar="RC_TAIL", 134 | show_envvar=True, 135 | help="Show only the last N lines of output", 136 | ) 137 | @click.option( 138 | "--trim-after", 139 | envvar="TRIM_AFTER", 140 | show_envvar=True, 141 | help="Don't print any more lines after this string is found", 142 | ) 143 | @click.option( 144 | "--truncated-text", 145 | default="[..truncated..]", 146 | envvar="TRUNCATED_TEXT", 147 | show_envvar=True, 148 | help="Text to show when --head or --tail truncate content", 149 | ) 150 | @click.option( 151 | "--skip-git-checks", 152 | is_flag=True, 153 | envvar="SKIP_GIT_CHECKS", 154 | show_envvar=True, 155 | help="Skip safety checks for git repos", 156 | ) 157 | @click.option( 158 | "--no-confirm", 159 | is_flag=True, 160 | envvar="NO_CONFIRM", 161 | show_envvar=True, 162 | help="Set to skip confirmation prompt before running commands", 163 | ) 164 | @click.option( 165 | "--min-pct-diff", 166 | envvar="MIN_PCT_DIFF", 167 | type=float, 168 | default=0, 169 | show_envvar=True, 170 | show_default=True, 171 | help="Minimum file percentage change required to update image", 172 | ) 173 | @click.option( 174 | "--skip-change-regex", 175 | envvar="SKIP_CHANGE_REGEX", 176 | show_envvar=True, 177 | help="Skip image update if file changes match regex", 178 | ) 179 | @click.option( 180 | "--terminal-width", 181 | envvar="TERMINAL_WIDTH", 182 | show_envvar=True, 183 | help="Width of the terminal", 184 | ) 185 | @click.option( 186 | "--terminal-min-width", 187 | type=int, 188 | default=80, 189 | envvar="TERMINAL_MIN_WIDTH", 190 | show_envvar=True, 191 | show_default=True, 192 | help="Minimum width of the terminal when trimming", 193 | ) 194 | @click.option( 195 | "--notrim", 196 | is_flag=True, 197 | envvar="NOTRIM", 198 | show_envvar=True, 199 | help="Disable automatic trimming of terminal width", 200 | ) 201 | @click.option( 202 | "--terminal-theme", 203 | envvar="TERMINAL_THEME", 204 | show_envvar=True, 205 | help="Colour theme", 206 | ) 207 | @click.option( 208 | "--snippet-theme", 209 | envvar="SNIPPET_THEME", 210 | show_envvar=True, 211 | help="Snippet Pygments theme", 212 | ) 213 | @click.option( 214 | "--use-pty", 215 | is_flag=True, 216 | envvar="USE_PTY", 217 | show_envvar=True, 218 | help="Use a pseudo-terminal for commands (may capture coloured output)", 219 | ) 220 | @click.option( 221 | "--created-files", 222 | envvar="CREATED_FILES", 223 | show_envvar=True, 224 | help="Save a list of created files to this file", 225 | metavar="", 226 | ) 227 | @click.option( 228 | "--deleted-files", 229 | envvar="DELETED_FILES", 230 | show_envvar=True, 231 | help="Save a list of deleted files to this file", 232 | metavar="", 233 | ) 234 | @click.option( 235 | "-v", 236 | "--verbose", 237 | is_flag=True, 238 | envvar="LOG_VERBOSE", 239 | show_envvar=True, 240 | help="Print verbose output to the console.", 241 | ) 242 | @click.option( 243 | "--save-log", 244 | is_flag=True, 245 | envvar="LOG_SAVE", 246 | show_envvar=True, 247 | help="Save a verbose log to a file (automatic filename).", 248 | metavar="", 249 | ) 250 | @click.option( 251 | "-l", 252 | "--log-file", 253 | envvar="LOG_FILENAME", 254 | show_envvar=True, 255 | help="Save a verbose log to a file (specific filename).", 256 | metavar="", 257 | ) 258 | def main( 259 | search_include, 260 | search_exclude, 261 | no_search, 262 | command, 263 | timeout, 264 | working_dir, 265 | before_command, 266 | after_command, 267 | snippet, 268 | snippet_syntax, 269 | img_paths, 270 | clean_img_paths, 271 | configs, 272 | fake_command, 273 | hide_command, 274 | title_command, 275 | head, 276 | tail, 277 | trim_after, 278 | truncated_text, 279 | skip_git_checks, 280 | no_confirm, 281 | min_pct_diff, 282 | skip_change_regex, 283 | terminal_width, 284 | terminal_min_width, 285 | notrim, 286 | terminal_theme, 287 | snippet_theme, 288 | use_pty, 289 | created_files, 290 | deleted_files, 291 | verbose, 292 | save_log, 293 | log_file, 294 | ): 295 | """Create rich code images for your docs.""" 296 | # Sensible defaults 297 | no_confirm = True if not no_confirm and getenv("GITHUB_ACTIONS") else no_confirm 298 | force_terminal = True if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS") else None 299 | terminal_width = int(terminal_width) if type(terminal_width) is str else terminal_width 300 | terminal_min_width = int(terminal_min_width) if type(terminal_min_width) is str else terminal_min_width 301 | saved_image_paths = [] 302 | cleaned_paths = [] 303 | num_skipped_images = 0 304 | num_saved_images = 0 305 | img_obj = None 306 | codex_obj = None 307 | 308 | # Set up the logger 309 | log.setLevel(logging.DEBUG) 310 | 311 | # Set up logs to the console 312 | log.addHandler( 313 | RichHandler( 314 | level=logging.DEBUG if verbose else logging.INFO, 315 | console=Console( 316 | stderr=True, 317 | force_terminal=force_terminal, 318 | ), 319 | show_time=False, 320 | markup=True, 321 | rich_tracebacks=True, 322 | show_path=False, 323 | tracebacks_suppress=[click], 324 | ) 325 | ) 326 | 327 | # Set up logs to a file if we asked for one 328 | if save_log and not log_file: 329 | timestamp = datetime.now().strftime("%Y.%m.%d--%H.%M.%S.%f") 330 | log_file = f"rich_codex_{timestamp}.log" 331 | 332 | if log_file: 333 | log_fh = logging.FileHandler(log_file, encoding="utf-8") 334 | log_fh.setLevel(logging.DEBUG) 335 | log_fh.setFormatter(logging.Formatter("[%(asctime)s] %(name)-20s [%(levelname)-7s] %(message)s")) 336 | log.addHandler(log_fh) 337 | 338 | log.info(f"[bold]rich-codex[/] ⚡️📖⚡️ [dim]version {__version__}[/dim]") 339 | 340 | # Check git status 341 | git_status, git_status_msg = utils.check_git_status() 342 | if skip_git_checks or git_status: 343 | log.debug(f"Git status check: {git_status_msg} (skip_git_checks: {skip_git_checks})") 344 | elif not git_status: 345 | log.error(f"[bright_red]Error with git:[/] [red]{git_status_msg}") 346 | log.info("Please resolve and run again, or use '--skip-git-checks'") 347 | exit(1) 348 | 349 | if no_confirm: 350 | log.debug("Skipping confirmation of commands") 351 | if force_terminal: 352 | log.debug("Forcing terminal logging output") 353 | if terminal_width: 354 | log.debug(f"Setting terminal width to {terminal_width}") 355 | if terminal_min_width and not notrim: 356 | log.debug(f"Trimming terminal output down to a minimum of {terminal_min_width}") 357 | if terminal_width and terminal_min_width: 358 | if terminal_min_width > terminal_width: 359 | log.error( 360 | "terminal_min_width ({}) > terminal_width ({})! Disabling terminal_min_width".format( 361 | terminal_min_width, terminal_width 362 | ) 363 | ) 364 | terminal_min_width = None 365 | 366 | # Console for printing to stdout 367 | console = Console( 368 | force_terminal=force_terminal, 369 | highlight=False, 370 | width=100 if getenv("GITHUB_ACTIONS") else None, 371 | ) 372 | 373 | # Check for mutually exclusive options 374 | if command and snippet: 375 | raise click.BadOptionUsage("--command", "Please use either --command OR --snippet but not both") 376 | if (command or snippet) and not img_paths: 377 | raise click.BadOptionUsage("--img-paths", "--img-paths is required when using --command or --snippet") 378 | 379 | # Generate image from a supplied command / snippet 380 | if command or snippet: 381 | img_obj = rich_img.RichImg( 382 | snippet_syntax=snippet_syntax, 383 | timeout=timeout, 384 | before_command=before_command, 385 | after_command=after_command, 386 | working_dir=working_dir, 387 | fake_command=fake_command, 388 | hide_command=hide_command, 389 | title_command=title_command, 390 | head=head, 391 | tail=tail, 392 | trim_after=trim_after, 393 | truncated_text=truncated_text, 394 | min_pct_diff=min_pct_diff, 395 | skip_change_regex=skip_change_regex, 396 | terminal_width=terminal_width, 397 | terminal_min_width=terminal_min_width, 398 | notrim=notrim, 399 | terminal_theme=terminal_theme, 400 | snippet_theme=snippet_theme, 401 | use_pty=use_pty, 402 | console=console, 403 | ) 404 | img_obj.no_confirm = no_confirm 405 | if command: 406 | log.info(f"Command: [white on black] {command} [/]") 407 | img_obj.command = command 408 | if snippet: 409 | log_snippet = snippet[0:30].replace("\n", " ") 410 | log.info(f"Snippet: [white on black] {log_snippet}... [/]") 411 | img_obj.snippet = snippet 412 | img_obj.img_paths = img_paths.splitlines() if img_paths else [] 413 | if img_obj.confirm_command(): 414 | img_obj.get_output() 415 | img_obj.save_images() 416 | saved_image_paths += img_obj.saved_img_paths 417 | num_saved_images += img_obj.num_img_saved 418 | num_skipped_images += img_obj.num_img_skipped 419 | 420 | # Generate images from config files 421 | 422 | # Search files for codex strings 423 | codex_obj = codex_search.CodexSearch( 424 | search_include=search_include, 425 | search_exclude=search_exclude, 426 | configs=configs, 427 | no_confirm=no_confirm, 428 | snippet_syntax=snippet_syntax, 429 | timeout=timeout, 430 | before_command=before_command, 431 | after_command=after_command, 432 | hide_command=hide_command, 433 | title_command=title_command, 434 | head=head, 435 | tail=tail, 436 | trim_after=trim_after, 437 | truncated_text=truncated_text, 438 | min_pct_diff=min_pct_diff, 439 | skip_change_regex=skip_change_regex, 440 | terminal_width=terminal_width, 441 | terminal_min_width=terminal_min_width, 442 | notrim=notrim, 443 | terminal_theme=terminal_theme, 444 | snippet_theme=snippet_theme, 445 | use_pty=use_pty, 446 | console=console, 447 | working_dir=working_dir, 448 | ) 449 | try: 450 | codex_obj.parse_configs() 451 | except ValidationError as e: 452 | log.critical(e) 453 | exit(1) 454 | if no_search: 455 | log.info("Skipping file search") 456 | else: 457 | num_errors = codex_obj.search_files() 458 | if num_errors > 0: 459 | log.error("Found errors whilst running") 460 | exit(1) 461 | codex_obj.collapse_duplicates() 462 | codex_obj.confirm_commands() 463 | codex_obj.check_duplicate_paths() 464 | codex_obj.save_all_images() 465 | saved_image_paths += codex_obj.saved_img_paths 466 | num_saved_images += codex_obj.num_img_saved 467 | num_skipped_images += codex_obj.num_img_skipped 468 | 469 | # Clean unrecognised images 470 | if clean_img_paths: 471 | cleaned_paths = utils.clean_images(clean_img_paths, img_obj, codex_obj) 472 | 473 | # Write saved file paths to disk 474 | if created_files and len(saved_image_paths): 475 | log.info(f"Saving list of new file paths to: [magenta]{created_files}[/]") 476 | with open(created_files, "w") as f: 477 | f.write("\n".join(saved_image_paths)) 478 | 479 | # Write cleaned file paths to disk 480 | if deleted_files and len(cleaned_paths): 481 | log.info(f"Saving list of deleted file paths to: [magenta]{deleted_files}[/]") 482 | with open(deleted_files, "w") as f: 483 | f.write("\n".join([str(path) for path in cleaned_paths])) 484 | 485 | if num_skipped_images > 0: 486 | log.info(f"[dim]Skipped {num_skipped_images} images 🤫") 487 | if num_saved_images > 0: 488 | log.info(f"Saved {num_saved_images} images ✨") 489 | if len(cleaned_paths) > 0: 490 | log.info(f"Deleted {len(cleaned_paths)} images 💥") 491 | if num_skipped_images == 0 and num_saved_images == 0: 492 | log.warning("Couldn't find anything to do 🙄") 493 | 494 | 495 | if __name__ == "__main__": 496 | main() 497 | -------------------------------------------------------------------------------- /src/rich_codex/config-schema.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "$schema": "http://json-schema.org/draft-04/schema#" 3 | "$id": https://github.com/ewels/rich-codex/raw/main/src/rich_codex/config-schema.yml 4 | title: rich-codex configs 5 | type: object 6 | additionalProperties: false 7 | properties: 8 | # top-level config options 9 | 10 | # Shared config options 11 | snippet_syntax: { "$ref": "#/$defs/snippet_syntax" } 12 | timeout: { "$ref": "#/$defs/timeout" } 13 | working_dir: { "$ref": "#/$defs/working_dir" } 14 | before_command: { "$ref": "#/$defs/before_command" } 15 | after_command: { "$ref": "#/$defs/after_command" } 16 | hide_command: { "$ref": "#/$defs/hide_command" } 17 | title_command: { "$ref": "#/$defs/title_command" } 18 | head: { "$ref": "#/$defs/head" } 19 | tail: { "$ref": "#/$defs/tail" } 20 | trim_after: { "$ref": "#/$defs/trim_after" } 21 | truncated_text: { "$ref": "#/$defs/truncated_text" } 22 | min_pct_diff: { "$ref": "#/$defs/min_pct_diff" } 23 | skip_change_regex: { "$ref": "#/$defs/skip_change_regex" } 24 | terminal_width: { "$ref": "#/$defs/terminal_width" } 25 | terminal_min_width: { "$ref": "#/$defs/terminal_min_width" } 26 | notrim: { "$ref": "#/$defs/notrim" } 27 | terminal_theme: { "$ref": "#/$defs/terminal_theme" } 28 | snippet_theme: { "$ref": "#/$defs/snippet_theme" } 29 | use_pty: { "$ref": "#/$defs/use_pty" } 30 | 31 | # Top-level only config options 32 | skip: 33 | title: Skip the next rich-codex image in the markdown source 34 | type: boolean 35 | 36 | # List of specific screenshots to generate 37 | outputs: 38 | title: Array of outputs to generate 39 | type: array 40 | items: 41 | title: A single output to generate 42 | type: object 43 | additionalProperties: false 44 | required: 45 | - img_paths 46 | # Need either a command or a snippet 47 | anyOf: 48 | - required: 49 | - command 50 | - required: 51 | - snippet 52 | 53 | # Additional properties needed to create an output, beyond config 54 | properties: 55 | command: 56 | title: Shell command to run 57 | type: string 58 | message: 59 | required: Either command or snippet is required 60 | snippet: 61 | title: Code snippet to use 62 | type: string 63 | message: 64 | required: Either command or snippet is required 65 | title: 66 | title: Title to use for the terminal window 67 | type: string 68 | fake_command: 69 | title: Pretend command to show in the screenshot prompt 70 | type: string 71 | extra_env: 72 | title: Additional environment variables to set for this command 73 | type: object 74 | img_paths: 75 | title: Array of image filenames to generate 76 | type: array 77 | minItems: 1 78 | items: 79 | type: string 80 | title: An image path 81 | pattern: "(?i)\\.(svg|png|pdf)$" 82 | source: 83 | title: Filename / meta about where the image came from 84 | source_type: 85 | title: Type of input that the image came from 86 | type: string 87 | 88 | # Shared config options 89 | snippet_syntax: { "$ref": "#/$defs/snippet_syntax" } 90 | timeout: { "$ref": "#/$defs/timeout" } 91 | working_dir: { "$ref": "#/$defs/working_dir" } 92 | before_command: { "$ref": "#/$defs/before_command" } 93 | after_command: { "$ref": "#/$defs/after_command" } 94 | hide_command: { "$ref": "#/$defs/hide_command" } 95 | title_command: { "$ref": "#/$defs/title_command" } 96 | head: { "$ref": "#/$defs/head" } 97 | tail: { "$ref": "#/$defs/tail" } 98 | trim_after: { "$ref": "#/$defs/trim_after" } 99 | truncated_text: { "$ref": "#/$defs/truncated_text" } 100 | min_pct_diff: { "$ref": "#/$defs/min_pct_diff" } 101 | skip_change_regex: { "$ref": "#/$defs/skip_change_regex" } 102 | terminal_width: { "$ref": "#/$defs/terminal_width" } 103 | terminal_min_width: { "$ref": "#/$defs/terminal_min_width" } 104 | notrim: { "$ref": "#/$defs/notrim" } 105 | terminal_theme: { "$ref": "#/$defs/terminal_theme" } 106 | snippet_theme: { "$ref": "#/$defs/snippet_theme" } 107 | use_pty: { "$ref": "#/$defs/use_pty" } 108 | 109 | # Single location to hold schema for config options, as used twice 110 | "$defs": 111 | snippet_syntax: 112 | title: Language to use for snippet sytax highlighting 113 | type: string 114 | timeout: 115 | title: Maximum run time for command (seconds) 116 | type: integer 117 | working_dir: 118 | title: Working directory to run command in 119 | before_command: 120 | title: Setup commands to run before running main output command 121 | type: string 122 | after_command: 123 | title: Setup commands to run after running main output command 124 | type: string 125 | hide_command: 126 | title: Hide the terminal prompt with the command at the top of the output 127 | type: boolean 128 | title_command: 129 | title: Use the command as the terminal title if not set explicitly 130 | type: boolean 131 | head: 132 | title: Show only the first N lines of output 133 | type: integer 134 | tail: 135 | title: Show only the last N lines of output 136 | type: integer 137 | trim_after: 138 | title: Don't print any more lines after this string is found 139 | type: string 140 | truncated_text: 141 | title: Text to show when --head or --tail truncate content 142 | type: string 143 | min_pct_diff: 144 | title: Minimum file percentage change required to update image 145 | type: number 146 | skip_change_regex: 147 | title: Skip image update if file changes match regex 148 | type: boolean 149 | terminal_width: 150 | title: Width of the terminal 151 | type: integer 152 | terminal_min_width: 153 | title: Minimum width of the terminal (use trimming) 154 | type: integer 155 | notrim: 156 | title: Disable automatic trimming of terminal width 157 | type: boolean 158 | terminal_theme: 159 | title: Colour theme 160 | type: string 161 | snippet_theme: 162 | title: Snippet Pygments theme 163 | type: string 164 | use_pty: 165 | title: Use a pseudo-terminal for commands (may capture coloured output) 166 | type: boolean 167 | -------------------------------------------------------------------------------- /src/rich_codex/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from git import Repo 5 | from git.exc import InvalidGitRepositoryError 6 | from jsonschema import Draft4Validator 7 | from jsonschema.exceptions import ValidationError 8 | 9 | log = logging.getLogger("rich-codex") 10 | 11 | 12 | def clean_images(clean_img_paths_raw, img_obj, codex_obj): 13 | """Delete any images matching CLEAN_IMG_PATHS that were not generated. 14 | 15 | Useful to remove existing files when a target filename is changed. 16 | """ 17 | clean_img_patterns = clean_img_paths_raw.splitlines() if clean_img_paths_raw else [] 18 | 19 | if len(clean_img_patterns) == 0: 20 | log.debug("[dim]Nothing found to clean in 'clean_img_paths'") 21 | return [] 22 | 23 | # Search glob patterns for images 24 | all_img_paths = set() 25 | for pattern in clean_img_patterns: 26 | for matched_path in Path.cwd().glob(pattern): 27 | all_img_paths.add(matched_path.resolve()) 28 | if len(all_img_paths) == 0: 29 | log.debug("[dim]No files found matching 'clean_img_paths' glob patterns") 30 | return [] 31 | 32 | # Collect list of generated images 33 | known_img_paths = set() 34 | if img_obj: 35 | for img_path in img_obj.img_paths: 36 | known_img_paths.add(Path(img_path).resolve()) 37 | if codex_obj: 38 | for img in codex_obj.rich_imgs: 39 | for img_path in img.img_paths: 40 | known_img_paths.add(Path(img_path).resolve()) 41 | 42 | # Paths found by glob that weren't generated 43 | clean_img_paths = all_img_paths - known_img_paths 44 | if len(clean_img_paths) == 0: 45 | log.debug("[dim]All files found matching 'clean_img_paths' were generated in this run. Nothing to clean.") 46 | return [] 47 | 48 | for path in clean_img_paths: 49 | path_to_delete = Path(path).resolve() 50 | path_relative = path_to_delete.relative_to(Path.cwd()) 51 | log.info(f"Deleting '{path_relative}'") 52 | path_to_delete.unlink() 53 | 54 | return clean_img_paths 55 | 56 | 57 | def check_git_status(): 58 | """Check if the working directory is a clean git repo.""" 59 | try: 60 | repo = Repo(Path.cwd().resolve(), search_parent_directories=True) 61 | if repo.is_dirty(untracked_files=True): 62 | changedFiles = [item.a_path for item in repo.index.diff(None)] 63 | return (False, f"Found uncommitted changes: {changedFiles + repo.untracked_files}") 64 | except InvalidGitRepositoryError: 65 | return (False, "Does not appear to be a git repository") 66 | return (True, "Git repo looks good.") 67 | 68 | 69 | def validate_config(schema, config, filename, line_number=None): 70 | """Validate a config file string against the rich-codex JSON schema.""" 71 | ln_text = f"line {line_number} " if line_number else "" 72 | v = Draft4Validator(schema) 73 | if not v.is_valid(config): 74 | err_msg = f"[red][✗] Rich-codex config in '{filename}' {ln_text}was invalid" 75 | 76 | for error in sorted(v.iter_errors(config), key=str): 77 | err_msg += f"\n - {error.message}" 78 | if len(error.context): 79 | err_msg += ":" 80 | for suberror in sorted(error.context, key=lambda e: e.schema_path): 81 | err_msg += f"\n * {suberror.message}" 82 | raise ValidationError(err_msg, v) 83 | --------------------------------------------------------------------------------