From 294eea0a1c6ed409ed8cd13e6c3adb6b75730a99 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Tue, 1 Apr 2025 07:34:20 +1100 Subject: [PATCH] Fix 3298 - Add package gruops to selection (#3322) --- archinstall/lib/interactions/general_conf.py | 27 ++++++++---- archinstall/lib/models/packages.py | 45 +++++++++++++++++++- archinstall/tui/menu_item.py | 26 ++++++++--- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index cfda677f..91f90207 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -11,7 +11,7 @@ from archinstall.tui.types import Alignment, FrameProperties, Orientation, Previ from ..locale.utils import list_timezones from ..models.audio_configuration import Audio, AudioConfiguration -from ..models.packages import AvailablePackage +from ..models.packages import AvailablePackage, PackageGroup from ..output import warn from ..translationhandler import Language @@ -174,30 +174,41 @@ def ask_additional_packages_to_install( repositories |= {Repository.Core, Repository.Extra} packages = list_available_packages(tuple(repositories)) + package_groups = PackageGroup.from_available_packages(packages) # Additional packages (with some light weight error handling for invalid package names) header = str(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + '\n' header += str(_('Select any packages from the below list that should be installed additionally')) + '\n' # there are over 15k packages so this needs to be quick - preset_packages = [] + preset_packages: list[AvailablePackage | PackageGroup] = [] for p in preset: if p in packages: preset_packages.append(packages[p]) + elif p in package_groups: + preset_packages.append(package_groups[p]) items = [ MenuItem( name, value=pkg, preview_action=lambda x: x.value.info() - ) for name, - pkg in packages.items() + ) for name, pkg in packages.items() ] - group = MenuItemGroup(items, sort_items=True) - group.set_selected_by_value(preset_packages) + + items += [ + MenuItem( + name, + value=group, + preview_action=lambda x: x.value.info() + ) for name, group in package_groups.items() + ] + + menu_group = MenuItemGroup(items, sort_items=True) + menu_group.set_selected_by_value(preset_packages) result = SelectMenu( - group, + menu_group, header=header, alignment=Alignment.LEFT, allow_reset=True, @@ -214,7 +225,7 @@ def ask_additional_packages_to_install( case ResultType.Reset: return [] case ResultType.Selection: - selected_pacakges: list[AvailablePackage] = result.get_values() + selected_pacakges: list[AvailablePackage | PackageGroup] = result.get_values() return [pkg.name for pkg in selected_pacakges] diff --git a/archinstall/lib/models/packages.py b/archinstall/lib/models/packages.py index 0e2e05d7..53cc323f 100644 --- a/archinstall/lib/models/packages.py +++ b/archinstall/lib/models/packages.py @@ -1,10 +1,17 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from functools import cached_property -from typing import Any, override +from typing import TYPE_CHECKING, Any, override from pydantic import BaseModel +if TYPE_CHECKING: + from collections.abc import Callable + + from archinstall.lib.translationhandler import DeferredTranslation + + _: Callable[[str], DeferredTranslation] + class Repository(Enum): Core = 'core' @@ -166,3 +173,37 @@ class AvailablePackage(BaseModel): output += f'{key} : {value}\n' return output + + +@dataclass +class PackageGroup: + name: str + packages: list[str] = field(default_factory=list) + + @classmethod + def from_available_packages( + cls, + packages: dict[str, AvailablePackage] + ) -> dict[str, 'PackageGroup']: + pkg_groups: dict[str, 'PackageGroup'] = {} + + for pkg in packages.values(): + if 'None' in pkg.groups: + continue + + groups = pkg.groups.split(' ') + + for group in groups: + # same group names have multiple spaces in between + if len(group) == 0: + continue + + pkg_groups.setdefault(group, PackageGroup(group)) + pkg_groups[group].packages.append(pkg.name) + + return pkg_groups + + def info(self) -> str: + output = str(_('Package group:')) + '\n - ' + output += '\n - '.join(self.packages) + return output diff --git a/archinstall/tui/menu_item.py b/archinstall/tui/menu_item.py index 65d7e39b..48646583 100644 --- a/archinstall/tui/menu_item.py +++ b/archinstall/tui/menu_item.py @@ -89,7 +89,7 @@ class MenuItemGroup: raise ValueError('Selected item not in menu') self.menu_items: list[MenuItem] = menu_items - self.focus_item: MenuItem = focus_item + self.focus_item: MenuItem | None = focus_item self.selected_items: list[MenuItem] = [] self.default_item: MenuItem | None = default_item @@ -146,7 +146,14 @@ class MenuItemGroup: def index_focus(self) -> int | None: if self.focus_item and self.items: - return self.items.index(self.focus_item) + try: + return self.items.index(self.focus_item) + except ValueError: + # on large menus (15k+) when filtering very quickly + # the index search is too slow while the items are reduced + # by the filter and it will blow up as it cannot find the + # focus item + pass return None @@ -227,6 +234,8 @@ class MenuItemGroup: if len(self.items) > 0: if self.focus_item not in self.items: self.focus_first() + else: + self.focus_item = None def is_item_selected(self, item: MenuItem) -> bool: return item in self.selected_items @@ -261,22 +270,25 @@ class MenuItemGroup: self.focus_item = last_item def focus_prev(self, skip_empty: bool = True) -> None: - assert self.focus_item is not None + # e.g. when filter shows no items + if self.focus_item is None: + return + item = self._find_next_selectable_item(self.items, self.focus_item, -1) if item is not None: self.focus_item = item def focus_next(self, skip_not_enabled: bool = True) -> None: - assert self.focus_item is not None + # e.g. when filter shows no items + if self.focus_item is None: + return + item = self._find_next_selectable_item(self.items, self.focus_item, 1) if item is not None: self.focus_item = item - def get_focus_index(self) -> int: - return self.items.index(self.focus_item) - def _find_next_selectable_item( self, items: list[MenuItem],