diff --git a/archinstall/default_profiles/desktops/hyprland.py b/archinstall/default_profiles/desktops/hyprland.py index 84b94aa0..fa0bc581 100644 --- a/archinstall/default_profiles/desktops/hyprland.py +++ b/archinstall/default_profiles/desktops/hyprland.py @@ -1,7 +1,7 @@ from typing import override from archinstall.default_profiles.desktops import SeatAccess -from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.lib.menu.helpers import Selection from archinstall.lib.translationhandler import tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup @@ -17,7 +17,7 @@ class HyprlandProfile(Profile): display_server=DisplayServerType.Wayland, ) - self.custom_settings = {'seat_access': None} + self.custom_settings = {CustomSetting.SeatAccess: None} @property @override @@ -45,7 +45,7 @@ class HyprlandProfile(Profile): @property @override def services(self) -> list[str]: - if pref := self.custom_settings.get('seat_access', None): + if pref := self.custom_settings.get(CustomSetting.SeatAccess, None): return [pref] return [] @@ -57,7 +57,7 @@ class HyprlandProfile(Profile): items = [MenuItem(s.value, value=s) for s in SeatAccess] group = MenuItemGroup(items, sort_items=True) - default = self.custom_settings.get('seat_access', None) + default = self.custom_settings.get(CustomSetting.SeatAccess, None) group.set_default_by_value(default) result = await Selection[SeatAccess]( @@ -67,7 +67,7 @@ class HyprlandProfile(Profile): ).show() if result.type_ == ResultType.Selection: - self.custom_settings['seat_access'] = result.get_value().value + self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value @override async def do_on_select(self) -> None: diff --git a/archinstall/default_profiles/desktops/labwc.py b/archinstall/default_profiles/desktops/labwc.py index d109ef13..bf4a3297 100644 --- a/archinstall/default_profiles/desktops/labwc.py +++ b/archinstall/default_profiles/desktops/labwc.py @@ -1,7 +1,7 @@ from typing import override from archinstall.default_profiles.desktops import SeatAccess -from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.lib.menu.helpers import Selection from archinstall.lib.translationhandler import tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup @@ -17,13 +17,13 @@ class LabwcProfile(Profile): display_server=DisplayServerType.Wayland, ) - self.custom_settings = {'seat_access': None} + self.custom_settings = {CustomSetting.SeatAccess: None} @property @override def packages(self) -> list[str]: additional = [] - if seat := self.custom_settings.get('seat_access', None): + if seat := self.custom_settings.get(CustomSetting.SeatAccess, None): additional = [seat] return [ @@ -39,7 +39,7 @@ class LabwcProfile(Profile): @property @override def services(self) -> list[str]: - if pref := self.custom_settings.get('seat_access', None): + if pref := self.custom_settings.get(CustomSetting.SeatAccess, None): return [pref] return [] @@ -51,7 +51,7 @@ class LabwcProfile(Profile): items = [MenuItem(s.value, value=s) for s in SeatAccess] group = MenuItemGroup(items, sort_items=True) - default = self.custom_settings.get('seat_access', None) + default = self.custom_settings.get(CustomSetting.SeatAccess, None) group.set_default_by_value(default) result = await Selection[SeatAccess]( @@ -61,7 +61,7 @@ class LabwcProfile(Profile): ).show() if result.type_ == ResultType.Selection: - self.custom_settings['seat_access'] = result.get_value().value + self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value @override async def do_on_select(self) -> None: diff --git a/archinstall/default_profiles/desktops/niri.py b/archinstall/default_profiles/desktops/niri.py index b05f3068..347290fe 100644 --- a/archinstall/default_profiles/desktops/niri.py +++ b/archinstall/default_profiles/desktops/niri.py @@ -1,7 +1,7 @@ from typing import override from archinstall.default_profiles.desktops import SeatAccess -from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.lib.menu.helpers import Selection from archinstall.lib.translationhandler import tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup @@ -17,13 +17,13 @@ class NiriProfile(Profile): display_server=DisplayServerType.Wayland, ) - self.custom_settings = {'seat_access': None} + self.custom_settings = {CustomSetting.SeatAccess: None} @property @override def packages(self) -> list[str]: additional = [] - if seat := self.custom_settings.get('seat_access', None): + if seat := self.custom_settings.get(CustomSetting.SeatAccess, None): additional = [seat] return [ @@ -47,7 +47,7 @@ class NiriProfile(Profile): @property @override def services(self) -> list[str]: - if pref := self.custom_settings.get('seat_access', None): + if pref := self.custom_settings.get(CustomSetting.SeatAccess, None): return [pref] return [] @@ -59,7 +59,7 @@ class NiriProfile(Profile): items = [MenuItem(s.value, value=s) for s in SeatAccess] group = MenuItemGroup(items, sort_items=True) - default = self.custom_settings.get('seat_access', None) + default = self.custom_settings.get(CustomSetting.SeatAccess, None) group.set_default_by_value(default) result = await Selection[SeatAccess]( @@ -69,7 +69,7 @@ class NiriProfile(Profile): ).show() if result.type_ == ResultType.Selection: - self.custom_settings['seat_access'] = result.get_value().value + self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value @override async def do_on_select(self) -> None: diff --git a/archinstall/default_profiles/desktops/plasma.py b/archinstall/default_profiles/desktops/plasma.py index 81f9fa78..e90f8a27 100644 --- a/archinstall/default_profiles/desktops/plasma.py +++ b/archinstall/default_profiles/desktops/plasma.py @@ -1,6 +1,67 @@ +from enum import StrEnum from typing import override -from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.lib.menu.helpers import Selection +from archinstall.lib.packages.packages import available_package, package_group_info +from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.result import ResultType + + +class PlasmaFlavor(StrEnum): + Meta = 'plasma-meta' + Plasma = 'plasma' + Desktop = 'plasma-desktop' + + def show(self) -> str: + match self: + case PlasmaFlavor.Meta: + return f'{self.value} ({tr("Recommended")})' + case PlasmaFlavor.Plasma | PlasmaFlavor.Desktop: + return self.value + + def package_details(self) -> str: + ty = '' + details = '' + desc = '' + + match self: + case PlasmaFlavor.Meta: + ty = tr('Package') + desc = tr('Curated selection of KDE Plasma packages') + info = available_package(self.value) + + if info is not None: + details = tr('Dependencies') + '\n' + details += '\n'.join(f'- {entry}' for entry in info.get_depends_on) + case PlasmaFlavor.Plasma: + ty = tr('Package group') + desc = tr('Extensive KDE Plasma installation') + group = package_group_info(self.value) + + if group is not None: + details = tr('Packages in group') + '\n' + details += '\n'.join(f'- {entry}' for entry in group.packages) + case PlasmaFlavor.Desktop: + ty = tr('Package group') + desc = tr('Minimal KDE Plasma installation') + info = available_package(self.value) + + if info is not None: + details = tr('Dependencies') + '\n' + details += '\n'.join(f'- {entry}' for entry in info.get_depends_on) + + return f'{tr("Type")}: {ty}\n{tr("Description")}: {desc}\n\n{details}' + + def packages(self) -> list[str]: + match self: + case PlasmaFlavor.Meta: + return ['plasma-meta'] + case PlasmaFlavor.Plasma: + return ['plasma'] + case PlasmaFlavor.Desktop: + return ['plasma-desktop'] class PlasmaProfile(Profile): @@ -15,18 +76,45 @@ class PlasmaProfile(Profile): @property @override def packages(self) -> list[str]: - return [ - 'plasma-desktop', - 'kscreen', - 'plasma-pa', - 'konsole', - 'kate', - 'dolphin', - 'ark', - 'plasma-workspace', - ] + flavor_str = self.custom_settings.get(CustomSetting.PlasmaFlavor) + + if flavor_str is not None: + flavor = PlasmaFlavor(flavor_str) + return flavor.packages() + else: + return PlasmaFlavor.Meta.packages() # use plasma-meta as the recommended default @property @override def default_greeter_type(self) -> GreeterType: return GreeterType.PlasmaLoginManager + + async def _select_flavor(self) -> None: + header = tr('Select a flavor of KDE Plasma to install') + '\n' + + items = [ + MenuItem( + s.show(), + value=s, + preview_action=lambda x: x.value.package_details() if x.value else None, + ) + for s in PlasmaFlavor + ] + group = MenuItemGroup(items, sort_items=False) + + default = self.custom_settings.get(CustomSetting.PlasmaFlavor, None) + group.set_default_by_value(default) + + result = await Selection[PlasmaFlavor]( + group, + header=header, + allow_skip=False, + preview_location='right', + ).show() + + if result.type_ == ResultType.Selection: + self.custom_settings[CustomSetting.PlasmaFlavor] = result.get_value().value + + @override + async def do_on_select(self) -> None: + await self._select_flavor() diff --git a/archinstall/default_profiles/desktops/sway.py b/archinstall/default_profiles/desktops/sway.py index 09a8e7fd..5d222307 100644 --- a/archinstall/default_profiles/desktops/sway.py +++ b/archinstall/default_profiles/desktops/sway.py @@ -1,7 +1,7 @@ from typing import override from archinstall.default_profiles.desktops import SeatAccess -from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType +from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType from archinstall.lib.menu.helpers import Selection from archinstall.lib.translationhandler import tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup @@ -17,13 +17,13 @@ class SwayProfile(Profile): display_server=DisplayServerType.Wayland, ) - self.custom_settings = {'seat_access': None} + self.custom_settings = {CustomSetting.SeatAccess: None} @property @override def packages(self) -> list[str]: additional = [] - if seat := self.custom_settings.get('seat_access', None): + if seat := self.custom_settings.get(CustomSetting.SeatAccess, None): additional = [seat] return [ @@ -49,7 +49,7 @@ class SwayProfile(Profile): @property @override def services(self) -> list[str]: - if pref := self.custom_settings.get('seat_access', None): + if pref := self.custom_settings.get(CustomSetting.SeatAccess, None): return [pref] return [] @@ -61,7 +61,7 @@ class SwayProfile(Profile): items = [MenuItem(s.value, value=s) for s in SeatAccess] group = MenuItemGroup(items, sort_items=True) - default = self.custom_settings.get('seat_access', None) + default = self.custom_settings.get(CustomSetting.SeatAccess, None) group.set_default_by_value(default) result = await Selection[SeatAccess]( @@ -71,7 +71,7 @@ class SwayProfile(Profile): ).show() if result.type_ == ResultType.Selection: - self.custom_settings['seat_access'] = result.get_value().value + self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value @override async def do_on_select(self) -> None: diff --git a/archinstall/default_profiles/profile.py b/archinstall/default_profiles/profile.py index a8551682..b54ce389 100644 --- a/archinstall/default_profiles/profile.py +++ b/archinstall/default_profiles/profile.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum, StrEnum, auto from typing import TYPE_CHECKING, Self from archinstall.lib.translationhandler import tr @@ -45,6 +45,11 @@ class SelectResult(Enum): ResetCurrent = auto() +class CustomSetting(StrEnum): + SeatAccess = 'seat_access' + PlasmaFlavor = 'plasma_flavor' + + class Profile: def __init__( self, @@ -59,7 +64,7 @@ class Profile: ) -> None: self.name = name self.profile_type = profile_type - self.custom_settings: dict[str, str | None] = {} + self.custom_settings: dict[CustomSetting, str | None] = {} self._support_gfx_driver = support_gfx_driver self._support_greeter = support_greeter @@ -128,7 +133,7 @@ class Profile: """ return SelectResult.NewSelection - def set_custom_settings(self, settings: dict[str, str | None]) -> None: + def set_custom_settings(self, settings: dict[CustomSetting, str | None]) -> None: """ Set the custom settings for the profile. This is also called when the settings are parsed from the config diff --git a/archinstall/lib/models/packages.py b/archinstall/lib/models/packages.py index f08cbfe1..6ba6e134 100644 --- a/archinstall/lib/models/packages.py +++ b/archinstall/lib/models/packages.py @@ -142,12 +142,26 @@ class AvailablePackage(BaseModel): return output + @cached_property + def get_depends_on(self) -> list[str]: + return [entry.strip() for entry in self.depends_on.split(' ') if entry.strip()] + + @cached_property + def get_optional_deps(self) -> list[str]: + return [entry.strip() for entry in self.optional_deps.split(' ') if entry.strip()] + @dataclass class PackageGroup: name: str packages: list[str] = field(default_factory=list) + @classmethod + def from_package_group_output(cls, data: list[str]) -> Self: + name = data[0].split()[0].strip() + packages = [line.split()[1].strip() for line in data if line.strip()] + return cls(name, packages) + @classmethod def from_available_packages( cls, diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index 28e90bc0..3572c6c4 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -34,6 +34,34 @@ def check_package_upgrade(package: str) -> str | None: return None +@lru_cache +def package_group_info(package: str) -> PackageGroup | None: + try: + package_info: list[str] = [] + for line in Pacman.run(f'-Sg {package}'): + package_info.append(line.decode().strip()) + group = PackageGroup.from_package_group_output(package_info) + return group + except SysCallError: + debug(f'Failed to get package info: {package}') + + return None + + +@lru_cache +def available_package(package: str) -> AvailablePackage | None: + try: + package_info: list[str] = [] + for line in Pacman.run(f'-S --info {package}'): + package_info.append(line.decode().strip()) + + return _parse_package_output(package_info, AvailablePackage) + except SysCallError: + pass + + return None + + @lru_cache def list_available_packages( repositories: tuple[Repository, ...], @@ -74,12 +102,18 @@ def _parse_package_output[PackageType: (AvailablePackage, LocalPackage)]( cls: type[PackageType], ) -> PackageType: package = {} + current_key = None for line in package_meta: - if ':' in line: - key, value = line.split(':', 1) - key = _normalize_key_name(key) - package[key] = value.strip() + if not line.strip(): + continue + + if ':' in line and not line.startswith(' '): + key_raw, value = line.split(':', 1) + current_key = _normalize_key_name(key_raw) + package[current_key] = value.strip() + elif current_key: + package[current_key] += ' ' + line.strip() return cls.model_validate(package) diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index 15f09128..c75f95c0 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -7,7 +7,7 @@ from tempfile import NamedTemporaryFile from types import ModuleType from typing import TYPE_CHECKING, NotRequired, TypedDict -from archinstall.default_profiles.profile import GreeterType, Profile +from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile from archinstall.lib.hardware import GfxDriver, GfxPackage from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.networking import fetch_data_from_url @@ -21,7 +21,7 @@ if TYPE_CHECKING: class ProfileSerialization(TypedDict): main: NotRequired[str] details: NotRequired[list[str]] - custom_settings: NotRequired[dict[str, dict[str, str | None]]] + custom_settings: NotRequired[dict[str, dict[CustomSetting, str | None]]] path: NotRequired[str] @@ -73,29 +73,6 @@ class ProfileHandler: else: self._import_profile_from_url(url_path) - # if custom := profile_config.get('custom', None): - # from archinstall.default_profiles.custom import CustomTypeProfile - # custom_types = [] - # - # for entry in custom: - # custom_types.append( - # CustomTypeProfile( - # entry['name'], - # entry['enabled'], - # entry.get('packages', []), - # entry.get('services', []) - # ) - # ) - # - # self.remove_custom_profiles(custom_types) - # self.add_custom_profiles(custom_types) - # - # # this doesn't mean it's actual going to be set as a selection - # # but we are simply populating the custom profile with all - # # possible custom definitions - # if custom_profile := self.get_profile_by_name('Custom'): - # custom_profile.set_current_selection(custom_types) - if main := profile_config.get('main', None): profile = self.get_profile_by_name(main) if main else None @@ -109,7 +86,7 @@ class ProfileHandler: if details: for detail in filter(None, details): # [2024-04-19] TODO: Backwards compatibility after naming change: https://github.com/archlinux/archinstall/pull/2421 - # 'Kde' is deprecated, remove this block in a future version + # 'Kde' is deprecated, remove this block in a future version if detail == 'Kde': detail = 'KDE Plasma' diff --git a/tests/test_args.py b/tests/test_args.py index 275ffd86..1b052ec0 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -4,7 +4,7 @@ from pathlib import Path from pytest import MonkeyPatch -from archinstall.default_profiles.profile import GreeterType +from archinstall.default_profiles.profile import CustomSetting, GreeterType from archinstall.lib.args import ArchConfig, ArchConfigHandler, Arguments from archinstall.lib.hardware import GfxDriver from archinstall.lib.models.application import ( @@ -173,10 +173,13 @@ def test_config_file_parsing( { 'custom_settings': { 'Hyprland': { - 'seat_access': 'polkit', + CustomSetting.SeatAccess: 'polkit', }, 'Sway': { - 'seat_access': 'seatd', + CustomSetting.SeatAccess: 'seatd', + }, + 'KDE Plasma': { + CustomSetting.PlasmaFlavor: 'plasma-meta', }, }, 'details': [