From 68e15f5bf74124c58c64983ac594c6fa6e8eadfa Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 00:43:40 +0200 Subject: [PATCH 1/6] GitLab, Gitea, Forgejo, Codeberg, Sourcehut. --- README.md | 42 ++-- src/gitfetch/cli.py | 89 ++++++++- src/gitfetch/config.py | 48 ++++- src/gitfetch/fetcher.py | 417 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 559 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index c1fd9bd..af4d0ea 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gitfetch -A neofetch-style CLI tool for GitHub statistics. Display your GitHub profile and stats in a beautiful, colorful terminal interface. +A neofetch-style CLI tool for GitHub, GitLab, Gitea, Forgejo, Codeberg, and Sourcehut statistics. Display your profile and stats from various git hosting platforms in a beautiful, colorful terminal interface. image @@ -9,41 +9,33 @@ A neofetch-style CLI tool for GitHub statistics. Display your GitHub profile and ## Features - Neofetch-style display with ASCII art -- Comprehensive GitHub statistics +- Comprehensive statistics from multiple git hosting platforms - Smart SQLite-based caching system for faster subsequent runs - Persistent configuration with default username support -- Uses GitHub CLI (gh) for authentication - no rate limits! - Cross-platform support (macOS and Linux) -- First-run initialization with interactive setup +- First-run initialization with interactive provider selection -## Prerequisites +## Supported Platforms -**GitHub CLI (gh) must be installed and authenticated:** +- **GitHub** - Uses GitHub CLI (gh) for authentication +- **GitLab** - Uses GitLab CLI (glab) for authentication +- **Gitea/Forgejo/Codeberg** - Uses personal access tokens +- **Sourcehut** - Uses personal access tokens -See installation instructions at: https://github.com/cli/cli#installation +## Installation -### macOS +`gitfetch` can be installed without any prerequisites. During first-run setup, you'll be guided to install and authenticate with the necessary CLI tools or provide access tokens for your chosen git hosting platform. -```bash -brew install gh -gh auth login -``` - -### Linux - -Then authenticate: - -```bash -gh auth login -``` +## First-run Setup -### Verify Installation +When you run `gitfetch` for the first time, you'll be prompted to: -```bash -gh auth status -``` +1. **Choose your git hosting provider** (GitHub, GitLab, Gitea/Forgejo/Codeberg, or Sourcehut) +2. **Install required CLI tools** (if using GitHub or GitLab) +3. **Authenticate** with your chosen platform +4. **Configure access tokens** (if using Gitea/Forgejo/Codeberg or Sourcehut) -You should see: `✓ Logged in to github.com as YOUR_USERNAME` +The setup process will provide helpful error messages and installation instructions if anything is missing. ## Installing `gitfetch` diff --git a/src/gitfetch/cli.py b/src/gitfetch/cli.py index 2de0fb6..ccfe350 100644 --- a/src/gitfetch/cli.py +++ b/src/gitfetch/cli.py @@ -6,7 +6,6 @@ import sys from typing import Optional -from .fetcher import GitHubFetcher from .display import DisplayFormatter from .cache import CacheManager from .config import ConfigManager @@ -105,7 +104,9 @@ def main() -> int: # Initialize components cache_expiry = config_manager.get_cache_expiry_hours() cache_manager = CacheManager(cache_expiry_hours=cache_expiry) - fetcher = GitHubFetcher() # Uses gh CLI, no token needed + provider = config_manager.get_provider() + provider_url = config_manager.get_provider_url() + fetcher = _create_fetcher(provider, provider_url) formatter = DisplayFormatter(config_manager) if args.spaced: spaced = True @@ -192,15 +193,59 @@ def refresh_cache(): def _prompt_username() -> Optional[str]: - """Prompt user for GitHub username if not provided.""" + """Prompt user for username if not provided.""" try: - username = input("Enter GitHub username: ").strip() + username = input("Enter username: ").strip() return username if username else None except (KeyboardInterrupt, EOFError): print() return None +def _prompt_provider() -> Optional[str]: + """Prompt user for git provider.""" + try: + print("Available git providers:") + print("1. GitHub") + print("2. GitLab") + print("3. Gitea/Forgejo/Codeberg") + print("4. Sourcehut") + + while True: + choice = input("Choose your git provider (1-4): ").strip() + if choice == '1': + return 'github' + elif choice == '2': + return 'gitlab' + elif choice == '3': + return 'gitea' + elif choice == '4': + return 'sourcehut' + else: + print("Invalid choice. Please enter 1-4.") + except (KeyboardInterrupt, EOFError): + print() + return None + + +def _create_fetcher(provider: str, base_url: str): + """Create the appropriate fetcher for the provider.""" + if provider == 'github': + from .fetcher import GitHubFetcher + return GitHubFetcher() + elif provider == 'gitlab': + from .fetcher import GitLabFetcher + return GitLabFetcher(base_url) + elif provider == 'gitea': + from .fetcher import GiteaFetcher + return GiteaFetcher(base_url) + elif provider == 'sourcehut': + from .fetcher import SourcehutFetcher + return SourcehutFetcher(base_url) + else: + raise ValueError(f"Unsupported provider: {provider}") + + def _initialize_gitfetch(config_manager: ConfigManager) -> bool: """ Initialize gitfetch by creating config directory and setting @@ -213,14 +258,42 @@ def _initialize_gitfetch(config_manager: ConfigManager) -> bool: True if initialization succeeded, False otherwise """ try: - # Try to get authenticated user from GitHub CLI - fetcher = GitHubFetcher() + # Ask user for git provider + provider = _prompt_provider() + if not provider: + return False + + config_manager.set_provider(provider) + + # Set default URL for known providers + if provider == 'github': + config_manager.set_provider_url('https://api.github.com') + elif provider == 'gitlab': + config_manager.set_provider_url('https://gitlab.com') + elif provider == 'gitea': + url = input("Enter Gitea/Forgejo/Codeberg URL: ").strip() + if not url: + print("Provider URL required", file=sys.stderr) + return False + config_manager.set_provider_url(url) + elif provider == 'sourcehut': + config_manager.set_provider_url('https://git.sr.ht') + + # Create appropriate fetcher + fetcher = _create_fetcher(provider, config_manager.get_provider_url()) + + # Try to get authenticated user try: username = fetcher.get_authenticated_user() - print(f"Using authenticated GitHub user: {username}") + print(f"Using authenticated user: {username}") except Exception as e: print(f"Could not get authenticated user: {e}") - print("Please ensure GitHub CLI is authenticated with: gh auth login") + if provider == 'github': + print("Please authenticate with: gh auth login") + elif provider == 'gitlab': + print("Please authenticate with: glab auth login") + else: + print("Please ensure you have a valid token configured") return False # Save configuration diff --git a/src/gitfetch/config.py b/src/gitfetch/config.py index fa11b39..a193c5c 100644 --- a/src/gitfetch/config.py +++ b/src/gitfetch/config.py @@ -126,9 +126,53 @@ def is_initialized(self) -> bool: Check if gitfetch has been initialized. Returns: - True if config exists and has default username + True if config exists and has default username and provider """ - return self.CONFIG_FILE.exists() and bool(self.get_default_username()) + return (self.CONFIG_FILE.exists() and + bool(self.get_default_username()) and + bool(self.get_provider())) + + def get_provider(self) -> Optional[str]: + """ + Get the git provider from config. + + Returns: + Provider name (github, gitlab, gitea, etc.) or None if not set + """ + provider = self.config.get('DEFAULT', 'provider', fallback='') + return provider if provider else None + + def set_provider(self, provider: str) -> None: + """ + Set the git provider in config. + + Args: + provider: Git provider name (github, gitlab, gitea, etc.) + """ + if 'DEFAULT' not in self.config: + self.config['DEFAULT'] = {} + self.config['DEFAULT']['provider'] = provider + + def get_provider_url(self) -> Optional[str]: + """ + Get the provider base URL from config. + + Returns: + Base URL for the git provider or None if not set + """ + url = self.config.get('DEFAULT', 'provider_url', fallback='') + return url if url else None + + def set_provider_url(self, url: str) -> None: + """ + Set the provider base URL in config. + + Args: + url: Base URL for the git provider + """ + if 'DEFAULT' not in self.config: + self.config['DEFAULT'] = {} + self.config['DEFAULT']['provider_url'] = url def save(self) -> None: """Save configuration to file.""" diff --git a/src/gitfetch/fetcher.py b/src/gitfetch/fetcher.py index ab98f96..b106378 100644 --- a/src/gitfetch/fetcher.py +++ b/src/gitfetch/fetcher.py @@ -1,14 +1,65 @@ """ -GitHub data fetcher using the GitHub CLI (gh) +Git data fetcher for various git hosting providers """ +from abc import ABC, abstractmethod from typing import Optional, Dict, Any import subprocess import json import sys -class GitHubFetcher: +class BaseFetcher(ABC): + """Abstract base class for git hosting provider fetchers.""" + + def __init__(self, token: Optional[str] = None): + """ + Initialize the fetcher. + + Args: + token: Optional authentication token + """ + self.token = token + + @abstractmethod + def get_authenticated_user(self) -> str: + """ + Get the authenticated username. + + Returns: + The login of the authenticated user + """ + pass + + @abstractmethod + def fetch_user_data(self, username: str) -> Dict[str, Any]: + """ + Fetch basic user profile data. + + Args: + username: Username to fetch data for + + Returns: + Dictionary containing user profile data + """ + pass + + @abstractmethod + def fetch_user_stats(self, username: str, user_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Fetch detailed statistics for a user. + + Args: + username: Username to fetch stats for + user_data: Optional pre-fetched user data + + Returns: + Dictionary containing user statistics + """ + pass + + +class GitHubFetcher(BaseFetcher): """Fetches GitHub user data and statistics using GitHub CLI.""" def __init__(self, token: Optional[str] = None): @@ -418,3 +469,365 @@ def _fetch_contribution_graph(self, username: str) -> list: except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError): return [] + + +class GitLabFetcher(BaseFetcher): + """Fetches GitLab user data and statistics.""" + + def __init__(self, base_url: str = "https://gitlab.com", + token: Optional[str] = None): + """ + Initialize the GitLab fetcher. + + Args: + base_url: GitLab instance base URL + token: Optional GitLab personal access token + """ + super().__init__(token) + self.base_url = base_url.rstrip('/') + self.api_base = f"{self.base_url}/api/v4" + + def _check_glab_cli(self) -> None: + """Check if GitLab CLI is installed and authenticated.""" + try: + result = subprocess.run( + ['glab', 'auth', 'status'], + capture_output=True, + text=True, + timeout=5 + ) + if result.returncode != 0: + print("GitLab CLI not authenticated", file=sys.stderr) + print("Please run: glab auth login", file=sys.stderr) + sys.exit(1) + except FileNotFoundError: + print("GitLab CLI (glab) not installed", file=sys.stderr) + print("Install: https://gitlab.com/gitlab-org/cli", + file=sys.stderr) + sys.exit(1) + except subprocess.TimeoutExpired: + print("Error: glab CLI timeout", file=sys.stderr) + sys.exit(1) + + def get_authenticated_user(self) -> str: + """ + Get the authenticated GitLab username. + + Returns: + The username of the authenticated user + """ + try: + result = subprocess.run( + ['glab', 'api', '/user'], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode != 0: + raise Exception("Failed to get user info") + + data = json.loads(result.stdout) + return data.get('username', '') + except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError): + raise Exception("Could not determine authenticated user") + + def _api_request(self, endpoint: str) -> Any: + """ + Make API request to GitLab. + + Args: + endpoint: API endpoint + + Returns: + Parsed JSON response + """ + cmd = ['glab', 'api', endpoint] + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode != 0: + raise Exception(f"API request failed: {result.stderr}") + return json.loads(result.stdout) + except subprocess.TimeoutExpired: + raise Exception("GitLab API request timed out") + except json.JSONDecodeError as e: + raise Exception(f"Failed to parse API response: {e}") + + def fetch_user_data(self, username: str) -> Dict[str, Any]: + """ + Fetch basic user profile data from GitLab. + + Args: + username: GitLab username + + Returns: + Dictionary containing user profile data + """ + return self._api_request(f'/users?username={username}')[0] + + def fetch_user_stats(self, username: str, user_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Fetch detailed statistics for a GitLab user. + + Args: + username: GitLab username + user_data: Optional pre-fetched user data + + Returns: + Dictionary containing user statistics + """ + if not user_data: + user_data = self.fetch_user_data(username) + + user_id = user_data.get('id') + + # Fetch user's projects + repos = self._api_request(f'/users/{user_id}/projects') + + total_stars = sum(repo.get('star_count', 0) for repo in repos) + total_forks = sum(repo.get('forks_count', 0) for repo in repos) + + # Calculate language stats + languages = {} + for repo in repos: + lang = repo.get('language', 'Unknown') + if lang in languages: + languages[lang] += 1 + else: + languages[lang] = 1 + + # GitLab doesn't have contribution graphs like GitHub + # Return simplified stats + return { + 'total_stars': total_stars, + 'total_forks': total_forks, + 'total_repos': len(repos), + 'languages': languages, + 'contribution_graph': [], # Not available + 'pull_requests': {'open': 0, 'awaiting_review': 0, 'mentions': 0}, + 'issues': {'assigned': 0, 'created': 0, 'mentions': 0}, + } + + +class GiteaFetcher(BaseFetcher): + """Fetches Gitea/Forgejo/Codeberg user data and statistics.""" + + def __init__(self, base_url: str, token: Optional[str] = None): + """ + Initialize the Gitea fetcher. + + Args: + base_url: Gitea instance base URL (required) + token: Optional Gitea personal access token + """ + super().__init__(token) + self.base_url = base_url.rstrip('/') + self.api_base = f"{self.base_url}/api/v1" + + def get_authenticated_user(self) -> str: + """ + Get the authenticated Gitea username. + + Returns: + The username of the authenticated user + """ + if not self.token: + raise Exception("Token required for Gitea authentication") + + try: + import requests + headers = {'Authorization': f'token {self.token}'} + response = requests.get( + f'{self.api_base}/user', headers=headers, timeout=10) + response.raise_for_status() + data = response.json() + return data.get('login', '') + except Exception as e: + raise Exception(f"Could not get authenticated user: {e}") + + def _api_request(self, endpoint: str) -> Any: + """ + Make API request to Gitea. + + Args: + endpoint: API endpoint + + Returns: + Parsed JSON response + """ + if not self.token: + raise Exception("Token required for Gitea API") + + try: + import requests + headers = {'Authorization': f'token {self.token}'} + response = requests.get( + f'{self.api_base}{endpoint}', headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except Exception as e: + raise Exception(f"Gitea API request failed: {e}") + + def fetch_user_data(self, username: str) -> Dict[str, Any]: + """ + Fetch basic user profile data from Gitea. + + Args: + username: Gitea username + + Returns: + Dictionary containing user profile data + """ + return self._api_request(f'/users/{username}') + + def fetch_user_stats(self, username: str, user_data=None): + """ + Fetch detailed statistics for a Gitea user. + + Args: + username: Gitea username + user_data: Optional pre-fetched user data + + Returns: + Dictionary containing user statistics + """ + if not user_data: + user_data = self.fetch_user_data(username) + + # Fetch user's repositories + repos = self._api_request(f'/users/{username}/repos') + + total_stars = sum(repo.get('stars_count', 0) for repo in repos) + total_forks = sum(repo.get('forks_count', 0) for repo in repos) + + # Calculate language stats + languages = {} + for repo in repos: + lang = repo.get('language', 'Unknown') + if lang and lang in languages: + languages[lang] += 1 + elif lang: + languages[lang] = 1 + + # Gitea doesn't have contribution graphs or PR/issue stats like GitHub + return { + 'total_stars': total_stars, + 'total_forks': total_forks, + 'total_repos': len(repos), + 'languages': languages, + 'contribution_graph': [], # Not available + 'pull_requests': {'open': 0, 'awaiting_review': 0, 'mentions': 0}, + 'issues': {'assigned': 0, 'created': 0, 'mentions': 0}, + } + + +class SourcehutFetcher(BaseFetcher): + """Fetches Sourcehut user data and statistics.""" + + def __init__(self, base_url: str = "https://git.sr.ht", token: Optional[str] = None): + """ + Initialize the Sourcehut fetcher. + + Args: + base_url: Sourcehut instance base URL + token: Optional Sourcehut personal access token + """ + super().__init__(token) + self.base_url = base_url.rstrip('/') + + def get_authenticated_user(self) -> str: + """ + Get the authenticated Sourcehut username. + + Returns: + The username of the authenticated user + """ + if not self.token: + raise Exception("Token required for Sourcehut authentication") + + # Sourcehut uses GraphQL API + try: + import requests + query = """ + query { + me { + username + } + } + """ + headers = {'Authorization': f'Bearer {self.token}'} + response = requests.post( + f'{self.base_url}/graphql', + json={'query': query}, + headers=headers, + timeout=10 + ) + response.raise_for_status() + data = response.json() + return data.get('data', {}).get('me', {}).get('username', '') + except Exception as e: + raise Exception(f"Could not get authenticated user: {e}") + + def fetch_user_data(self, username: str) -> Dict[str, Any]: + """ + Fetch basic user profile data from Sourcehut. + + Args: + username: Sourcehut username + + Returns: + Dictionary containing user profile data + """ + # Sourcehut GraphQL query for user + query = f""" + query {{ + user(username: "{username}") {{ + username + name + bio + location + website + }} + }} + """ + try: + import requests + headers = { + 'Authorization': f'Bearer {self.token}'} if self.token else {} + response = requests.post( + f'{self.base_url}/graphql', + json={'query': query}, + headers=headers, + timeout=30 + ) + response.raise_for_status() + data = response.json() + return data.get('data', {}).get('user', {}) + except Exception as e: + raise Exception(f"Sourcehut API request failed: {e}") + + def fetch_user_stats(self, username: str, user_data=None): + """ + Fetch detailed statistics for a Sourcehut user. + + Args: + username: Sourcehut username + user_data: Optional pre-fetched user data + + Returns: + Dictionary containing user statistics + """ + # Sourcehut has limited public stats, return minimal data + return { + 'total_stars': 0, # Not available + 'total_forks': 0, # Not available + 'total_repos': 0, # Would need separate API call + 'languages': {}, # Not available + 'contribution_graph': [], # Not available + 'pull_requests': {'open': 0, 'awaiting_review': 0, 'mentions': 0}, + 'issues': {'assigned': 0, 'created': 0, 'mentions': 0}, + } From d5691359970ff58cde3e68cea05b30fa985a3cf7 Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 00:46:44 +0200 Subject: [PATCH 2/6] better update instructions --- src/gitfetch/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gitfetch/cli.py b/src/gitfetch/cli.py index ccfe350..883c59b 100644 --- a/src/gitfetch/cli.py +++ b/src/gitfetch/cli.py @@ -81,7 +81,7 @@ def main() -> int: print( f"\033[93mUpdate available: {latest}\n" "Get it at: https://github.com/Matars/gitfetch/releases/latest\n" - "Or run: brew upgrade gitfetch\033[0m") + "Or run: brew update && brew upgrade gitfetch\033[0m") else: print("You are using the latest version.") else: From a173d1b61a7d8ed44e01e93a7b351c8dd5c03b8d Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 01:09:06 +0200 Subject: [PATCH 3/6] update workflows --- .github/workflows/update-aur.yml | 4 ++++ .github/workflows/update-homebrew.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/update-aur.yml b/.github/workflows/update-aur.yml index 2a2349d..55c693a 100644 --- a/.github/workflows/update-aur.yml +++ b/.github/workflows/update-aur.yml @@ -51,6 +51,8 @@ jobs: sed -i 's/pkgver=.*/pkgver=${{ steps.get_version.outputs.version }}/' PKGBUILD sed -i 's|source=.*|source=("$pkgname-$pkgver.tar.gz::https://github.com/Matars/gitfetch/archive/refs/tags/v$pkgver.tar.gz")|' PKGBUILD sed -i "s/sha256sums=.*/sha256sums=('${{ steps.sha256.outputs.sha256 }}')/" PKGBUILD + # Update dependencies + sed -i 's/depends=.*/depends=("python-requests" "python-readchar")/' PKGBUILD - name: Update .SRCINFO run: | @@ -58,6 +60,8 @@ jobs: sed -i 's/pkgver = .*/pkgver = ${{ steps.get_version.outputs.version }}/' .SRCINFO sed -i 's|source = .*|source = gitfetch-python-${{ steps.get_version.outputs.version }}.tar.gz::https://github.com/Matars/gitfetch/archive/refs/tags/v${{ steps.get_version.outputs.version }}.tar.gz|' .SRCINFO sed -i "s/sha256sums = .*/sha256sums = ${{ steps.sha256.outputs.sha256 }}/" .SRCINFO + # Update dependencies in .SRCINFO + sed -i 's/depends = python-requests/depends = python-requests\n\tdepends = python-readchar/' .SRCINFO - name: Commit and push run: | diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index eb39be5..f3275f1 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -43,6 +43,8 @@ jobs: sed -i 's|url "https://github.com/Matars/gitfetch/archive/refs/tags/v.*"|url "https://github.com/Matars/gitfetch/archive/refs/tags/v${{ steps.get_version.outputs.version }}.tar.gz"|' homebrew-tap/Formula/gitfetch.rb sed -i 's|version ".*"|version "${{ steps.get_version.outputs.version }}"|' homebrew-tap/Formula/gitfetch.rb sed -i 's|sha256 ".*"|sha256 "${{ steps.sha256.outputs.sha256 }}"|' homebrew-tap/Formula/gitfetch.rb + # Update dependencies + sed -i 's|depends_on "requests"|depends_on "requests"\n depends_on "readchar"|' homebrew-tap/Formula/gitfetch.rb - name: Commit and push run: | From 420cda2b5676a94781c9fcb5001b1cfcfcf979d3 Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 01:09:56 +0200 Subject: [PATCH 4/6] multiple providers --- src/gitfetch/cli.py | 56 ++++++++++++++++++++++++++++-------------- src/gitfetch/config.py | 30 +++++++++++++++++++++- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/gitfetch/cli.py b/src/gitfetch/cli.py index 883c59b..a5bcfe5 100644 --- a/src/gitfetch/cli.py +++ b/src/gitfetch/cli.py @@ -6,6 +6,8 @@ import sys from typing import Optional +import readchar + from .display import DisplayFormatter from .cache import CacheManager from .config import ConfigManager @@ -15,7 +17,7 @@ def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser( - description="A neofetch-style CLI tool for GitHub statistics", + description="A neofetch-style CLI tool for git provider statistics", formatter_class=argparse.RawDescriptionHelpFormatter ) @@ -203,26 +205,42 @@ def _prompt_username() -> Optional[str]: def _prompt_provider() -> Optional[str]: - """Prompt user for git provider.""" - try: - print("Available git providers:") - print("1. GitHub") - print("2. GitLab") - print("3. Gitea/Forgejo/Codeberg") - print("4. Sourcehut") + """Prompt user for git provider with interactive selection.""" + providers = [ + ('github', 'GitHub'), + ('gitlab', 'GitLab'), + ('gitea', 'Gitea/Forgejo/Codeberg'), + ('sourcehut', 'Sourcehut') + ] + + selected = 0 + try: while True: - choice = input("Choose your git provider (1-4): ").strip() - if choice == '1': - return 'github' - elif choice == '2': - return 'gitlab' - elif choice == '3': - return 'gitea' - elif choice == '4': - return 'sourcehut' - else: - print("Invalid choice. Please enter 1-4.") + # Clear screen and print header + print("\033[2J\033[H", end="") + print("Choose your git provider:") + print() + + # Print options with cursor + for i, (key, name) in enumerate(providers): + indicator = "●" if i == selected else "○" + print(f"{indicator} {name}") + + print() + print("Use ↑/↓ arrows, ● = selected, Enter to confirm") + + # Read key + key = readchar.readkey() + + if key == readchar.key.UP: + selected = (selected - 1) % len(providers) + elif key == readchar.key.DOWN: + selected = (selected + 1) % len(providers) + elif key == readchar.key.ENTER: + print() # New line after selection + return providers[selected][0] + except (KeyboardInterrupt, EOFError): print() return None diff --git a/src/gitfetch/config.py b/src/gitfetch/config.py index a193c5c..5ddc32d 100644 --- a/src/gitfetch/config.py +++ b/src/gitfetch/config.py @@ -176,6 +176,34 @@ def set_provider_url(self, url: str) -> None: def save(self) -> None: """Save configuration to file.""" + import os self._ensure_config_dir() + # Remove the file if it exists to ensure clean write + if self.CONFIG_FILE.exists(): + os.remove(self.CONFIG_FILE) with open(self.CONFIG_FILE, 'w') as f: - self.config.write(f) + f.write("# gitfetch configuration file\n") + f.write("# See docs/providers.md for provider configuration\n") + f.write("# See docs/colors.md for color customization\n\n") + + f.write("[DEFAULT]\n") + username = self.config.get('DEFAULT', 'username', fallback='') + f.write(f"username = {username}\n\n") + + cache_hours = self.config.get('DEFAULT', 'cache_expiry_hours', + fallback='24') + f.write(f"cache_expiry_hours = {cache_hours}\n\n") + + provider = self.config.get('DEFAULT', 'provider', fallback='') + f.write(f"provider = {provider}\n\n") + + provider_url = self.config.get('DEFAULT', 'provider_url', + fallback='') + f.write(f"provider_url = {provider_url}\n\n") + + if 'COLORS' in self.config: + f.write("[COLORS]\n") + for key, value in self.config['COLORS'].items(): + f.write(f"{key} = {value}\n") + f.write("\n") + f.write("\n") From 84006e21d128d83ea33391510090bd8be3ef39db Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 01:10:04 +0200 Subject: [PATCH 5/6] dependencies --- pyproject.toml | 8 ++++++-- requirements.txt | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3fe9400..f9ec8dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gitfetch" -version = "1.0.20" +version = "1.1.0" description = "A neofetch-style CLI tool for GitHub statistics" readme = "README.md" requires-python = ">=3.8" @@ -27,7 +27,8 @@ classifiers = [ ] dependencies = [ - "requests>=2.0.0" + "requests>=2.0.0", + "readchar>=4.0.0" ] [project.optional-dependencies] @@ -50,6 +51,9 @@ Issues = "https://github.com/Matars/gitfetch/issues" [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.package-data] +gitfetch = ["docs/*.md"] + [tool.black] line-length = 100 target-version = ['py38', 'py39', 'py310', 'py311'] diff --git a/requirements.txt b/requirements.txt index 0c384e0..9fef660 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # No Python dependencies required # Requires GitHub CLI (gh) to be installed and authenticated requests>=2.0.0 +readchar>=4.0.0 From e7605201c439e72df107dce2e43ab2c1b07debf9 Mon Sep 17 00:00:00 2001 From: km222uq Date: Tue, 21 Oct 2025 01:10:08 +0200 Subject: [PATCH 6/6] docs --- README.md | 96 +++++++++-------------------------------------- docs/colors.md | 65 ++++++++++++++++++++++++++++++++ docs/providers.md | 47 +++++++++++++++++++++++ 3 files changed, 130 insertions(+), 78 deletions(-) create mode 100644 docs/colors.md create mode 100644 docs/providers.md diff --git a/README.md b/README.md index af4d0ea..109b719 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ gh auth status Configuration file location: `~/.config/gitfetch/gitfetch.conf` -The configuration file is automatically created on first run and contains two main sections: +The configuration file is automatically created on first run. See `docs/providers.md` for detailed provider configuration and `docs/colors.md` for color customization options. ### [DEFAULT] Section @@ -165,99 +165,39 @@ The configuration file is automatically created on first run and contains two ma [DEFAULT] username = yourusername cache_expiry_hours = 24 +provider = github +provider_url = https://api.github.com ``` -- `username`: Your default GitHub username (automatically set from authenticated GitHub CLI user) +- `username`: Your default username (automatically detected) - `cache_expiry_hours`: How long to keep cached data (default: 24 hours) +- `provider`: Git hosting provider (github, gitlab, gitea, sourcehut) +- `provider_url`: API URL for the provider ### [COLORS] Section -gitfetch supports extensive color customization. All colors use ANSI escape codes. +gitfetch supports extensive color customization. All colors use ANSI escape codes. See `docs/colors.md` for detailed color configuration options. +````ini ```ini [COLORS] reset = \033[0m bold = \033[1m -dim = \033[2m -red = \033[91m -green = \033[92m -yellow = \033[93m -blue = \033[94m -magenta = \033[95m -cyan = \033[96m -white = \033[97m -orange = \033[38;2;255;165;0m -accent = \033[1m -header = \033[38;2;118;215;161m -muted = \033[2m -0 = \033[48;5;238m -1 = \033[48;5;28m -2 = \033[48;5;34m -3 = \033[48;5;40m -4 = \033[48;5;82m -``` - -#### Color Reference - -- **Text Styles**: - - - `reset`: Reset all formatting - - `bold`: Bold text - - `dim`: Dimmed text - - `accent`: Accent styling (bold) - -- **Basic Colors**: - - - `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`: Standard ANSI colors - - `orange`: Custom orange color +# ... color definitions ... +```` -- **UI Elements**: +See `docs/colors.md` for detailed color configuration options and customization examples. - - `header`: Section headers and main display text - - `muted`: Separators and underlines +## Supported Providers -- **Contribution Graph**: - - `0`: No contributions (lightest) - - `1`: 1-2 contributions - - `2`: 3-6 contributions - - `3`: 7-12 contributions - - `4`: 13+ contributions (darkest) +gitfetch supports multiple Git hosting platforms: -#### Customizing Colors - -To change colors, edit `~/.config/gitfetch/gitfetch.conf` and modify the ANSI escape codes: - -**Example: Change header color to blue** - -```ini -header = \033[94m -``` - -**Example: Change contribution graph colors to a purple theme** - -```ini -0 = \033[48;5;235m # Dark gray for no contributions -1 = \033[48;5;60m # Dark purple -2 = \033[48;5;62m # Medium purple -3 = \033[48;5;64m # Light purple -4 = \033[48;5;66m # Bright purple -``` - -**Common ANSI Color Codes**: - -- `\033[91m` = Bright Red -- `\033[92m` = Bright Green -- `\033[93m` = Bright Yellow -- `\033[94m` = Bright Blue -- `\033[95m` = Bright Magenta -- `\033[96m` = Bright Cyan -- `\033[97m` = Bright White - -**Background Colors** (for contribution blocks): - -- `\033[48;5;{color_code}m` where color_code is 0-255 (256-color palette) +- **GitHub** - Uses GitHub CLI (gh) for authentication +- **GitLab** - Uses GitLab CLI (glab) for authentication +- **Gitea/Forgejo/Codeberg** - Direct API access with personal access tokens +- **Sourcehut** - Direct API access with personal access tokens -Changes take effect immediately - no restart required. +See `docs/providers.md` for detailed setup instructions for each provider. ## Caching diff --git a/docs/colors.md b/docs/colors.md new file mode 100644 index 0000000..6c4a56b --- /dev/null +++ b/docs/colors.md @@ -0,0 +1,65 @@ +# Color Configuration + +gitfetch uses ANSI color codes for terminal output. Colors can be customized in the `[COLORS]` section of the config file. + +## Available Colors + +The following color keys can be customized: + +### Text Formatting + +- `reset`: Reset all formatting +- `bold`: Bold text +- `dim`: Dimmed text + +### Basic Colors + +- `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` + +### Special Colors + +- `orange`: Orange color +- `accent`: Accent color for highlights +- `header`: Header text color +- `muted`: Muted text color + +### Contribution Graph Colors (0-4) + +- `0`: Lowest contribution level +- `1`: Low contribution level +- `2`: Medium contribution level +- `3`: High contribution level +- `4`: Highest contribution level + +## Configuration + +Colors use ANSI escape codes. Examples: + +```ini +[COLORS] +reset = \033[0m +bold = \033[1m +red = \033[91m +green = \033[92m +blue = \033[94m +header = \033[38;2;118;215;161m +0 = \033[48;5;238m +1 = \033[48;5;28m +``` + +## ANSI Color Codes + +- `\033[0m`: Reset +- `\033[1m`: Bold +- `\033[2m`: Dim +- `\033[91m`: Bright Red +- `\033[92m`: Bright Green +- `\033[93m`: Bright Yellow +- `\033[94m`: Bright Blue +- `\033[95m`: Bright Magenta +- `\033[96m`: Bright Cyan +- `\033[97m`: Bright White + +For 256-color codes, use `\033[38;5;{color_code}m` for foreground or `\033[48;5;{color_code}m` for background. + +For RGB colors, use `\033[38;2;{r};{g};{b}m` for foreground or `\033[48;2;{r};{g};{b}m` for background. diff --git a/docs/providers.md b/docs/providers.md new file mode 100644 index 0000000..1c7bf79 --- /dev/null +++ b/docs/providers.md @@ -0,0 +1,47 @@ +# Git Providers + +gitfetch supports multiple Git hosting providers. Configure your preferred provider in the config file. + +## Supported Providers + +### GitHub + +- **provider**: `github` +- **provider_url**: `https://api.github.com` +- **Requirements**: GitHub CLI (`gh`) must be installed and authenticated +- **Authentication**: Run `gh auth login` + +### GitLab + +- **provider**: `gitlab` +- **provider_url**: `https://gitlab.com` +- **Requirements**: GitLab CLI (`glab`) must be installed and authenticated +- **Authentication**: Run `glab auth login` + +### Gitea/Forgejo/Codeberg + +- **provider**: `gitea` +- **provider_url**: Custom URL (e.g., `https://codeberg.org`, `https://gitea.com`) +- **Requirements**: None (uses API directly) +- **Authentication**: Set personal access token in environment or use CLI tools + +### Sourcehut + +- **provider**: `sourcehut` +- **provider_url**: `https://git.sr.ht` +- **Requirements**: None (uses API directly) +- **Authentication**: Set personal access token in environment + +## Configuration + +Set the provider and URL in your `gitfetch.conf`: + +```ini +[DEFAULT] +provider = github +provider_url = https://api.github.com +``` + +## Adding New Providers + +To add support for a new Git provider, implement a new fetcher class in `fetcher.py` and update the provider selection logic in `cli.py`.