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 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:

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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': [