diff --git a/.gitignore b/.gitignore index 485dee6..9bfbf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,37 @@ .idea + +# Environment variables +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Temporary files +*.tmp +*.temp +.venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..5239d89 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Homebrew Fonts Tap + +This repository contains a collection of custom fonts for use with Homebrew. + +## Installation + +```bash +brew tap trtmn-fonts/fonts +brew install font- +``` + +## Available Fonts + +This tap provides a variety of custom fonts. To see all available fonts, run: + +```bash +brew search trtmn-fonts/fonts/font- +``` + +## Adding New Fonts + +To add a new font to this tap, follow these steps: + +1. **Create a Gitea API Token**: + - Go to your Gitea profile settings + - Navigate to "Applications" > "Generate New Token" + - Name the token (e.g., "Homebrew Fonts") + - Select the necessary permissions (at minimum: `repo`) + - Generate and copy the token + +2. **Set Up Environment Variables**: + - Create a `.env` file in the root of this repository with the following content: + ``` + GITEA_URL=http://clancy.genet-godzilla.ts.net:3002 + GITEA_USERNAME=trtmn-fonts + GITEA_API_TOKEN=your_api_token_here + ``` + - Replace `your_api_token_here` with your actual Gitea API token + - **Important**: Do not commit the `.env` file to version control if it contains sensitive information + +3. **Install Required Python Packages**: + ```bash + pip install -r requirements.txt + ``` + +4. **Run the Add Font Script**: + ```bash + python3 add-font-submodule.py + ``` + - Follow the prompts to enter the font name and location of the font files + - The script will: + - Create a new repository on Gitea for the font + - Add the font as a submodule to this repository + - Create a Homebrew formula for the font + - Update the font index + +5. **Push Changes to Gitea**: + ```bash + git push + ``` + +## Removing Fonts + +To remove a font from this tap, follow these steps: + +1. **Ensure Environment Variables are Set**: + - Make sure your `.env` file is properly configured (see "Adding New Fonts" section) + +2. **Run the Remove Font Script**: + ```bash + python3 remove-font.py + ``` + - Replace `` with the name of the font to remove + - The script will: + - Ask for confirmation before proceeding + - Remove the font submodule + - Delete the font repository from Gitea + - Update the font index + +3. **Push Changes to Gitea**: + ```bash + git push + ``` + +## Repository Structure + +- `Formula/`: Contains Homebrew formulas for each font +- `fonts/`: Contains submodules for each font repository +- `fonts/index.json`: Index of all available fonts +- `add-font-submodule.py`: Script for adding new fonts +- `remove-font.py`: Script for removing fonts +- `.env`: Configuration file for Gitea credentials (not tracked by git) +- `requirements.txt`: Python dependencies + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/add-font-submodule.py b/add-font-submodule.py new file mode 100755 index 0000000..120ef87 --- /dev/null +++ b/add-font-submodule.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +import os +import sys +import shutil +import subprocess +import hashlib +import zipfile +import tempfile +import json +import requests +from pathlib import Path +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +def create_gitea_repo(gitea_url, gitea_username, gitea_token, repo_name, description): + """Create a new repository on Gitea using the API.""" + api_url = f"{gitea_url}/api/v1/user/repos" + headers = { + "Authorization": f"token {gitea_token}", + "Content-Type": "application/json" + } + data = { + "name": repo_name, + "description": description, + "private": False, + "auto_init": False + } + + print(f"Creating repository {repo_name} on Gitea...") + response = requests.post(api_url, headers=headers, json=data) + + if response.status_code == 201: + print(f"Repository {repo_name} created successfully.") + return response.json() + else: + print(f"Error creating repository: {response.status_code} - {response.text}") + return None + +def create_zip(font_files, zip_path): + """Create a zip file with the font files.""" + print(f"Creating zip file at {zip_path} with the following font files:") + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for font_file in font_files: + print(f" - {font_file}") + # Add each font file to the zip with its original name (no path) + arcname = os.path.basename(font_file) + zipf.write(font_file, arcname=arcname) + print(f"Zip file created at {zip_path}") + +def generate_sha256(zip_path): + """Generate SHA256 hash for the zip file.""" + sha256_hash = hashlib.sha256() + with open(zip_path, "rb") as f: + # Read in 4k chunks to hash + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + +def create_font_repo(font_name, font_files, gitea_url, gitea_username, gitea_token): + """Create a new repository for the font and set it up with the necessary files.""" + # Create a temporary directory for the font repository + with tempfile.TemporaryDirectory() as temp_dir: + # Create the zip file + zip_filename = f"{font_name}.zip" + zip_path = os.path.join(temp_dir, zip_filename) + create_zip(font_files, zip_path) + + # Generate SHA256 hash for the zip file + sha256_hash = generate_sha256(zip_path) + print(f"SHA256 hash generated: {sha256_hash}") + + # Create the repository on Gitea + repo_name = f"font-{font_name}" + description = f"Font files for {font_name}" + repo_data = create_gitea_repo(gitea_url, gitea_username, gitea_token, repo_name, description) + + if not repo_data: + print("Failed to create repository on Gitea.") + return None, None, None + + # Clone the repository + repo_url = repo_data["clone_url"] + repo_path = os.path.join(temp_dir, repo_name) + subprocess.run(["git", "clone", repo_url, repo_path], check=True) + + # Copy the zip file to the repository + shutil.copy(zip_path, os.path.join(repo_path, zip_filename)) + + # Create a README.md file + with open(os.path.join(repo_path, "README.md"), "w") as f: + f.write(f"# {font_name} Font\n\n") + f.write(f"This repository contains the font files for {font_name}.\n\n") + f.write(f"SHA256: {sha256_hash}\n") + + # Commit and push the changes + subprocess.run(["git", "-C", repo_path, "add", "."], check=True) + subprocess.run(["git", "-C", repo_path, "commit", "-m", f"Add {font_name} font files"], check=True) + subprocess.run(["git", "-C", repo_path, "push"], check=True) + + # Create a local copy of the zip file for the formula + local_zip_path = os.path.join(os.getcwd(), zip_filename) + shutil.copy(zip_path, local_zip_path) + + return repo_path, sha256_hash, zip_filename + +def create_formula(font_name, sha256_hash, gitea_url, gitea_username, zip_filename): + """Create the Homebrew formula for the font.""" + # Get the font filename without extension (assuming it's a valid font file in the zip) + font_file = os.path.basename(font_files[0]) # Assumes the first font file is the primary font file + + # Convert font_name to CamelCase for the class name + class_name = ''.join(word.capitalize() for word in font_name.split('-')) + + # Create the formula content + formula_content = f"""class {class_name} < Formula + desc "A custom font" + homepage "{gitea_url}/{gitea_username}/font-{font_name}" + url "{gitea_url}/{gitea_username}/font-{font_name}/raw/branch/master/{zip_filename}" + sha256 "{sha256_hash}" + version "1.0.0" + + def install + (share/"fonts").install "{font_file}" + end + + test do + # Test installation by checking that the font exists + system "fc-list | grep '{font_file.split('.')[0]}'" + end +end +""" + + # Create the formula directory if it doesn't exist + formula_dir = os.path.join(os.getcwd(), "Formula") + os.makedirs(formula_dir, exist_ok=True) + + # Write the formula to a file + formula_path = os.path.join(formula_dir, f"font-{font_name}.rb") + with open(formula_path, "w") as f: + f.write(formula_content) + + print(f"Formula created at {formula_path}") + return formula_path + +def add_submodule(font_name, gitea_url, gitea_username): + """Add the font repository as a submodule.""" + # Create the fonts directory if it doesn't exist + fonts_dir = os.path.join(os.getcwd(), "fonts") + os.makedirs(fonts_dir, exist_ok=True) + + # Add the submodule + submodule_path = os.path.join(fonts_dir, font_name) + repo_url = f"{gitea_url}/{gitea_username}/font-{font_name}.git" + + # Check if the submodule already exists + if os.path.exists(submodule_path): + print(f"Submodule {submodule_path} already exists. Removing it first.") + subprocess.run(["git", "submodule", "deinit", "-f", submodule_path], check=True) + subprocess.run(["git", "rm", "-f", submodule_path], check=True) + subprocess.run(["rm", "-rf", f".git/modules/{submodule_path}"], check=True) + + # Add the submodule + print(f"Adding submodule {submodule_path}...") + subprocess.run(["git", "submodule", "add", repo_url, submodule_path], check=True) + + return submodule_path + +def update_submodule_index(font_name, submodule_path): + """Update the submodule index file.""" + # Create the index file if it doesn't exist + index_path = os.path.join(os.getcwd(), "fonts", "index.json") + if not os.path.exists(index_path): + with open(index_path, "w") as f: + json.dump({"fonts": []}, f, indent=2) + + # Read the index file + with open(index_path, "r") as f: + index = json.load(f) + + # Add the font to the index + font_info = { + "name": font_name, + "path": submodule_path, + "formula": f"font-{font_name}" + } + + # Check if the font is already in the index + for i, font in enumerate(index.get("fonts", [])): + if font["name"] == font_name: + index["fonts"][i] = font_info + break + else: + # Add the font to the index + if "fonts" not in index: + index["fonts"] = [] + index["fonts"].append(font_info) + + # Write the updated index back to the file + with open(index_path, "w") as f: + json.dump(index, f, indent=2) + + print(f"Index updated at {index_path}") + +def main(): + # Get the Gitea URL from environment variable or prompt + gitea_url = os.getenv("GITEA_URL") + if not gitea_url: + gitea_url = input("Enter your Gitea URL (e.g., http://clancy.genet-godzilla.ts.net:3002): ") + + # Get the Gitea username from environment variable or prompt + gitea_username = os.getenv("GITEA_USERNAME") + if not gitea_username: + gitea_username = input("Enter your Gitea username: ") + + # Get the Gitea token from environment variable or prompt + gitea_token = os.getenv("GITEA_API_TOKEN") + if not gitea_token: + gitea_token = input("Enter your Gitea API token: ") + + # Get the font name + font_name = input("Enter the name of the font (e.g., FiraCode): ").lower() + + # Get the font files + font_folder = input("Enter the location of the font folder: ").strip("'\"") # Strip quotes from the input + print(f"Looking for font files in: {font_folder}") + + # Check if the folder exists + if not os.path.exists(font_folder): + print(f"Error: The folder '{font_folder}' does not exist.") + return + + # Find all the font files in the provided folder + font_files = [os.path.join(font_folder, f) for f in os.listdir(font_folder) if f.endswith(('ttf', 'otf'))] + + if not font_files: + print("No font files found in the provided folder.") + return + + print(f"Found {len(font_files)} font files:") + for font_file in font_files: + print(f" - {os.path.basename(font_file)}") + + # Create the font repository + font_repo_path, sha256_hash, zip_filename = create_font_repo(font_name, font_files, gitea_url, gitea_username, gitea_token) + + if not font_repo_path: + print("Failed to create font repository. Exiting.") + return + + # Create the formula + formula_path = create_formula(font_name, sha256_hash, gitea_url, gitea_username, zip_filename) + + # Add the submodule + submodule_path = add_submodule(font_name, gitea_url, gitea_username) + + # Update the submodule index + update_submodule_index(font_name, submodule_path) + + # Stage and commit the changes + subprocess.run(["git", "add", formula_path, "fonts/index.json", submodule_path], check=True) + subprocess.run(["git", "commit", "-m", f"Add {font_name} font as submodule"], check=True) + + print(f"Font {font_name} has been added successfully as a submodule.") + print("Remember to push your changes to Gitea manually.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/add-font.py b/add-font.py deleted file mode 100644 index 9b7d164..0000000 --- a/add-font.py +++ /dev/null @@ -1,150 +0,0 @@ -import os -import hashlib -import zipfile -from pathlib import Path -import shutil -import subprocess - - -def get_font_files(font_folder): - """Find all .ttf and .otf files in the given folder.""" - font_files = [] - for root, _, files in os.walk(font_folder): - for file in files: - if file.lower().endswith(('.ttf', '.otf')): - font_files.append(os.path.join(root, file)) - return font_files - - -def generate_sha256(file_path): - """Generate the sha256 hash of the given file.""" - sha256_hash = hashlib.sha256() - with open(file_path, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): - sha256_hash.update(byte_block) - return sha256_hash.hexdigest() - - -def create_zip(font_files, font_folder, zip_path): - """Create a zip file from the font files.""" - print(f"Creating zip file at {zip_path} with the following font files:") - for font_file in font_files: - print(f" - {font_file}") - - with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - for file in font_files: - # Add the font file with its relative path - arcname = os.path.relpath(file, font_folder) - zipf.write(file, arcname) - print(f"Added {file} to the zip as {arcname}") - - -def create_ruby_file(font_name, zip_path, sha256_hash): - """Generate a Ruby formula file for the font.""" - formula_content = f"""class {font_name} < Formula - desc "Font for {font_name}" - homepage "https://github.com/{font_name}" - url "{zip_path}" - sha256 "{sha256_hash}" - version "latest" - - def install - (share/"fonts").install Dir["*.ttf", "*.otf"] - end - - test do - assert_predicate share/"fonts/{font_name}-Regular.ttf", :exist? - end -end -""" - formula_path = f"./{font_name}/{font_name.lower()}.rb" - with open(formula_path, 'w') as formula_file: - formula_file.write(formula_content) - print(f"Ruby file created at {formula_path}") - - return formula_path - - -def move_formula_to_formula_folder(formula_path): - """Move the formula file to the Formula directory in the tap.""" - formula_folder = "./Formula" - if not os.path.exists(formula_folder): - os.makedirs(formula_folder) - - shutil.move(formula_path, os.path.join(formula_folder, os.path.basename(formula_path))) - print(f"Moved {formula_path} to {formula_folder}/") - - -def font_exists(font_name): - """Check if the formula already exists in the Formula folder.""" - formula_path = f"./Formula/font-{font_name.lower()}.rb" - return os.path.exists(formula_path) - - -def stage_and_commit_changes(font_name, font_folder_path, formula_path): - """Stage only the new font zip file and the formula file, then commit.""" - try: - # Stage the new font zip file - zip_file_path = font_folder_path / f"{font_name.lower()}.zip" - subprocess.run(["git", "add", str(zip_file_path)], check=True) - - # Stage the formula file from the Formula folder - formula_in_formula_folder = f"./Formula/{font_name.lower()}.rb" - subprocess.run(["git", "add", formula_in_formula_folder], check=True) - - # Commit with a default message - commit_message = f"Add {font_name} font" - subprocess.run(["git", "commit", "-m", commit_message], check=True) - print(f"Changes staged and committed with message: '{commit_message}'") - except subprocess.CalledProcessError as e: - print(f"Error while staging or committing: {e}") - -def main(): - font_folder = input("Enter the location of the font folder: ").strip() - - if not os.path.isdir(font_folder): - print("The folder does not exist. Please provide a valid path.") - return - - font_files = get_font_files(font_folder) - if not font_files: - print("No .ttf or .otf font files found in the specified folder.") - return - - font_name = input("Enter the name of the font (e.g., FiraCode): ").strip() - - # Add the "font-" prefix to the font name - font_package_name = f"font-{font_name.lower()}" - - # Check if the font already exists in the Formula folder - if font_exists(font_name): - update = input( - f"The font '{font_package_name}' already exists. Do you want to update it? (y/n): ").strip().lower() - if update != 'y': - print("Font update canceled.") - return - - # Create the folder for the font - font_folder_path = Path(f"./{font_package_name}") - font_folder_path.mkdir(exist_ok=True) - - zip_path = f"./{font_package_name}/{font_package_name}.zip" - create_zip(font_files, font_folder, zip_path) - - sha256_hash = generate_sha256(zip_path) - print(f"SHA256 hash generated: {sha256_hash}") - - formula_path = create_ruby_file(font_package_name, zip_path, sha256_hash) - - # Move the formula to the Formula directory - move_formula_to_formula_folder(formula_path) - - # Stage and commit the changes (only the font and its formula) - stage_and_commit_changes(font_package_name, font_folder_path, formula_path) - - print(f"Font {font_package_name} has been added successfully.") - print("Remember to push your changes to GitHub manually.") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/font-buffalo.zip b/font-buffalo.zip deleted file mode 100644 index af25da2..0000000 Binary files a/font-buffalo.zip and /dev/null differ diff --git a/font-buffalo/Buffalo.otf b/font-buffalo/Buffalo.otf new file mode 100644 index 0000000..025eace Binary files /dev/null and b/font-buffalo/Buffalo.otf differ diff --git a/remove-font.py b/remove-font.py new file mode 100755 index 0000000..3976da5 --- /dev/null +++ b/remove-font.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import subprocess +import requests +import re +from pathlib import Path +from dotenv import load_dotenv +from urllib.parse import urlparse + +# Load environment variables from .env file +load_dotenv() + +def get_gitea_credentials(): + """Get Gitea credentials from environment variables.""" + gitea_url = os.getenv("GITEA_URL") + gitea_username = os.getenv("GITEA_USERNAME") + gitea_token = os.getenv("GITEA_API_TOKEN") + + if not all([gitea_url, gitea_username, gitea_token]): + print("Error: Missing Gitea credentials in .env file.") + print("Please make sure GITEA_URL, GITEA_USERNAME, and GITEA_API_TOKEN are set.") + sys.exit(1) + + return gitea_url, gitea_username, gitea_token + +def get_available_fonts(): + """Get a list of available fonts from the index.json file.""" + index_path = Path("fonts/index.json") + if not index_path.exists(): + print("Error: fonts/index.json not found.") + sys.exit(1) + + with open(index_path, "r") as f: + index = json.load(f) + + return [font["name"] for font in index.get("fonts", [])] + +def get_repo_name_from_submodule(font_name): + """Get the repository name from the submodule URL.""" + submodule_path = f"fonts/{font_name}" + if not os.path.exists(submodule_path): + return None + + # Get the submodule URL from .gitmodules + try: + result = subprocess.run( + ["git", "config", "-f", ".gitmodules", f"submodule.{submodule_path}.url"], + capture_output=True, text=True, check=True + ) + url = result.stdout.strip() + + # Handle both HTTP and SSH URLs + if url.startswith('http://') or url.startswith('https://'): + # HTTP URL format: http://clancy.genet-godzilla.ts.net:3002/fishy/homebrew-font-test + match = re.search(r'/([^/]+)/([^/]+)$', url) + if match: + return match.group(2) # Return the repository name + elif url.startswith('git@') or ':' in url: + # SSH URL format: git@clancy.genet-godzilla.ts.net:fishy/homebrew-font-test + # or with custom port: ssh://git@clancy.genet-godzilla.ts.net:2222/fishy/homebrew-font-test + parts = url.split(':') + if len(parts) >= 2: + # Get the last part which should be username/repo + last_part = parts[-1] + if '/' in last_part: + return last_part.split('/')[-1] + + print(f"Warning: Could not extract repository name from URL: {url}") + return None + except subprocess.CalledProcessError: + return None + +def delete_gitea_repo(gitea_url, gitea_username, gitea_token, repo_name): + """Delete a repository from Gitea using the API.""" + api_url = f"{gitea_url}/api/v1/repos/{gitea_username}/{repo_name}" + headers = { + "Authorization": f"token {gitea_token}", + "Content-Type": "application/json" + } + + print(f"Deleting repository {repo_name} from Gitea...") + response = requests.delete(api_url, headers=headers) + + if response.status_code == 204: + print(f"Repository {repo_name} deleted successfully.") + return True + else: + print(f"Error deleting repository: {response.status_code} - {response.text}") + return False + +def remove_font(font_name): + """Remove a font from the Homebrew tap and delete its repository from Gitea.""" + # Get Gitea credentials + gitea_url, gitea_username, gitea_token = get_gitea_credentials() + + # Check if the font exists + available_fonts = get_available_fonts() + if font_name not in available_fonts: + print(f"Error: Font '{font_name}' not found in the available fonts.") + print(f"Available fonts: {', '.join(available_fonts)}") + sys.exit(1) + + # Confirm deletion + print(f"\nWARNING: You are about to remove the font '{font_name}' and delete its repository from Gitea.") + print("This action cannot be undone.") + confirmation = input("Are you sure you want to proceed? (yes/no): ").lower() + + if confirmation != "yes": + print("Operation cancelled.") + sys.exit(0) + + # Get the repository name from the submodule + repo_name = get_repo_name_from_submodule(font_name) + if not repo_name: + print(f"Warning: Could not determine the repository name for font '{font_name}'.") + print("The repository will not be deleted from Gitea.") + repo_name = None + + # Remove the submodule + submodule_path = f"fonts/{font_name}" + if os.path.exists(submodule_path): + print(f"Removing submodule {submodule_path}...") + subprocess.run(["git", "submodule", "deinit", "-f", submodule_path], check=True) + subprocess.run(["git", "rm", "-f", submodule_path], check=True) + subprocess.run(["rm", "-rf", f".git/modules/{submodule_path}"], check=True) + + # Remove the formula + formula_path = f"Formula/font-{font_name}.rb" + if os.path.exists(formula_path): + print(f"Removing formula {formula_path}...") + subprocess.run(["git", "rm", "-f", formula_path], check=True) + + # Update the index.json file + index_path = Path("fonts/index.json") + with open(index_path, "r") as f: + index = json.load(f) + + # Remove the font from the index + index["fonts"] = [font for font in index.get("fonts", []) if font["name"] != font_name] + + # Write the updated index back to the file + with open(index_path, "w") as f: + json.dump(index, f, indent=2) + + # Delete the repository from Gitea if we found the name + if repo_name: + delete_gitea_repo(gitea_url, gitea_username, gitea_token, repo_name) + else: + print("Skipping repository deletion as the repository name could not be determined.") + + # Commit the changes + subprocess.run(["git", "add", "fonts/index.json"], check=True) + subprocess.run(["git", "commit", "-m", f"Remove {font_name} font"], check=True) + + print(f"\nFont '{font_name}' has been removed successfully.") + print("Remember to push your changes to Gitea manually.") + +def main(): + if len(sys.argv) != 2: + print("Usage: python3 remove-font.py ") + print("\nAvailable fonts:") + available_fonts = get_available_fonts() + for font in available_fonts: + print(f" - {font}") + sys.exit(1) + + font_name = sys.argv[1] + remove_font(font_name) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..76bcb73 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.25.1 +python-dotenv>=0.19.0 \ No newline at end of file