Allow granular plasma configuration (#4389)

* Flexible plasma profile selection

* Update

* Update

* Update

* Update
This commit is contained in:
Daniel Girtler 2026-04-10 15:07:28 +10:00 committed by GitHub
parent 101f647319
commit b2f413124b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 191 additions and 70 deletions

View File

@ -1,7 +1,7 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess 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.menu.helpers import Selection
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
@ -17,7 +17,7 @@ class HyprlandProfile(Profile):
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
@ -45,7 +45,7 @@ class HyprlandProfile(Profile):
@property @property
@override @override
def services(self) -> list[str]: 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 [pref]
return [] return []
@ -57,7 +57,7 @@ class HyprlandProfile(Profile):
items = [MenuItem(s.value, value=s) for s in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True) 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) group.set_default_by_value(default)
result = await Selection[SeatAccess]( result = await Selection[SeatAccess](
@ -67,7 +67,7 @@ class HyprlandProfile(Profile):
).show() ).show()
if result.type_ == ResultType.Selection: if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:

View File

@ -1,7 +1,7 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess 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.menu.helpers import Selection
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
@ -17,13 +17,13 @@ class LabwcProfile(Profile):
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -39,7 +39,7 @@ class LabwcProfile(Profile):
@property @property
@override @override
def services(self) -> list[str]: 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 [pref]
return [] return []
@ -51,7 +51,7 @@ class LabwcProfile(Profile):
items = [MenuItem(s.value, value=s) for s in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True) 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) group.set_default_by_value(default)
result = await Selection[SeatAccess]( result = await Selection[SeatAccess](
@ -61,7 +61,7 @@ class LabwcProfile(Profile):
).show() ).show()
if result.type_ == ResultType.Selection: if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:

View File

@ -1,7 +1,7 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess 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.menu.helpers import Selection
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
@ -17,13 +17,13 @@ class NiriProfile(Profile):
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -47,7 +47,7 @@ class NiriProfile(Profile):
@property @property
@override @override
def services(self) -> list[str]: 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 [pref]
return [] return []
@ -59,7 +59,7 @@ class NiriProfile(Profile):
items = [MenuItem(s.value, value=s) for s in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True) 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) group.set_default_by_value(default)
result = await Selection[SeatAccess]( result = await Selection[SeatAccess](
@ -69,7 +69,7 @@ class NiriProfile(Profile):
).show() ).show()
if result.type_ == ResultType.Selection: if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:

View File

@ -1,6 +1,67 @@
from enum import StrEnum
from typing import override 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): class PlasmaProfile(Profile):
@ -15,18 +76,45 @@ class PlasmaProfile(Profile):
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
return [ flavor_str = self.custom_settings.get(CustomSetting.PlasmaFlavor)
'plasma-desktop',
'kscreen', if flavor_str is not None:
'plasma-pa', flavor = PlasmaFlavor(flavor_str)
'konsole', return flavor.packages()
'kate', else:
'dolphin', return PlasmaFlavor.Meta.packages() # use plasma-meta as the recommended default
'ark',
'plasma-workspace',
]
@property @property
@override @override
def default_greeter_type(self) -> GreeterType: def default_greeter_type(self) -> GreeterType:
return GreeterType.PlasmaLoginManager 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()

View File

