diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index e0a250ffee..ee0d8c2737 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -1,11 +1,12 @@ from __future__ import annotations +import threading from enum import Enum from pathlib import Path from typing import assert_never from archinstall.lib.models.packages import Repository -from archinstall.lib.packages.packages import list_available_packages +from archinstall.lib.packages.packages import enrich_package_info, list_available_packages from archinstall.lib.translationhandler import tr from archinstall.tui.curses_menu import EditMenu, SelectMenu, Tui from archinstall.tui.menu_item import MenuItem, MenuItemGroup @@ -177,7 +178,7 @@ def ask_additional_packages_to_install( MenuItem( name, value=pkg, - preview_action=lambda x: x.value.info(), + preview_action=None, # Will be set after menu_group is created ) for name, pkg in packages.items() ] @@ -194,6 +195,46 @@ def ask_additional_packages_to_install( menu_group = MenuItemGroup(items, sort_items=True) menu_group.set_selected_by_value(preset_packages) + # Helper to prefetch packages in both directions in background + def _prefetch_packages(group: MenuItemGroup, current_item: MenuItem) -> None: + try: + filtered_items = group.items + current_idx = filtered_items.index(current_item) + + # Collect next 50 packages (forward) + prefetch = [] + for i in range(current_idx + 1, min(current_idx + 51, len(filtered_items))): + next_pkg = filtered_items[i].value + if isinstance(next_pkg, AvailablePackage): + prefetch.append(next_pkg) + + # Collect previous 50 packages (backward) + for i in range(max(0, current_idx - 50), current_idx): + prev_pkg = filtered_items[i].value + if isinstance(prev_pkg, AvailablePackage): + prefetch.append(prev_pkg) + + if prefetch: + enrich_package_info(prefetch[0], prefetch=prefetch[1:]) + except (ValueError, IndexError): + pass + + # Preview function for packages - enriches current and prefetches ±50 packages + def preview_package(item: MenuItem) -> str: + pkg = item.value + if isinstance(pkg, AvailablePackage): + # Enrich current package synchronously + enrich_package_info(pkg) + # Prefetch next 50 in background thread + threading.Thread(target=_prefetch_packages, args=(menu_group, item), daemon=True).start() + return pkg.info() + return '' + + # Set preview action for package items only + for item in items: + if isinstance(item.value, AvailablePackage): + item.preview_action = preview_package + result = SelectMenu[AvailablePackage | PackageGroup]( menu_group, header=header, diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index c8d8e53276..169d35fd37 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -128,6 +128,52 @@ def check_package_upgrade(package: str) -> str | None: return None +def _create_package_stub(repo: str, name: str, version: str) -> AvailablePackage: + defaults = {field_name: '' for field_name in AvailablePackage.model_fields.keys()} + defaults.update({'repository': repo, 'name': name, 'version': version}) + return AvailablePackage(**defaults) + + +def _update_package(pkg: AvailablePackage, detailed: AvailablePackage) -> None: + for field_name in AvailablePackage.model_fields.keys(): + setattr(pkg, field_name, getattr(detailed, field_name)) + + +def enrich_package_info(pkg: AvailablePackage, prefetch: list[AvailablePackage] = []) -> None: + # Collect packages that need enrichment + to_enrich = [] + if not pkg.description: + to_enrich.append(pkg) + + for p in prefetch: + if not p.description: + to_enrich.append(p) + + if not to_enrich: + return + + # Batch fetch with single pacman call + try: + pkg_names = ' '.join(p.name for p in to_enrich) + current_package = [] + + for line in Pacman.run(f'-Si {pkg_names}'): + dec_line = line.decode().strip() + current_package.append(dec_line) + + if dec_line.startswith('Validated'): + if current_package: + detailed = _parse_package_output(current_package, AvailablePackage) + # Find matching package and update it + for p in to_enrich: + if p.name == detailed.name: + _update_package(p, detailed) + break + current_package = [] + except Exception: + pass + + @lru_cache def list_available_packages( repositories: tuple[Repository, ...], @@ -136,24 +182,21 @@ def list_available_packages( Returns a list of all available packages in the database """ packages: dict[str, AvailablePackage] = {} - current_package: list[str] = [] - filtered_repos = [repo.value for repo in repositories] try: Pacman.run('-Sy') except Exception as e: debug(f'Failed to sync Arch Linux package database: {e}') - for line in Pacman.run('-S --info'): - dec_line = line.decode().strip() - current_package.append(dec_line) - - if dec_line.startswith('Validated'): - if current_package: - avail_pkg = _parse_package_output(current_package, AvailablePackage) - if avail_pkg.repository in filtered_repos: - packages[avail_pkg.name] = avail_pkg - current_package = [] + # Load package stubs from repositories + for repo in repositories: + try: + for line in Pacman.run(f'-Sl {repo.value}'): + parts = line.decode().strip().split() + if len(parts) >= 3: + packages[parts[1]] = _create_package_stub(parts[0], parts[1], parts[2]) + except Exception as e: + debug(f'Failed to list packages from {repo.value}: {e}') return packages