Enhance config types and summary

This commit is contained in:
Daniel Girtler 2026-05-10 15:03:46 +10:00
parent 2de7254b21
commit 58dadccd2c
15 changed files with 394 additions and 107 deletions

View File

@ -6,6 +6,7 @@ import urllib.error
import urllib.parse import urllib.parse
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import StrEnum
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, Self
from urllib.request import Request, urlopen 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.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration 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.device import DiskEncryption, DiskLayoutConfiguration
from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration
@ -57,6 +59,72 @@ class Arguments:
verbose: bool = False 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 @dataclass
class ArchConfig: class ArchConfig:
version: str | None = None version: str | None = None
@ -97,41 +165,67 @@ class ArchConfig:
return config return config
def safe_config(self) -> dict[str, Any]: def safe_config(self) -> dict[ArchConfigType, Any]:
config: Any = { base_config: dict[ArchConfigType, Any] = {
'version': self.version, ArchConfigType.VERSION: self.version,
'script': self.script, ArchConfigType.SCRIPT: self.script,
'archinstall-language': self.archinstall_language.json(), ArchConfigType.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,
} }
if self.locale_config: base_config.update(self.plain_cfg())
config['locale_config'] = self.locale_config.json() sub_config = self.sub_cfg()
if self.disk_config: for config_type, value in sub_config.items():
config['disk_config'] = self.disk_config.json() 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: return base_config
config['profile_config'] = self.profile_config.json()
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: 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: 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 @classmethod
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:

View File

@ -6,13 +6,12 @@ from typing import Any
from pydantic import TypeAdapter 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.crypt import encrypt
from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir 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.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.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType from archinstall.tui.result import ResultType
@ -45,7 +44,7 @@ class ConfigurationOutput:
def user_config_to_json(self) -> str: def user_config_to_json(self) -> str:
config = self._config.safe_config() config = self._config.safe_config()
adapter = TypeAdapter(dict[str, Any]) adapter = TypeAdapter(dict[ArchConfigType, Any])
python_dict = adapter.dump_python(config) python_dict = adapter.dump_python(config)
return json.dumps(python_dict, indent=4, sort_keys=True) 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. 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. 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 for key, value in self._config.plain_cfg().items():
if disk_config and disk_config.device_modifications: cfg[key.text()] = value
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)))
bl_config = self._config.bootloader_config for config_type, obj in self._config.sub_cfg().items():
if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER: if not hasattr(obj, 'summary'):
rows.append((tr('Bootloader'), bl_config.bootloader.value)) continue
kernels = self._config.kernels summary = obj.summary()
if kernels: if summary:
rows.append((tr('Kernel'), ', '.join(kernels))) cfg[config_type.text()] = summary
profile_config = self._config.profile_config simple_summary = as_key_value_pair(cfg, ignore_empty=True)
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))
packages = self._config.packages return simple_summary
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)
async def confirm_config(self, show_install_warnings: bool = False) -> bool: async def confirm_config(self, show_install_warnings: bool = False) -> bool:
header = f'{tr("The specified configuration will be applied")}. ' header = f'{tr("The specified configuration will be applied")}. '

View File

@ -1,7 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum, auto 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 from archinstall.lib.translationhandler import tr
@ -168,7 +169,7 @@ class FontsConfiguration:
@dataclass(frozen=True) @dataclass(frozen=True)
class ZramConfiguration: class ZramConfiguration(SubConfig):
enabled: bool enabled: bool
algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD algorithm: ZramAlgorithm = ZramAlgorithm.ZSTD
@ -181,9 +182,28 @@ class ZramConfiguration:
algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value)) algo = arg.get('algorithm', arg.get('algo', ZramAlgorithm.ZSTD.value))
return cls(enabled=enabled, algorithm=ZramAlgorithm(algo)) 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 @dataclass
class ApplicationConfiguration: class ApplicationConfiguration(SubConfig):
bluetooth_config: BluetoothConfiguration | None = None bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None power_management_config: PowerManagementConfiguration | None = None
@ -223,6 +243,7 @@ class ApplicationConfiguration:
return app_config return app_config
@override
def json(self) -> ApplicationSerialization: def json(self) -> ApplicationSerialization:
config: ApplicationSerialization = {} config: ApplicationSerialization = {}
@ -245,3 +266,28 @@ class ApplicationConfiguration:
config['fonts_config'] = self.fonts_config.json() config['fonts_config'] = self.fonts_config.json()
return config 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

View File

@ -1,7 +1,8 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum 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.models.users import Password, User
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -58,7 +59,7 @@ class U2FLoginConfiguration:
@dataclass @dataclass
class AuthenticationConfiguration: class AuthenticationConfiguration(SubConfig):
root_enc_password: Password | None = None root_enc_password: Password | None = None
users: list[User] = field(default_factory=list) users: list[User] = field(default_factory=list)
u2f_config: U2FLoginConfiguration | None = None u2f_config: U2FLoginConfiguration | None = None
@ -75,6 +76,7 @@ class AuthenticationConfiguration:
return auth_config return auth_config
@override
def json(self) -> AuthenticationSerialization: def json(self) -> AuthenticationSerialization:
config: AuthenticationSerialization = {} config: AuthenticationSerialization = {}
@ -83,6 +85,21 @@ class AuthenticationConfiguration:
return config 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: def has_superuser(self) -> bool:
return any(u.sudo for u in self.users) return any(u.sudo for u in self.users)

