#!/usr/bin/env python3 import argparse import urllib.request import json import signal import re import shutil from concurrent.futures import ThreadPoolExecutor, as_completed from urllib.error import HTTPError, URLError # Updated color codes with simpler separator COLORS = { 'reset': '\033[0m', 'bold': '\033[1m', 'red': '\033[91m', 'green': '\033[92m', 'yellow': '\033[93m', 'header': '\033[94m' # Simpler header color } def colorize(text, color): if not hasattr(colorize, 'is_tty'): colorize.is_tty = __import__('sys').stdout.isatty() return f"{COLORS[color]}{text}{COLORS['reset']}" if colorize.is_tty else text signal.signal(signal.SIGINT, lambda s, f: exit(1)) REQUEST_HEADERS = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) checkpkg/1.0', 'Accept': 'application/json' } PM_MAPPINGS = { 'apt': ['debian_', 'ubuntu_'], 'dnf': ['fedora_'], 'pacman': ['arch'], 'aur': ['aur'], 'apk': ['alpine_'], 'zypper': ['opensuse_'] } REPO_FORMATS = { 'debian_': "Debian {}", 'ubuntu_': "Ubuntu {}", 'fedora_': "Fedora {}", 'arch': "Arch", 'aur': "AUR", 'alpine_': "Alpine {}", 'opensuse_': "openSUSE {}" } def version_key(version): """Create a sorting key for version comparison""" return [ (0, int(part)) if part.isdigit() else (1, part.lower()) for part in re.findall(r'(\d+|\D+)', version) ] def get_package_manager(repo): for pm, patterns in PM_MAPPINGS.items(): if any(repo.startswith(p) for p in patterns): return pm return None def format_repository(repo): for pattern, fmt in REPO_FORMATS.items(): if repo.startswith(pattern): parts = repo.split('_') return fmt.format(parts[1] if len(parts) > 1 else '') return repo def fetch_package_data(package): try: req = urllib.request.Request( f'https://repology.org/api/v1/project/{package}', headers=REQUEST_HEADERS ) with urllib.request.urlopen(req, timeout=10) as response: return json.load(response) except HTTPError as e: if e.code == 403: print(colorize(f"Error: Repology blocked the request for {package} (try again later)", 'red')) return None except Exception: return None def main(): parser = argparse.ArgumentParser(description='Package search tool') parser.add_argument('--all', action='store_true') parser.add_argument('--apt', action='store_true') parser.add_argument('--dnf', action='store_true') parser.add_argument('--pacman', action='store_true') parser.add_argument('--apk', action='store_true') parser.add_argument('--zypper', action='store_true') parser.add_argument('--aur', action='store_true') parser.add_argument('packages', nargs='+') args = parser.parse_args() selected_pms = [pm for pm, flag in [ ('apt', args.apt or args.all), ('dnf', args.dnf or args.all), ('pacman', args.pacman or args.all), ('apk', args.apk or args.all), ('zypper', args.zypper or args.all), ('aur', args.aur or args.all) ] if flag] if not selected_pms: print(colorize("Error: No package managers selected", 'red')) return results = {} with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(fetch_package_data, pkg): pkg for pkg in args.packages} for future in as_completed(futures): pkg = futures[future] try: data = future.result() results[pkg] = data or [] except Exception as e: print(colorize(f"Error processing {pkg}: {str(e)}", 'red')) results[pkg] = [] output = {} for pkg, entries in results.items(): pm_versions = {pm: {'version': '', 'repos': set(), 'key': []} for pm in selected_pms} for entry in entries: repo = entry.get('repo', '') version = entry.get('version', 'N/A') pm = get_package_manager(repo) if pm in selected_pms and version != 'N/A': repo_fmt = format_repository(repo) current_key = version_key(version) stored = pm_versions[pm] if not stored['key'] or current_key > stored['key']: stored['version'] = version stored['repos'] = {repo_fmt} stored['key'] = current_key elif current_key == stored['key']: stored['repos'].add(repo_fmt) output[pkg] = {} for pm in selected_pms: data = pm_versions[pm] if data['version']: repos = ', '.join(sorted(data['repos'])) output[pkg][pm] = f"{data['version']} ({repos})" else: output[pkg][pm] = 'Not found' headers = ['Package'] + selected_pms terminal_width = get_terminal_width() # Calculate initial column widths col_widths = [len(h) for h in headers] content_widths = {0: max(len(pkg) for pkg in args.packages)} for pkg in args.packages: versions = output.get(pkg, {}) for i, pm in enumerate(selected_pms, 1): content_widths[i] = max(content_widths.get(i, 0), len(versions.get(pm, ''))) # Set initial widths for i in range(len(headers)): col_widths[i] = max(len(headers[i]), content_widths.get(i, 0)) # Adjust for terminal width total_width = sum(col_widths) + 3 * (len(headers) - 1) # 3 chars per separator max_reductions = 5 # Max characters to reduce per column if total_width > terminal_width: excess = total_width - terminal_width reducible = [i for i in range(1, len(headers)) if col_widths[i] > 10] while excess > 0 and reducible: per_col = max(1, min(max_reductions, excess // len(reducible))) for i in reducible: reduction = min(per_col, col_widths[i] - 10, excess) col_widths[i] -= reduction excess -= reduction if excess <= 0: break # Truncate text with ellipsis if needed def truncate(text, width): if len(text) > width and width > 3: return text[:width-3] + '...' return text.ljust(width) # Print header header_line = ' | '.join( truncate(colorize(h, 'header'), w) for h, w in zip(headers, col_widths) ) print(header_line) # Separator line print('-' * min(sum(col_widths) + 3*(len(headers)-1), terminal_width)) # Print rows for pkg in args.packages: row = [truncate(colorize(pkg, 'bold'), col_widths[0])] versions = output.get(pkg, {pm: 'Not found' for pm in selected_pms}) for i, pm in enumerate(selected_pms, 1): version = versions.get(pm, 'Not found') color = 'green' if version != 'Not found' else 'red' if 'AUR' in version: color = 'yellow' # Fixed line with proper parenthesis closure row.append(truncate(colorize(version, color), col_widths[i])) print(' | '.join(row)) if __name__ == '__main__': main()