@ -1,7 +1,7 @@
from typing import override from typing import override
from archinstall.default_profiles.desktops import SeatAccess 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.menu.helpers import Selection
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
@ -17,13 +17,13 @@ class SwayProfile(Profile):
display_server=DisplayServerType.Wayland, display_server=DisplayServerType.Wayland,
) )
self.custom_settings = {'seat_access': None} self.custom_settings = {CustomSetting.SeatAccess: None}
@property @property
@override @override
def packages(self) -> list[str]: def packages(self) -> list[str]:
additional = [] additional = []
if seat := self.custom_settings.get('seat_access', None): if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
additional = [seat] additional = [seat]
return [ return [
@ -49,7 +49,7 @@ class SwayProfile(Profile):
@property @property
@override @override
def services(self) -> list[str]: 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 [pref]
return [] return []
@ -61,7 +61,7 @@ class SwayProfile(Profile):
items = [MenuItem(s.value, value=s) for s in SeatAccess] items = [MenuItem(s.value, value=s) for s in SeatAccess]
group = MenuItemGroup(items, sort_items=True) 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) group.set_default_by_value(default)
result = await Selection[SeatAccess]( result = await Selection[SeatAccess](
@ -71,7 +71,7 @@ class SwayProfile(Profile):
).show() ).show()
if result.type_ == ResultType.Selection: if result.type_ == ResultType.Selection:
self.custom_settings['seat_access'] = result.get_value().value self.custom_settings[CustomSetting.SeatAccess] = result.get_value().value
@override @override
async def do_on_select(self) -> None: async def do_on_select(self) -> None:

View File

@ -1,4 +1,4 @@
from enum import Enum, auto from enum import Enum, StrEnum, auto
from typing import TYPE_CHECKING, Self from typing import TYPE_CHECKING, Self
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -45,6 +45,11 @@ class SelectResult(Enum):
ResetCurrent = auto() ResetCurrent = auto()
class CustomSetting(StrEnum):
SeatAccess = 'seat_access'
PlasmaFlavor = 'plasma_flavor'
class Profile: class Profile:
def __init__( def __init__(
self, self,
@ -59,7 +64,7 @@ class Profile:
) -> None: ) -> None:
self.name = name self.name = name
self.profile_type = profile_type 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_gfx_driver = support_gfx_driver
self._support_greeter = support_greeter self._support_greeter = support_greeter
@ -128,7 +133,7 @@ class Profile:
""" """
return SelectResult.NewSelection 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. Set the custom settings for the profile.
This is also called when the settings are parsed from the config This is also called when the settings are parsed from the config

View File

@ -142,12 +142,26 @@ class AvailablePackage(BaseModel):
return output 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 @dataclass
class PackageGroup: class PackageGroup:
name: str name: str
packages: list[str] = field(default_factory=list) 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 @classmethod
def from_available_packages( def from_available_packages(
cls, cls,

View File

@ -34,6 +34,34 @@ def check_package_upgrade(package: str) -> str | None:
return 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 @lru_cache
def list_available_packages( def list_available_packages(
repositories: tuple[Repository, ...], repositories: tuple[Repository, ...],
@ -74,12 +102,18 @@ def _parse_package_output[PackageType: (AvailablePackage, LocalPackage)](
cls: type[PackageType], cls: type[PackageType],
) -> PackageType: ) -> PackageType:
package = {} package = {}
current_key = None
for line in package_meta: for line in package_meta:
if ':' in line: if not line.strip():
key, value = line.split(':', 1) continue
key = _normalize_key_name(key)
package[key] = value.strip() 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) return cls.model_validate(package)

View File

@ -7,7 +7,7 @@ from tempfile import NamedTemporaryFile
from types import ModuleType from types import ModuleType
from typing import TYPE_CHECKING, NotRequired, TypedDict 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.hardware import GfxDriver, GfxPackage
from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.networking import fetch_data_from_url from archinstall.lib.networking import fetch_data_from_url
@ -21,7 +21,7 @@ if TYPE_CHECKING:
class ProfileSerialization(TypedDict): class ProfileSerialization(TypedDict):
main: NotRequired[str] main: NotRequired[str]
details: NotRequired[list[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] path: NotRequired[str]
@ -73,29 +73,6 @@ class ProfileHandler:
else: else:
self._import_profile_from_url(url_path) 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): if main := profile_config.get('main', None):
profile = self.get_profile_by_name(main) if main else None profile = self.get_profile_by_name(main) if main else None
@ -109,7 +86,7 @@ class ProfileHandler:
if details: if details:
for detail in filter(None, details): for detail in filter(None, details):
# [2024-04-19] TODO: Backwards compatibility after naming change: https://github.com/archlinux/archinstall/pull/2421 # [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': if detail == 'Kde':
detail = 'KDE Plasma' detail = 'KDE Plasma'

View File

@ -4,7 +4,7 @@ from pathlib import Path
from pytest import MonkeyPatch 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.args import ArchConfig, ArchConfigHandler, Arguments
from archinstall.lib.hardware import GfxDriver from archinstall.lib.hardware import GfxDriver
from archinstall.lib.models.application import ( from archinstall.lib.models.application import (
@ -173,10 +173,13 @@ def test_config_file_parsing(
{ {
'custom_settings': { 'custom_settings': {
'Hyprland': { 'Hyprland': {
'seat_access': 'polkit', CustomSetting.SeatAccess: 'polkit',
}, },
'Sway': { 'Sway': {
'seat_access': 'seatd', CustomSetting.SeatAccess: 'seatd',
},
'KDE Plasma': {
CustomSetting.PlasmaFlavor: 'plasma-meta',
}, },
}, },
'details': [ 'details': [