View File

@ -1,8 +1,9 @@
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum 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.output import warn
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -60,14 +61,26 @@ class Bootloader(Enum):
@dataclass @dataclass
class BootloaderConfiguration: class BootloaderConfiguration(SubConfig):
bootloader: Bootloader bootloader: Bootloader
uki: bool = False uki: bool = False
removable: bool = True removable: bool = True
@override
def json(self) -> dict[str, Any]: def json(self) -> dict[str, Any]:
return {'bootloader': self.bootloader.json(), 'uki': self.uki, 'removable': self.removable} 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 @classmethod
def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self: def parse_arg(cls, config: dict[str, Any], skip_boot: bool) -> Self:
bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot) bootloader = Bootloader.from_arg(config.get('bootloader', ''), skip_boot)

View File

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

View File

@ -12,6 +12,7 @@ from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from archinstall.lib.hardware import SysInfo from archinstall.lib.hardware import SysInfo
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password from archinstall.lib.models.users import Password
from archinstall.lib.output import debug from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -34,6 +35,15 @@ class DiskLayoutType(Enum):
case DiskLayoutType.Pre_mount: case DiskLayoutType.Pre_mount:
return tr('Pre-mounted configuration') 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): class _DiskLayoutConfigurationSerialization(TypedDict):
config_type: str config_type: str
@ -45,7 +55,7 @@ class _DiskLayoutConfigurationSerialization(TypedDict):
@dataclass @dataclass
class DiskLayoutConfiguration: class DiskLayoutConfiguration(SubConfig):
config_type: DiskLayoutType config_type: DiskLayoutType
device_modifications: list[DeviceModification] = field(default_factory=list) device_modifications: list[DeviceModification] = field(default_factory=list)
lvm_config: LvmConfiguration | None = None lvm_config: LvmConfiguration | None = None
@ -55,6 +65,7 @@ class DiskLayoutConfiguration:
# used for pre-mounted config # used for pre-mounted config
mountpoint: Path | None = None mountpoint: Path | None = None
@override
def json(self) -> _DiskLayoutConfigurationSerialization: def json(self) -> _DiskLayoutConfigurationSerialization:
if self.config_type == DiskLayoutType.Pre_mount: if self.config_type == DiskLayoutType.Pre_mount:
return { return {
@ -78,6 +89,28 @@ class DiskLayoutConfiguration:
return config 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 @classmethod
def parse_arg( def parse_arg(
cls, cls,
@ -1321,7 +1354,7 @@ class _SnapshotConfigSerialization(TypedDict):
type: str type: str
class SnapshotType(Enum): class SnapshotType(StrEnum):
Snapper = 'Snapper' Snapper = 'Snapper'
Timeshift = 'Timeshift' Timeshift = 'Timeshift'

View File

@ -1,12 +1,13 @@
from dataclasses import dataclass 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.locale.utils import get_kb_layout
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@dataclass @dataclass
class LocaleConfiguration: class LocaleConfiguration(SubConfig):
kb_layout: str kb_layout: str
sys_lang: str sys_lang: str
sys_enc: str sys_enc: str
@ -23,6 +24,7 @@ class LocaleConfiguration:
layout = 'us' layout = 'us'
return cls(layout, 'en_US.UTF-8', 'UTF-8') return cls(layout, 'en_US.UTF-8', 'UTF-8')
@override
def json(self) -> dict[str, str]: def json(self) -> dict[str, str]:
return { return {
'kb_layout': self.kb_layout, 'kb_layout': self.kb_layout,
@ -31,6 +33,15 @@ class LocaleConfiguration:
'console_font': self.console_font, '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: def preview(self) -> str:
output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout) output = '{}: {}\n'.format(tr('Keyboard layout'), self.kb_layout)
output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang) output += '{}: {}\n'.format(tr('Locale language'), self.sys_lang)

View File

@ -9,9 +9,11 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator 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.models.packages import Repository
from archinstall.lib.networking import DownloadTimer, ping from archinstall.lib.networking import DownloadTimer, ping
from archinstall.lib.output import debug from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.mirror.mirror_handler import MirrorListHandler
@ -236,7 +238,7 @@ class _MirrorConfigurationSerialization(TypedDict):
@dataclass @dataclass
class MirrorConfiguration: class MirrorConfiguration(SubConfig):
mirror_regions: list[MirrorRegion] = field(default_factory=list) mirror_regions: list[MirrorRegion] = field(default_factory=list)
custom_servers: list[CustomServer] = field(default_factory=list) custom_servers: list[CustomServer] = field(default_factory=list)
optional_repositories: list[Repository] = field(default_factory=list) optional_repositories: list[Repository] = field(default_factory=list)
@ -250,6 +252,7 @@ class MirrorConfiguration:
def custom_server_urls(self) -> str: def custom_server_urls(self) -> str:
return '\n'.join(s.url for s in self.custom_servers) return '\n'.join(s.url for s in self.custom_servers)
@override
def json(self) -> _MirrorConfigurationSerialization: def json(self) -> _MirrorConfigurationSerialization:
regions = {} regions = {}
for m in self.mirror_regions: for m in self.mirror_regions:
@ -262,6 +265,24 @@ class MirrorConfiguration:
'custom_repositories': [c.json() for c in self.custom_repositories], '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: def custom_servers_config(self) -> str:
config = '' config = ''

View File

@ -3,6 +3,7 @@ from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import NotRequired, Self, TypedDict, override from typing import NotRequired, Self, TypedDict, override
from archinstall.lib.models.config import SubConfig
from archinstall.lib.output import debug from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
@ -103,10 +104,11 @@ class _NetworkConfigurationSerialization(TypedDict):
@dataclass @dataclass
class NetworkConfiguration: class NetworkConfiguration(SubConfig):
type: NicType type: NicType
nics: list[Nic] = field(default_factory=list) nics: list[Nic] = field(default_factory=list)
@override
def json(self) -> _NetworkConfigurationSerialization: def json(self) -> _NetworkConfigurationSerialization:
config: _NetworkConfigurationSerialization = {'type': self.type.value} config: _NetworkConfigurationSerialization = {'type': self.type.value}
if self.nics: if self.nics:
@ -114,6 +116,10 @@ class NetworkConfiguration:
return config return config
@override
def summary(self) -> str:
return self.type.display_msg()
@classmethod @classmethod
def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None: def parse_arg(cls, config: _NetworkConfigurationSerialization) -> Self | None:
nic_type = config.get('type', None) nic_type = config.get('type', None)

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass 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 from archinstall.lib.translationhandler import tr
@ -10,7 +11,7 @@ class PacmanConfigSerialization(TypedDict):
@dataclass @dataclass
class PacmanConfiguration: class PacmanConfiguration(SubConfig):
parallel_downloads: int = 5 parallel_downloads: int = 5
color: bool = True color: bool = True
@ -18,12 +19,19 @@ class PacmanConfiguration:
def default(cls) -> Self: def default(cls) -> Self:
return cls() return cls()
@override
def json(self) -> PacmanConfigSerialization: def json(self) -> PacmanConfigSerialization:
return { return {
'parallel_downloads': self.parallel_downloads, 'parallel_downloads': self.parallel_downloads,
'color': self.color, 'color': self.color,
} }
@override
def summary(self) -> str | None:
if self.color:
return tr('Color enabled')
return None
def preview(self) -> str: def preview(self) -> str:
color_str = str(self.color) color_str = str(self.color)
output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads) output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads)

View File

@ -1,8 +1,10 @@
from dataclasses import dataclass 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.default_profiles.profile import GreeterType, Profile
from archinstall.lib.hardware import GfxDriver from archinstall.lib.hardware import GfxDriver
from archinstall.lib.models.config import SubConfig
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.profile.profiles_handler import ProfileSerialization from archinstall.lib.profile.profiles_handler import ProfileSerialization
@ -15,11 +17,12 @@ class _ProfileConfigurationSerialization(TypedDict):
@dataclass @dataclass
class ProfileConfiguration: class ProfileConfiguration(SubConfig):
profile: Profile | None = None profile: Profile | None = None
gfx_driver: GfxDriver | None = None gfx_driver: GfxDriver | None = None
greeter: GreeterType | None = None greeter: GreeterType | None = None
@override
def json(self) -> _ProfileConfigurationSerialization: def json(self) -> _ProfileConfigurationSerialization:
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
@ -29,6 +32,23 @@ class ProfileConfiguration:
'greeter': self.greeter.value if self.greeter else None, '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 @classmethod
def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self: def parse_arg(cls, arg: _ProfileConfigurationSerialization) -> Self:
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler

View File

@ -11,6 +11,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
from archinstall.tui.rich import BaseRichTable
if TYPE_CHECKING: if TYPE_CHECKING:
from _typeshed import DataclassInstance from _typeshed import DataclassInstance
@ -20,7 +21,7 @@ class FormattedOutput:
@staticmethod @staticmethod
def _get_values( def _get_values(
o: DataclassInstance, 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] = [], filter_list: list[str] = [],
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
@ -129,6 +130,35 @@ class FormattedOutput:
return output 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: class Journald:
@staticmethod @staticmethod
def log(message: str, level: int = logging.DEBUG) -> None: def log(message: str, level: int = logging.DEBUG) -> None:

View File

@ -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.cursor_position = Offset(option_list.region.x, target_y)
self.app.refresh() 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.cursor_position = Offset(data_table.region.x, target_y)
self.app.refresh() self.app.refresh()

25
archinstall/tui/rich.py Normal file
View File

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