├── .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 | [](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 | 
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 
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: ``  ``
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 | 
36 |
37 | `MONOKAI`:
38 |
39 |
40 |
41 | 
42 |
43 | `DIMMED_MONOKAI`:
44 |
45 |
46 |
47 | 
48 |
49 | `NIGHT_OWLISH`:
50 |
51 |
52 |
53 | 
54 |
55 | `SVG_EXPORT_THEME`:
56 |
57 |
58 |
59 | 
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 | 
93 |
94 | `snippet_theme: monokai` + `terminal_theme: SVG_EXPORT_THEME`:
95 |
96 |
116 |
117 | 
118 |
119 | `snippet_theme: fruity` + `terminal_theme: MONOKAI`:
120 |
121 |
141 |
142 | 
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 | 
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 | 
36 | ```
37 | 
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 | 
64 | ```
65 | 
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 | 
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 | 
22 | ```
23 | 
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 | 
36 | ```
37 | 
38 |
39 | With `hide_command` set to `true`:
40 |
41 | ```markdown
42 |
43 | 
44 | ```
45 |
46 | 
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 | 
61 | ```
62 | 
63 |
64 | With `title_command` set to `true`:
65 |
66 | ```markdown
67 |
68 | 
69 | ```
70 | 
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 | 
85 | ```
86 | 
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 | 
110 | ```
111 | 
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 |
63 |
--------------------------------------------------------------------------------
/docs/img/cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ewels/rich-codex/80de9de011c994f32274bb4cffee140567621d8e/docs/img/cat.png
--------------------------------------------------------------------------------
/docs/img/fake_command.svg:
--------------------------------------------------------------------------------
1 |
67 |
--------------------------------------------------------------------------------
/docs/img/git-pull-rebase.svg:
--------------------------------------------------------------------------------
1 |
103 |
--------------------------------------------------------------------------------
/docs/img/git-push-error.svg:
--------------------------------------------------------------------------------
1 |
101 |
--------------------------------------------------------------------------------
/docs/img/git-push-success.svg:
--------------------------------------------------------------------------------
1 |
103 |
--------------------------------------------------------------------------------
/docs/img/head-tail.svg:
--------------------------------------------------------------------------------
1 |
136 |
--------------------------------------------------------------------------------
/docs/img/hide-command-default.svg:
--------------------------------------------------------------------------------
1 |
63 |
--------------------------------------------------------------------------------
/docs/img/hide-command.svg:
--------------------------------------------------------------------------------
1 |
60 |
--------------------------------------------------------------------------------
/docs/img/rich-codex-snippet.svg:
--------------------------------------------------------------------------------
1 |
140 |
--------------------------------------------------------------------------------
/docs/img/snippet-syntax.svg:
--------------------------------------------------------------------------------
1 |
88 |
--------------------------------------------------------------------------------
/docs/img/theme-default_terminal_theme.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/docs/img/theme-dimmed_monokai.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/docs/img/theme-monokai.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/docs/img/theme-night_owlish.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/docs/img/theme-svg_export_theme.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/docs/img/title-command-default.svg:
--------------------------------------------------------------------------------
1 |
63 |
--------------------------------------------------------------------------------
/docs/img/title-command.svg:
--------------------------------------------------------------------------------
1 |
63 |
--------------------------------------------------------------------------------
/docs/img/trim-after_truncated-text.svg:
--------------------------------------------------------------------------------
1 |
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 | [](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 | 
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 
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: ``  ``
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 |
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 | 
16 | ```
17 |
18 | 
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 | 
32 | ```
33 |
34 | 
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 | 
57 | ```
58 |
59 |
60 | 
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 | 
76 | ```
77 |
78 |
79 | 
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 | 
112 | ```
113 | 
114 |
115 | For other code languages, use `snippet_syntax` to define which language to format in. For example:
116 |
117 | ```markdown
118 |
134 | 
135 | ```
136 | 
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 ``  `` - 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 | 
43 |
44 |
58 |
59 | 
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 | 
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 |
--------------------------------------------------------------------------------