├── out └── fetch.png ├── requirements.txt ├── config.json ├── main.py ├── src ├── draw_ascii.py ├── fetch_info.py └── gen_readme.py ├── LICENSE ├── .github └── workflows │ └── build.yaml ├── README.md └── .gitignore /out/fetch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pruefsumme/readmefetch/HEAD/out/fetch.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pruefsumme/readmefetch/HEAD/requirements.txt -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_stats": [ 3 | "username", 4 | "bio", 5 | "location", 6 | "company", 7 | "email", 8 | "hireable", 9 | "followers", 10 | "following", 11 | "public_repos", 12 | "public_gists", 13 | "total_stars", 14 | "bytes_of_code", 15 | "created_at", 16 | "updated_at", 17 | "languages", 18 | "total_commits", 19 | "total_issues", 20 | "total_prs" 21 | ], 22 | "additional_info": "", 23 | "preferred_color": "lightblue", 24 | "max_languages": 5, 25 | "append_automatic": true, 26 | "exclude_orgainzations": true 27 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | from dotenv import load_dotenv 5 | from github import Github 6 | from src.gen_readme import generate_fetch, generate_readme, gen_image 7 | 8 | def main(): 9 | try: 10 | load_dotenv() 11 | token = os.getenv("GH_TOKEN") 12 | if not token: 13 | raise ValueError("GH_TOKEN environment variable not set") 14 | 15 | g = Github(token) 16 | generate_readme(g) 17 | print("✨ README updated successfully! Wahooo!") 18 | return 0 19 | 20 | except Exception as e: 21 | 22 | print(f"❌ {e}", file=sys.stderr) 23 | traceback.print_exc(file=sys.stderr) 24 | return 1 25 | 26 | if __name__ == "__main__": 27 | sys.exit(main()) -------------------------------------------------------------------------------- /src/draw_ascii.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from github import Github 3 | from PIL import Image 4 | from io import BytesIO 5 | from src.fetch_info import fetch_stats 6 | 7 | def get_ascii_char(pixel): 8 | ascii_chars = '.:-=+*#%@' 9 | brightness = sum(pixel)/3 10 | char_index = min(int(brightness/255 * (len(ascii_chars)-1)), len(ascii_chars)-1) 11 | return ascii_chars[char_index] 12 | 13 | def image_to_ascii(image, width=50) -> str: 14 | aspect_ratio = image.width / image.height 15 | height = int((width*aspect_ratio)*0.5) 16 | 17 | image = image.resize((width, height)) 18 | image = image.convert('RGB') 19 | ascii_str = "" 20 | 21 | for y in range(height): 22 | for x in range(width): 23 | pixel = image.getpixel((x,y)) 24 | ascii_str += get_ascii_char(pixel) 25 | ascii_str += "\n" 26 | 27 | return ascii_str 28 | 29 | def generate_logo(g:Github) -> str: 30 | user_pfp = g.get_user().avatar_url 31 | response = requests.get(user_pfp) 32 | img = Image.open(BytesIO(response.content)) 33 | 34 | return image_to_ascii(img) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 br0sinski 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 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Start Workflow 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */24 * * *' 6 | workflow_dispatch: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Setup Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -r requirements.txt 26 | 27 | - name: Run update script 28 | env: 29 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 30 | run: python main.py 31 | 32 | - name: Commit and push changes 33 | env: 34 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 35 | run: | 36 | git config --global user.name 'GitHub Actions' 37 | git config --global user.email 'actions@github.com' 38 | git add README.md out/fetch.png 39 | git diff --quiet && git diff --staged --quiet || git commit -m "Update README" 40 | git push https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # readmefetch 3 | 4 | **readmefetch** This GitHub Workflow script, written in Python, automatically generates a neofetch-like README based on your GitHub account stats :D 5 | 6 | ## 🚀 Setup 7 | 8 | ### 1. Repository Setup 9 | 1. **Fork this repository**. 10 | 2. **Rename the fork** to match your GitHub username (e.g., `username/username`). 11 | 12 | ### 2. Create a GitHub Token 13 | 1. Go to [GitHub Settings → Developer Settings → Personal Access Tokens → Tokens (classic)](https://github.com/settings/tokens). 14 | 2. Click **"Generate new token (classic)"**. 15 | 3. **Name**: `whatever you want (e.g Readmefetch Token)`. 16 | 4. **Expiration**: Choose as needed. Never recommended, otherwise it won't automatically update 17 | 5. **Select scopes**: `workflow`. 18 | 6. Click **"Generate"** and **copy the token**! You will never see it again! 19 | 20 | ### 3. Configure the Repository 21 | 1. Go to your fork's **"Settings" → "Secrets and variables" → "Actions"**. 22 | 2. Create a new repository secret: 23 | - **Name**: `GH_TOKEN` 24 | - **Value**: Your copied token 25 | 26 | ### 4. Enable Actions 27 | 1. Go to the **"Actions"** tab. 28 | 2. Click **"Enable workflow"**. 29 | 3. Click **"Run workflow"**. 30 | 31 | ⚠️ **Note**: It might take a couple of seconds until your new README appears! 😊 32 | 33 | ## ⚙️ Configuration 34 | 35 | Customize your profile by editing `config.json`: 36 | Remove keys to exclude them from your README. 37 | 38 | ```json 39 | { 40 | "display_stats": [ 41 | "username", 42 | "bio", 43 | "location", 44 | "company", 45 | "email", 46 | "hireable", 47 | "followers", 48 | "following", 49 | "public_repos", 50 | "public_gists", 51 | "total_stars", 52 | "lines_of_code", 53 | "created_at", 54 | "updated_at", 55 | "languages", 56 | "total_commits", 57 | "total_issues", 58 | "total_prs" 59 | ], 60 | "additional_info": "", // some additional information you can provide :) 61 | "preferred_color": "lightblue", // color it will be printed out in 62 | "max_languages": 5, // number of max. biggest languages displayed in the fetch 63 | "append_autmatic": true // appends the embedded image automatically if not found in README 64 | } 65 | ``` 66 | ### Editing the README 67 | 68 | After the initial deployment, you can edit the `README.md` file however you want! 69 | 70 | ## Disclaimer 71 | 72 | Use this script at your own risk. The software is provided "as is", without warranty of any kind, express or implied, as stated in the MIT License. 73 | 74 | ## Example Output 75 | 76 | 77 | 78 |
79 | Github Fetch 80 |
81 | -------------------------------------------------------------------------------- /src/fetch_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from dotenv import load_dotenv 4 | from github import Github 5 | from collections import Counter 6 | 7 | with open("config.json", "r") as f: 8 | config = json.load(f) 9 | 10 | with open('config.json') as config_file: 11 | config = json.load(config_file) 12 | 13 | def get_repos(g: Github): 14 | exclude_organizations = config.get("exclude_organizations", True) 15 | repos = [repo for repo in g.get_user().get_repos(type="public") if repo.visibility == "public"] 16 | if exclude_organizations: 17 | repos = [repo for repo in repos if repo.owner.type != "Organization"] 18 | return repos 19 | 20 | def get_lines_of_code(g: Github) -> int: 21 | total_lines = 0 22 | for repo in get_repos(g): 23 | if repo.visibility == "public": # Double check visibility again! 24 | try: 25 | languages = repo.get_languages() 26 | total_lines += sum(languages.values()) 27 | except Exception: 28 | continue 29 | return total_lines 30 | 31 | def get_languages(g: Github) -> dict: 32 | languages = Counter() 33 | for repo in get_repos(g): 34 | if not repo.fork and repo.visibility == "public": # Double check visibility again! 35 | try: 36 | for lang, bytes_count in repo.get_languages().items(): 37 | languages[lang] += bytes_count 38 | except Exception: 39 | continue 40 | return dict(languages) 41 | 42 | def format_languages(languages: dict) -> str: 43 | sorted_lang = sorted(languages.items(), key=lambda x: x[1], reverse=True) 44 | max_languages = config.get("max_languages", -1) 45 | if max_languages != -1: 46 | sorted_lang = sorted_lang[:max_languages] 47 | return '\n' + '\n'.join([f"- {lang}: {bytes_count} bytes of code" for lang, bytes_count in sorted_lang]) # The GitHUB API returns the bytes of code written in a language, not the lines of code 48 | 49 | def fetch_stats(g: Github) -> dict: 50 | user = g.get_user() 51 | total_commits = 0 52 | total_issues = 0 53 | total_prs = 0 54 | 55 | for repo in get_repos(g): 56 | if not repo.fork and repo.visibility == "public": # Check both fork and visibility 57 | try: 58 | total_commits += repo.get_commits().totalCount 59 | total_issues += repo.get_issues().totalCount 60 | total_prs += repo.get_pulls().totalCount 61 | except Exception: 62 | continue 63 | 64 | return { 65 | "username": user.login, 66 | "followers": user.followers, 67 | "following": user.following, 68 | "public_repos": user.public_repos, 69 | "public_gists": user.public_gists, 70 | "total_stars": sum([repo.stargazers_count for repo in get_repos(g)]), 71 | "bytes_of_code": get_lines_of_code(g), 72 | "bio": user.bio, 73 | "location": user.location, 74 | "company": user.company, 75 | "email": user.email, 76 | "website": user.blog, 77 | "hireable": user.hireable, 78 | "created_at": user.created_at.strftime("%d-%m-%Y"), 79 | "updated_at": user.updated_at.strftime("%d-%m-%Y"), 80 | "languages": format_languages(get_languages(g)), 81 | "total_commits": total_commits, 82 | "total_issues": total_issues, 83 | "total_prs": total_prs, 84 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /src/gen_readme.py: -------------------------------------------------------------------------------- 1 | import json, os, re 2 | from src.draw_ascii import generate_logo 3 | from src.fetch_info import fetch_stats 4 | from PIL import Image, ImageDraw, ImageFont 5 | from github import Github 6 | 7 | def generate_fetch(g:Github) -> str: 8 | with open("config.json", "r") as f: 9 | config = json.load(f) 10 | 11 | user = fetch_stats(g) 12 | pfp = generate_logo(g) 13 | 14 | 15 | stats = f"{user['username']}@github.com\n------------------------------\n" 16 | for stat in config['display_stats']: 17 | if stat in user: 18 | stats += f"{stat.replace('_', ' ').title()}: {user[stat]}\n" 19 | stats += f"\n{config['additional_info']}\n" 20 | 21 | pfp_lines = pfp.split("\n") 22 | stats_lines = stats.split("\n") 23 | 24 | max_lines = max(len(pfp_lines), len(stats_lines)) 25 | pfp_lines += [""] * (max_lines - len(pfp_lines)) 26 | stats_lines += [""] * (max_lines - len(stats_lines)) 27 | 28 | combined = "\n".join(f"{pfp_line:<50} {stats_line}" for pfp_line, stats_line in zip(pfp_lines, stats_lines)) 29 | 30 | return combined 31 | 32 | def return_preffered_color() -> tuple: 33 | with open("config.json", "r") as f: 34 | config = json.load(f) 35 | 36 | color = config['preferred_color'] 37 | color_map = { 38 | "red": (255, 0, 0), 39 | "green": (0, 128, 0), 40 | "blue": (0, 0, 255), 41 | "yellow": (255, 255, 0), 42 | "purple": (128, 0, 128), 43 | "orange": (255, 165, 0), 44 | "pink": (255, 192, 203), 45 | "white": (255, 255, 255), 46 | "lightblue": (173, 216, 230), 47 | } 48 | 49 | if color in color_map: 50 | return color_map[color] 51 | else: 52 | return color_map["lightblue"] 53 | 54 | 55 | def gen_image(g: Github): 56 | width, initial_height = 1200, 550 57 | ascii_width = 450 58 | text_margin = 60 59 | 60 | bg_color = (0, 0, 0) 61 | value_color = return_preffered_color() 62 | text_color = (255, 255, 255) 63 | font_size = 16 64 | 65 | font = None 66 | font_paths = [ 67 | "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 68 | "/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf", 69 | "/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf", 70 | "monospace", 71 | "consola.ttf" 72 | ] 73 | 74 | fetch = generate_fetch(g) 75 | image = Image.new("RGB", (width, initial_height), bg_color) 76 | draw = ImageDraw.Draw(image) 77 | for font_path in font_paths: 78 | try: 79 | font = ImageFont.truetype(font_path, font_size) 80 | break 81 | except IOError: 82 | continue 83 | 84 | if font is None: 85 | print("No suitable fonts found. Aborting!") 86 | return 87 | 88 | 89 | lines = fetch.split("\n") 90 | ascii_lines = [line[:50] for line in lines] 91 | info_lines = [line[50:].strip() for line in lines] 92 | 93 | y_offset = 10 94 | line_spacing = font_size + 4 95 | for ascii_line in ascii_lines: 96 | draw.text((10, y_offset), ascii_line, fill=value_color, font=font) 97 | y_offset += line_spacing 98 | 99 | y_offset = 10 100 | x_text = ascii_width + text_margin 101 | max_text_width = width - ascii_width - (text_margin * 2) 102 | 103 | for info_line in info_lines: 104 | if info_line: 105 | parts = info_line.split(':', 1) 106 | if len(parts) == 2: 107 | title = parts[0] + ':' 108 | value = parts[1].strip() 109 | 110 | title_width = font.getlength(title) 111 | draw.text((x_text, y_offset), title, fill=value_color, font=font) 112 | 113 | x_value = x_text + title_width + 5 114 | remaining_width = max_text_width - title_width - 5 115 | 116 | words = value.split() 117 | line = [] 118 | x_current = x_value 119 | 120 | for word in words: 121 | test_line = ' '.join(line + [word]) 122 | text_width = font.getlength(test_line) 123 | 124 | if text_width <= remaining_width: 125 | line.append(word) 126 | else: 127 | if line: 128 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 129 | y_offset += line_spacing 130 | line = [word] 131 | x_current = x_text + text_margin 132 | else: 133 | draw.text((x_current, y_offset), word, fill=text_color, font=font) 134 | y_offset += line_spacing 135 | if line: 136 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 137 | y_offset += line_spacing 138 | else: 139 | words = info_line.split() 140 | line = [] 141 | x_current = x_text 142 | 143 | for word in words: 144 | test_line = ' '.join(line + [word]) 145 | text_width = font.getlength(test_line) 146 | 147 | if text_width <= max_text_width: 148 | line.append(word) 149 | else: 150 | if line: 151 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 152 | y_offset += line_spacing 153 | line = [word] 154 | x_current = x_text 155 | else: 156 | draw.text((x_current, y_offset), word, fill=text_color, font=font) 157 | y_offset += line_spacing 158 | x_current = x_text 159 | 160 | if line: 161 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 162 | y_offset += line_spacing 163 | 164 | # Check if the text goes out of bounds and adjust the image height if necessary 165 | if y_offset > initial_height: 166 | new_height = y_offset + 20 167 | image = Image.new("RGB", (width, new_height), bg_color) 168 | draw = ImageDraw.Draw(image) 169 | 170 | # Redraw the text on the new image 171 | y_offset = 10 172 | for ascii_line in ascii_lines: 173 | draw.text((10, y_offset), ascii_line, fill=value_color, font=font) 174 | y_offset += line_spacing 175 | 176 | y_offset = 10 177 | for info_line in info_lines: 178 | if info_line: 179 | parts = info_line.split(':', 1) 180 | if len(parts) == 2: 181 | title = parts[0] + ':' 182 | value = parts[1].strip() 183 | 184 | title_width = font.getlength(title) 185 | draw.text((x_text, y_offset), title, fill=value_color, font=font) 186 | 187 | x_value = x_text + title_width + 5 188 | remaining_width = max_text_width - title_width - 5 189 | 190 | words = value.split() 191 | line = [] 192 | x_current = x_value 193 | 194 | for word in words: 195 | test_line = ' '.join(line + [word]) 196 | text_width = font.getlength(test_line) 197 | 198 | if text_width <= remaining_width: 199 | line.append(word) 200 | else: 201 | if line: 202 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 203 | y_offset += line_spacing 204 | line = [word] 205 | x_current = x_text + text_margin 206 | else: 207 | draw.text((x_current, y_offset), word, fill=text_color, font=font) 208 | y_offset += line_spacing 209 | if line: 210 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 211 | y_offset += line_spacing 212 | else: 213 | words = info_line.split() 214 | line = [] 215 | x_current = x_text 216 | 217 | for word in words: 218 | test_line = ' '.join(line + [word]) 219 | text_width = font.getlength(test_line) 220 | 221 | if text_width <= max_text_width: 222 | line.append(word) 223 | else: 224 | if line: 225 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 226 | y_offset += line_spacing 227 | line = [word] 228 | x_current = x_text 229 | else: 230 | draw.text((x_current, y_offset), word, fill=text_color, font=font) 231 | y_offset += line_spacing 232 | x_current = x_text 233 | 234 | if line: 235 | draw.text((x_current, y_offset), ' '.join(line), fill=text_color, font=font) 236 | y_offset += line_spacing 237 | 238 | # prompt_y = new_height - line_spacing if y_offset > initial_height else initial_height - line_spacing 239 | # x_prompt = 10 240 | # draw.text((x_prompt, prompt_y), g.get_user().login, fill=value_color, font=font) 241 | # x_prompt += font.getlength(g.get_user().login) 242 | # draw.text((x_prompt, prompt_y), "@", fill=(255,255,255), font=font) 243 | # x_prompt += font.getlength("@") 244 | # draw.text((x_prompt, prompt_y), "github", fill=value_color, font=font) 245 | # x_prompt += font.getlength("githubdotcom") 246 | # draw.text((x_prompt, prompt_y), ": $", fill=text_color, font=font) 247 | 248 | os.makedirs("out", exist_ok=True) 249 | image.save("out/fetch.png") 250 | #image.show() 251 | 252 | def generate_readme(g: Github): 253 | gen_image(g) 254 | 255 | image_pattern = r'
\s*\'Github\s*
' 256 | image_content = "\n## Example Output\n
\n Github Fetch\n
\n" 257 | 258 | try: 259 | with open("README.md", "r", encoding="utf-8") as f: 260 | content = f.read() 261 | 262 | start_comment = "" 263 | end_comment = "" 264 | pattern = re.compile(f"{start_comment}.*?{end_comment}", re.DOTALL) 265 | content = re.sub(pattern, "", content) 266 | 267 | with open("config.json", "r") as f: 268 | config = json.load(f) 269 | append_automatic = config.get("append_automatic", True) 270 | 271 | if append_automatic and not re.search(image_pattern, content): 272 | content = content.rstrip() + "\n\n" + image_content 273 | except FileNotFoundError: 274 | content = image_content 275 | 276 | with open("README.md", "w", encoding="utf-8") as f: 277 | f.write(content) --------------------------------------------------------------------------------