From 224f88aebdcd57b71f31c831ab41389cf9ee3d21 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Tue, 6 Jan 2026 11:51:53 +0500 Subject: [PATCH] feat: rework proton sort Signed-off-by: Boris Yumankulov --- portprotonqt/version_utils.py | 124 +++++++++++++++------------------- 1 file changed, 55 insertions(+), 69 deletions(-) diff --git a/portprotonqt/version_utils.py b/portprotonqt/version_utils.py index 204bfe7..ab44811 100644 --- a/portprotonqt/version_utils.py +++ b/portprotonqt/version_utils.py @@ -1,93 +1,79 @@ import os +import re import urllib.parse - def version_sort_key(entry): """ Create a sort key for version-aware sorting of Proton/Wine entries. - This sorts alphabetically first, but within entries with the same prefix (like "wine-"), - it sorts by version number (descending). + Sorts by prefix alphabetically, then by version number (descending - newer first). """ if isinstance(entry, dict): - # If entry is a dict (from JSON metadata), extract name name = entry.get('name', '') - - # Извлекаем имя файла из URL если нет имени url = entry.get('url', '') if url and not name: parsed_url = urllib.parse.urlparse(url) name = os.path.basename(parsed_url.path) else: - # If entry is a string (directory name), use it directly name = entry - # Remove extensions to get clean name + # Remove extensions for ext in ['.tar.gz', '.tar.xz', '.zip']: if name.lower().endswith(ext): name = name[:-len(ext)] break - # Determine the prefix (e.g., "wine", "GE-Proton", "proton") for grouping - prefix = name.lower() - version_part = name + # Normalize the name + name_lower = name.lower().strip() - # Extract version part and prefix for different naming patterns - if name.startswith('GE-Proton-'): - # For "GE-Proton-9-25", prefix is "ge-proton", version is "9-25" - parts = name.split('-', 2) - if len(parts) >= 3: - prefix = f"{parts[0]}-{parts[1]}".lower() # "ge-proton" - version_part = parts[2] # "9-25" - elif len(parts) >= 2: - prefix = parts[0].lower() - version_part = parts[1] - elif name.lower().startswith('wine-'): - # For "wine-8.0-rc1", prefix is "wine", version is "8.0-rc1" - parts = name.split('-', 2) - if len(parts) >= 2: - prefix = parts[0].lower() # "wine" - if len(parts) >= 3: - version_part = f"{parts[1]}-{parts[2]}" # "8.0-rc1" - elif len(parts) >= 2: - version_part = parts[1] # "8.0" - elif name.lower().startswith('proton-'): - # For "proton-8.0-rc1", prefix is "proton", version is "8.0-rc1" - parts = name.split('-', 2) - if len(parts) >= 2: - prefix = parts[0].lower() # "proton" - if len(parts) >= 3: - version_part = f"{parts[1]}-{parts[2]}" # "8.0-rc1" - elif len(parts) >= 2: - version_part = parts[1] # "8.0" - else: - # For entries without standard prefixes, use the first part as prefix - if '-' in name: - prefix = name.split('-', 1)[0].lower() - else: - prefix = name.lower() - version_part = name + # Replace underscores and spaces with hyphens for consistency + normalized = name_lower.replace('_', '-').replace(' ', '-') - # Handle different version formats - create a proper version tuple for descending sorting - if '-' in version_part: - # Split on '-' and convert numeric parts for proper sorting (inverted for descending) - parts = version_part.split('-') - numeric_parts = [] - for part in parts: - try: - # Convert to negative integer for descending numeric sorting - numeric_parts.append(-int(part)) - except ValueError: - # For non-numeric parts, use a large number to sort them after numeric parts - # and append the lowercase string for consistent ordering - numeric_parts.append(float('inf')) - numeric_parts.append(part.lower()) - # Return tuple: (prefix for alphabetical sort, version for version sort, original name for tie-breaker) - return (prefix, numeric_parts, name.lower()) + # Extract all numeric sequences and text parts + tokens = re.findall(r'\d+|\D+', normalized) + + # Find where the version numbers start + # Usually after the first text token(s) + prefix_parts = [] + version_parts = [] + first_number_index = -1 + + # Find the first number token + for i, token in enumerate(tokens): + if token.isdigit(): + first_number_index = i + break + + # If we found a number, everything before it is prefix + if first_number_index > 0: + for i in range(first_number_index): + prefix_parts.append(tokens[i].strip('-')) + + # Everything from first number onwards is version + for i in range(first_number_index, len(tokens)): + token = tokens[i] + if token.isdigit(): + # Negative for descending order (higher versions first) + version_parts.append((0, -int(token))) + else: + # Part of version string (like "rc", "staging", etc.) + cleaned = token.strip('-') + if cleaned: + version_parts.append((1, cleaned)) else: - # If no dash in version part, try to parse as a simple version - try: - # Return tuple with prefix for alphabetical sort, version for version sort, and name for tie-breaker - return (prefix, [-int(version_part)], name.lower()) # Negative for descending order - except ValueError: - # For non-numeric versions, use prefix for alphabetical sort and version part for secondary sort - return (prefix, [float('inf'), version_part.lower()], name.lower()) + # No numbers found, treat entire thing as prefix + prefix_parts = [t.strip('-') for t in tokens if t.strip('-')] + + # Clean up prefix + prefix = '-'.join(p for p in prefix_parts if p).strip('-') + + # If no prefix found, use first token + if not prefix: + prefix = tokens[0] if tokens else normalized + + # If no version parts found, add a default + if not version_parts: + version_parts = [(0, 0)] + + # Return sort key: (prefix for grouping, version parts for version sorting, normalized name) + # Use normalized name for tie-breaking to avoid issues with spaces vs underscores + return (prefix, version_parts, normalized)