diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index d8aaaa83..4a96b5d4 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -6,6 +6,7 @@ import urllib.error import urllib.parse from argparse import ArgumentParser, Namespace from dataclasses import dataclass, field +from enum import StrEnum from pathlib import Path from typing import Any, Self from urllib.request import Request, urlopen @@ -17,6 +18,7 @@ from archinstall.lib.menu.util import get_password from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration +from archinstall.lib.models.config import SubConfig from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration @@ -57,6 +59,72 @@ class Arguments: verbose: bool = False +class ArchConfigType(StrEnum): + VERSION = 'version' + SCRIPT = 'script' + LOCALE_CONFIG = 'locale_config' + ARCHINSTALL_LANGUAGE = 'archinstall_language' + DISK_CONFIG = 'disk_config' + PROFILE_CONFIG = 'profile_config' + MIRROR_CONFIG = 'mirror_config' + NETWORK_CONFIG = 'network_config' + BOOTLOADER_CONFIG = 'bootloader_config' + APP_CONFIG = 'app_config' + AUTH_CONFIG = 'auth_config' + SWAP = 'swap' + HOSTNAME = 'hostname' + KERNELS = 'kernels' + NTP = 'ntp' + TIMEZONE = 'timezone' + SERVICES = 'services' + PACKAGES = 'packages' + PACMAN_CONFIG = 'pacman_config' + CUSTOM_COMMANDS = 'custom_commands' + + def text(self) -> str: + match self: + case ArchConfigType.ARCHINSTALL_LANGUAGE: + return tr('ArchInstall Language') + case ArchConfigType.VERSION: + return tr('Version') + case ArchConfigType.SCRIPT: + return tr('Installation Script') + case ArchConfigType.LOCALE_CONFIG: + return tr('Locales') + case ArchConfigType.DISK_CONFIG: + return tr('Disk configuration') + case ArchConfigType.PROFILE_CONFIG: + return tr('Profile') + case ArchConfigType.MIRROR_CONFIG: + return tr('Mirrors and repositories') + case ArchConfigType.NETWORK_CONFIG: + return tr('Network') + case ArchConfigType.BOOTLOADER_CONFIG: + return tr('Bootloader') + case ArchConfigType.APP_CONFIG: + return tr('Application') + case ArchConfigType.AUTH_CONFIG: + return tr('Authentication') + case ArchConfigType.SWAP: + return tr('Swap') + case ArchConfigType.HOSTNAME: + return tr('Hostname') + case ArchConfigType.KERNELS: + return tr('Kernels') + case ArchConfigType.NTP: + return tr('Automatic time sync (NTP)') + case ArchConfigType.TIMEZONE: + return tr('Timezone') + case ArchConfigType.SERVICES: + return tr('Services') + case ArchConfigType.PACKAGES: + return tr('Additional packages') + case ArchConfigType.PACMAN_CONFIG: + return tr('Pacman') + case ArchConfigType.CUSTOM_COMMANDS: + return tr('Custom commands') + + @dataclass class ArchConfig: version: str | None = None @@ -97,41 +165,67 @@ class ArchConfig: return config - def safe_config(self) -> dict[str, Any]: - config: Any = { - 'version': self.version, - 'script': self.script, - 'archinstall-language': self.archinstall_language.json(), - 'hostname': self.hostname, - 'kernels': self.kernels, - 'ntp': self.ntp, - 'packages': self.packages, - 'pacman_config': self.pacman_config.json(), - 'swap': self.swap, - 'timezone': self.timezone, - 'services': self.services, - 'custom_commands': self.custom_commands, - 'bootloader_config': self.bootloader_config.json() if self.bootloader_config else None, - 'app_config': self.app_config.json() if self.app_config else None, - 'auth_config': self.auth_config.json() if self.auth_config else None, + def safe_config(self) -> dict[ArchConfigType, Any]: + base_config: dict[ArchConfigType, Any] = { + ArchConfigType.VERSION: self.version, + ArchConfigType.SCRIPT: self.script, + ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(), } - if self.locale_config: - config['locale_config'] = self.locale_config.json() + base_config.update(self.plain_cfg()) + sub_config = self.sub_cfg() - if self.disk_config: - config['disk_config'] = self.disk_config.json() + for config_type, value in sub_config.items(): + if not hasattr(value, 'json'): + raise ValueError(f'Config value for {config_type} must implement json() method') + base_config[config_type] = value.json() - if self.profile_config: - config['profile_config'] = self.profile_config.json() + return base_config + + def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]: + return { + ArchConfigType.HOSTNAME: self.hostname, + ArchConfigType.KERNELS: self.kernels, + ArchConfigType.NTP: self.ntp, + ArchConfigType.TIMEZONE: self.timezone, + ArchConfigType.SERVICES: self.services, + ArchConfigType.PACKAGES: self.packages, + ArchConfigType.CUSTOM_COMMANDS: self.custom_commands, + } + + def sub_cfg(self) -> dict[ArchConfigType, SubConfig]: + cfg: dict[ArchConfigType, SubConfig] = { + ArchConfigType.PACMAN_CONFIG: self.pacman_config, + } if self.mirror_config: - config['mirror_config'] = self.mirror_config.json() + cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config + + if self.bootloader_config: + cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config + + if self.disk_config: + cfg[ArchConfigType.DISK_CONFIG] = self.disk_config + + if self.swap: + cfg[ArchConfigType.SWAP] = self.swap + + if self.auth_config: + cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config + + if self.locale_config: + cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config + + if self.profile_config: + cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config if self.network_config: - config['network_config'] = self.network_config.json() + cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config - return config + if self.app_config: + cfg[ArchConfigType.APP_CONFIG] = self.app_config + + return cfg @classmethod def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index aba835d9..8cc2e998 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -6,13 +6,12 @@ from typing import Any from pydantic import TypeAdapter -from archinstall.lib.args import ArchConfig +from archinstall.lib.args import ArchConfig, ArchConfigType from archinstall.lib.crypt import encrypt from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.util import get_password, prompt_dir -from archinstall.lib.models.bootloader import Bootloader from archinstall.lib.models.network import NetworkConfiguration -from archinstall.lib.output import debug, logger, warn +from archinstall.lib.output import as_key_value_pair, debug, logger, warn from archinstall.lib.translationhandler import tr from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -45,7 +44,7 @@ class ConfigurationOutput: def user_config_to_json(self) -> str: config = self._config.safe_config() - adapter = TypeAdapter(dict[str, Any]) + adapter = TypeAdapter(dict[ArchConfigType, Any]) python_dict = adapter.dump_python(config) return json.dumps(python_dict, indent=4, sort_keys=True) @@ -64,65 +63,24 @@ class ConfigurationOutput: """ Render a concise two-column summary of the current configuration. - The left column holds section labels, the right column holds values. - Column width adapts to the longest translated label so translations - do not break the alignment. Rows whose underlying config is not set - are skipped. - Returns an empty string if nothing meaningful to show. """ - rows: list[tuple[str, str]] = [] + cfg: dict[str, str | list[str] | bool] = {} - disk_config = self._config.disk_config - if disk_config and disk_config.device_modifications: - disk_parts: list[str] = [] - for mod in disk_config.device_modifications: - path = str(mod.device_path) - root_part = mod.get_root_partition() - flags: list[str] = [] - if root_part and root_part.fs_type: - flags.append(root_part.fs_type.value) - if disk_config.disk_encryption: - flags.append(tr('LUKS')) - disk_parts.append(f'{path} ({" + ".join(flags)})' if flags else path) - rows.append((tr('Disks'), ', '.join(disk_parts))) + for key, value in self._config.plain_cfg().items(): + cfg[key.text()] = value - bl_config = self._config.bootloader_config - if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER: - rows.append((tr('Bootloader'), bl_config.bootloader.value)) + for config_type, obj in self._config.sub_cfg().items(): + if not hasattr(obj, 'summary'): + continue - kernels = self._config.kernels - if kernels: - rows.append((tr('Kernel'), ', '.join(kernels))) + summary = obj.summary() + if summary: + cfg[config_type.text()] = summary - profile_config = self._config.profile_config - if profile_config and profile_config.profile: - names = profile_config.profile.current_selection_names() - rows.append((tr('Profile'), ', '.join(names) if names else profile_config.profile.name)) - if profile_config.greeter: - rows.append((tr('Greeter'), profile_config.greeter.value)) + simple_summary = as_key_value_pair(cfg, ignore_empty=True) - packages = self._config.packages - if packages: - rows.append((tr('Packages'), str(len(packages)))) - - net_config = self._config.network_config - if isinstance(net_config, NetworkConfiguration): - rows.append((tr('Network'), net_config.type.display_msg())) - - locale_config = self._config.locale_config - if locale_config: - rows.append((tr('Locale'), locale_config.sys_lang)) - - tz = self._config.timezone - if tz: - rows.append((tr('Timezone'), tz)) - - if not rows: - return '' - - label_width = max(len(label) for label, _ in rows) + 2 - return '\n'.join(f'{label:<{label_width}}{value}' for label, value in rows) + return simple_summary async def confirm_config(self, show_install_warnings: bool = False) -> bool: header = f'{tr("The specified configuration will be applied")}. ' diff --git a/archinstall/lib/models/application.py b/archinstall/lib/models/application.py index 04d1a342..1e97a784 100644 --- a/archinstall/lib/models/application.py +++ b/archinstall/lib/models/application.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from enum import StrEnum, auto -from typing import Any, NotRequired, Self, TypedDict +from typing import Any, NotRequired, Self, TypedDict, override +from archinstall.lib.models.config import SubConfig from archinstall.lib.translationhandler import tr @@ -168,7 +169,7 @@ class FontsConfiguration: @dataclass(frozen=True) -class ZramConfiguration: +class ZramConfiguration(SubConfig): enabled: bool algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD @@ -181,9 +182,28 @@ class ZramConfiguration: algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value)) return cls(enabled=enabled, algorithm=ZramAlgorithm(algo)) + @override + def json(self) -> dict[str, bool | str]: + return { + 'enabled': self.enabled, + 'algorithm': self.algorithm.value, + } + + @override + def summary(self) -> list[str] | None: + out: list[str] = [] + + if self.enabled: + out.append(tr('Zram enabled')) + + out.append(tr('Zram algorithm {}').format(self.algorithm)) + return out + + return None + @dataclass -class ApplicationConfiguration: +class ApplicationConfiguration(SubConfig): bluetooth_config: BluetoothConfiguration | None = None audio_config: AudioConfiguration | None = None power_management_config: PowerManagementConfiguration | None = None @@ -223,6 +243,7 @@ class ApplicationConfiguration: return app_config + @override def json(self) -> ApplicationSerialization: config: ApplicationSerialization = {} @@ -245,3 +266,28 @@ class ApplicationConfiguration: config['fonts_config'] = self.fonts_config.json() return config + + @override + def summary(self) -> list[str]: + out: list[str] = [] + + if self.bluetooth_config and self.bluetooth_config.enabled: + out.append(tr('Bluetooth enabled')) + + if self.audio_config: + out.append(tr('Audio server "{}"').format(self.audio_config.audio)) + + if self.power_management_config: + out.append(tr('Power management "{}"').format(self.power_management_config.power_management)) + + if self.print_service_config and self.print_service_config.enabled: + out.append(tr('Print service enabled')) + + if self.firewall_config: + out.append(tr('Firewall "{}"').format(self.firewall_config.firewall)) + + if self.fonts_config and self.fonts_config.fonts: + fonts = ', '.join(f.value for f in self.fonts_config.fonts) + out.append(tr('Extra fonts "{}"').format(fonts)) + + return out diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py index 51120479..37261e7c 100644 --- a/archinstall/lib/models/authentication.py +++ b/archinstall/lib/models/authentication.py @@ -1,7 +1,8 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Any, NotRequired, Self, TypedDict +from typing import Any, NotRequired, Self, TypedDict, override +from archinstall.lib.models.config import SubConfig from archinstall.lib.models.users import Password, User from archinstall.lib.translationhandler import tr @@ -58,7 +59,7 @@ class U2FLoginConfiguration: @dataclass -class AuthenticationConfiguration: +class AuthenticationConfiguration(SubConfig): root_enc_password: Password | None = None users: list[User] = field(default_factory=list) u2f_config: U2FLoginConfiguration | None = None @@ -75,6 +76,7 @@ class AuthenticationConfiguration: return auth_config + @override def json(self) -> AuthenticationSerialization: config: AuthenticationSerialization = {} @@ -83,6 +85,21 @@ class AuthenticationConfiguration: return config + @override + def summary(self) -> list[str]: + out: list[str] = [] + + if self.root_enc_password: + out.append(tr('Root password set')) + + if self.users: + out.append(tr('Configured {} user(s)').format(len(self.users))) + + if self.u2f_config: + out.append(tr('U2F set up')) + + return out + def has_superuser(self) -> bool: return any(u.sudo for u in self.users) diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 5d8d0d80..0dbd4268 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -1,8 +1,9 @@ import sys from dataclasses import dataclass from enum import Enum -from typing import Any, Self +from typing import Any, Self, override +from archinstall.lib.models.config import SubConfig from archinstall.lib.output import warn from archinstall.lib.translationhandler import tr @@ -60,14 +61,26 @@ class Bootloader(Enum): @dataclass -class BootloaderConfiguration: +class BootloaderConfiguration(SubConfig): bootloader: Bootloader uki: bool = False removable: bool = True + @override def json(self) -> dict[str, Any]: return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable} + @override + def summary(self) -> list[str]: + out = [tr('Bootloader "{}"').format(self.bootloader.value)] + + if self.uki: + out.append(tr('UKI enabled')) + if self.removable: + out.append(tr('Removable')) + + return out + @classmethod def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self: bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot) diff --git a/archinstall/lib/models/config.py b/archinstall/lib/models/config.py new file mode 100644 index 00000000..45835bc0 --- /dev/null +++ b/archinstall/lib/models/config.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class SubConfig(ABC): + @abstractmethod + def json(self) -> Any: + pass + + @abstractmethod + def summary(self) -> str | list[str] | None: + pass diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index 7fa05407..a21f3c6f 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -12,6 +12,7 @@ from parted import Disk, Geometry, Partition from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator from archinstall.lib.hardware import SysInfo +from archinstall.lib.models.config import SubConfig from archinstall.lib.models.users import Password from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr @@ -34,6 +35,15 @@ class DiskLayoutType(Enum): case DiskLayoutType.Pre_mount: return tr('Pre-mounted configuration') + def short_msg(self) -> str: + match self: + case DiskLayoutType.Default: + return tr('Default') + case DiskLayoutType.Manual: + return tr('Manual') + case DiskLayoutType.Pre_mount: + return tr('Pre-mount') + class _DiskLayoutConfigurationSerialization(TypedDict): config_type: str @@ -45,7 +55,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict): @dataclass -class DiskLayoutConfiguration: +class DiskLayoutConfiguration(SubConfig): config_type: DiskLayoutType device_modifications: list[DeviceModification] = field(default_factory=list) lvm_config: LvmConfiguration | None = None @@ -55,6 +65,7 @@ class DiskLayoutConfiguration: # used for pre-mounted config mountpoint: Path | None = None + @override def json(self) -> _DiskLayoutConfigurationSerialization: if self.config_type == DiskLayoutType.Pre_mount: return { @@ -78,6 +89,28 @@ class DiskLayoutConfiguration: return config + @override + def summary(self) -> list[str]: + out = [tr('{} layout').format(self.config_type.short_msg())] + + devices = set(mod.device_path for mod in self.device_modifications) + + if devices: + dev_str = ', '.join(str(d) for d in devices) + out.append(tr('Devices {}').format(dev_str)) + + if self.lvm_config is not None: + out.append(tr('LVM set up')) + + if self.disk_encryption is not None: + out.append(tr('{} encryption').format(self.disk_encryption.encryption_type.type_to_text())) + + if self.btrfs_options is not None: + if self.btrfs_options.snapshot_config: + out.append(tr('Btrfs snapshot "{}"').format(self.btrfs_options.snapshot_config.snapshot_type)) + + return out + @classmethod def parse_arg( cls, @@ -1321,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict): type: str -class SnapshotType(Enum): +class SnapshotType(StrEnum): Snapper = 'Snapper' Timeshift = 'Timeshift' diff --git a/archinstall/lib/models/locale.py b/archinstall/lib/models/locale.py index 8f580989..26ab3cc0 100644 --- a/archinstall/lib/models/locale.py +++ b/archinstall/lib/models/locale.py @@ -1,12 +1,13 @@ from dataclasses import dataclass -from typing import Any, Self +from typing import Any, Self, override from archinstall.lib.locale.utils import get_kb_layout +from archinstall.lib.models.config import SubConfig from archinstall.lib.translationhandler import tr @dataclass -class LocaleConfiguration: +class LocaleConfiguration(SubConfig): kb_layout: str sys_lang: str sys_enc: str @@ -23,6 +24,7 @@ class LocaleConfiguration: layout = 'us' return cls(layout, 'en_US.UTF-8', 'UTF-8') + @override def json(self) -> dict[str, str]: return { 'kb_layout': self.kb_layout, @@ -31,6 +33,15 @@ class LocaleConfiguration: 'console_font': self.console_font, } + @override + def summary(self) -> list[str]: + return [ + tr('Keyboard layout "{}"').format(self.kb_layout), + tr('Locale language "{}"').format(self.sys_lang), + tr('Locale encoding "{}"').format(self.sys_enc), + tr('Console font "{}"').format(self.console_font), + ] + def preview(self) -> str: output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout) output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang) diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py index 0a596c13..3ded2a9a 100644 --- a/archinstall/lib/models/mirrors.py +++ b/archinstall/lib/models/mirrors.py @@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override from pydantic import BaseModel, ValidationInfo, field_validator, model_validator +from archinstall.lib.models.config import SubConfig from archinstall.lib.models.packages import Repository from archinstall.lib.networking import DownloadTimer, ping from archinstall.lib.output import debug +from archinstall.lib.translationhandler import tr if TYPE_CHECKING: from archinstall.lib.mirror.mirror_handler import MirrorListHandler @@ -236,7 +238,7 @@ class _MirrorConfigurationSerialization(TypedDict): @dataclass -class MirrorConfiguration: +class MirrorConfiguration(SubConfig): mirror_regions: list[MirrorRegion] = field(default_factory=list) custom_servers: list[CustomServer] = field(default_factory=list) optional_repositories: list[Repository] = field(default_factory=list) @@ -250,6 +252,7 @@ class MirrorConfiguration: def custom_server_urls(self) -> str: return '\n'.join(s.url for s in self.custom_servers) + @override def json(self) -> _MirrorConfigurationSerialization: regions = {} for m in self.mirror_regions: @@ -262,6 +265,24 @@ class MirrorConfiguration: 'custom_repositories': [c.json() for c in self.custom_repositories], } + @override + def summary(self) -> list[str]: + out: list[str] = [] + + if self.mirror_regions: + out.append(tr('Mirror regions "{}"').format(', '.join(m.name for m in self.mirror_regions))) + + if self.optional_repositories: + out.append(tr('Optional repositories "{}"').format(', '.join(r.value for r in self.optional_repositories))) + + if self.custom_servers: + out.append(tr('Custom servers set up')) + + if self.custom_repositories: + out.append(tr('Custom repositories set up')) + + return out + def custom_servers_config(self) -> str: config = '' diff --git a/archinstall/lib/models/network.py b/archinstall/lib/models/network.py index 71f3dca2..9e61cb56 100644 --- a/archinstall/lib/models/network.py +++ b/archinstall/lib/models/network.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from enum import Enum from typing import NotRequired, Self, TypedDict, override +from archinstall.lib.models.config import SubConfig from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr @@ -103,10 +104,11 @@ class _NetworkConfigurationSerialization(TypedDict): @dataclass -class NetworkConfiguration: +class NetworkConfiguration(SubConfig): type: NicType nics: list[Nic] = field(default_factory=list) + @override def json(self) -> _NetworkConfigurationSerialization: config: _NetworkConfigurationSerialization = {'type': self.type.value} if self.nics: @@ -114,6 +116,10 @@ class NetworkConfiguration: return config + @override + def summary(self) -> str: + return self.type.display_msg() + @classmethod def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None: nic_type = config.get('type', None) diff --git a/archinstall/lib/models/pacman.py b/archinstall/lib/models/pacman.py index 5d271ea9..004dfbc3 100644 --- a/archinstall/lib/models/pacman.py +++ b/archinstall/lib/models/pacman.py @@ -1,6 +1,7 @@ from dataclasses import dataclass -from typing import Self, TypedDict +from typing import Self, TypedDict, override +from archinstall.lib.models.config import SubConfig from archinstall.lib.translationhandler import tr @@ -10,7 +11,7 @@ class PacmanConfigSerialization(TypedDict): @dataclass -class PacmanConfiguration: +class PacmanConfiguration(SubConfig): parallel_downloads: int = 5 color: bool = True @@ -18,12 +19,19 @@ class PacmanConfiguration: def default(cls) -> Self: return cls() + @override def json(self) -> PacmanConfigSerialization: return { 'parallel_downloads': self.parallel_downloads, 'color': self.color, } + @override + def summary(self) -> str | None: + if self.color: + return tr('Color enabled') + return None + def preview(self) -> str: color_str = str(self.color) output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads) diff --git a/archinstall/lib/models/profile.py b/archinstall/lib/models/profile.py index e7ae8c8c..fa890a1e 100644 --- a/archinstall/lib/models/profile.py +++ b/archinstall/lib/models/profile.py @@ -1,8 +1,10 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, Self, TypedDict +from typing import TYPE_CHECKING, Self, TypedDict, override from archinstall.default_profiles.profile import GreeterType, Profile from archinstall.lib.hardware import GfxDriver +from archinstall.lib.models.config import SubConfig +from archinstall.lib.translationhandler import tr if TYPE_CHECKING: from archinstall.lib.profile.profiles_handler import ProfileSerialization @@ -15,11 +17,12 @@ class _ProfileConfigurationSerialization(TypedDict): @dataclass -class ProfileConfiguration: +class ProfileConfiguration(SubConfig): profile: Profile | None = None gfx_driver: GfxDriver | None = None greeter: GreeterType | None = None + @override def json(self) -> _ProfileConfigurationSerialization: from archinstall.lib.profile.profiles_handler import profile_handler @@ -29,6 +32,23 @@ class ProfileConfiguration: 'greeter': self.greeter.value if self.greeter else None, } + @override + def summary(self) -> list[str] | None: + out: list[str] = [] + + if self.profile: + out.append(self.profile.name) + + if self.gfx_driver: + out.append(tr('{} grphics driver').format(self.gfx_driver.value)) + + if self.greeter: + out.append(tr('{} greeter').format(self.greeter.value)) + + return out + + return None + @classmethod def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self: from archinstall.lib.profile.profiles_handler import profile_handler diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 3a972dfb..86a47ed1 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust +from archinstall.tui.rich import BaseRichTable if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -20,7 +21,7 @@ class FormattedOutput: @staticmethod def _get_values( o: DataclassInstance, - class_formatter: str | Callable | None = None, # type: ignore[type-arg] + class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument] filter_list: list[str] = [], ) -> dict[str, Any]: """ @@ -129,6 +130,35 @@ class FormattedOutput: return output +def as_key_value_pair( + entries: dict[str, str | list[str] | bool], + ignore_empty: bool = True, +) -> str: + """ + Formats key-values as a Rich Table: + key1 : value1 + key2 : value2 + ... + """ + table = BaseRichTable() + table.add_column('key', style='bold', no_wrap=True) + table.add_column('value', style='white', max_width=70) + + for label, value in entries.items(): + if ignore_empty and not value: + continue + + if isinstance(value, bool): + value = 'Yes' if value else 'No' + + if isinstance(value, list): + value = '\n '.join(str(val) for val in value) + + table.add_row(label.title(), f': {value}') + + return table.stringify() + + class Journald: @staticmethod def log(message: str, level: int = logging.DEBUG) -> None: diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py index f732c21f..9fecf3c6 100644 --- a/archinstall/tui/components.py +++ b/archinstall/tui/components.py @@ -350,11 +350,6 @@ class OptionListScreen(BaseScreen[ValueT]): ] ) - # debug(f'Index: {index}') - # debug(f'Region: {option_list.region}') - # debug(f'Scroll offset: {option_list.scroll_offset}') - # debug(f'Target_Y: {target_y}') - self.app.cursor_position = Offset(option_list.region.x, target_y) self.app.refresh() @@ -1163,8 +1158,6 @@ class TableSelectionScreen(BaseScreen[ValueT]): ] ) - debug(f'Setting cursor to target_y: {target_y}') - self.app.cursor_position = Offset(data_table.region.x, target_y) self.app.refresh() diff --git a/archinstall/tui/rich.py b/archinstall/tui/rich.py new file mode 100644 index 00000000..055ec22c --- /dev/null +++ b/archinstall/tui/rich.py @@ -0,0 +1,25 @@ +from io import StringIO + +from rich import box +from rich.console import Console +from rich.table import Table as RichTable + + +class BaseRichTable(RichTable): + def __init__(self) -> None: + super().__init__( + box=box.SIMPLE, + show_header=False, + pad_edge=False, + show_edge=False, + ) + + def stringify(self) -> str: + string_io = StringIO() + buf = Console(file=string_io, highlight=False) + buf.print(self) + + _ = string_io.seek(0) + output = string_io.read() + + return output