├── 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 |

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*

\s*
'
256 | image_content = "\n## Example Output\n\n

\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)
--------------------------------------------------------------------------------