From 7fc33c2507637e5b8feff5aa0ac254950b7a2767 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 11 May 2026 06:35:53 +1000 Subject: [PATCH 01/15] Enhance config types and summary (#4532) * Enhance config types and summary * Update --- archinstall/lib/args.py | 165 ++++++++++++++++++----- archinstall/lib/configuration.py | 76 +++-------- archinstall/lib/models/application.py | 52 ++++++- archinstall/lib/models/authentication.py | 21 ++- archinstall/lib/models/bootloader.py | 17 ++- archinstall/lib/models/config.py | 12 ++ archinstall/lib/models/device.py | 37 ++++- archinstall/lib/models/locale.py | 15 ++- archinstall/lib/models/mirrors.py | 23 +++- archinstall/lib/models/network.py | 8 +- archinstall/lib/models/pacman.py | 12 +- archinstall/lib/models/profile.py | 24 +++- archinstall/lib/output.py | 32 ++++- archinstall/tui/components.py | 7 - archinstall/tui/rich.py | 25 ++++ 15 files changed, 411 insertions(+), 115 deletions(-) create mode 100644 archinstall/lib/models/config.py create mode 100644 archinstall/tui/rich.py diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index d8aaaa83..efe9baa2 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,81 @@ 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' + USERS = 'users' + ROOT_ENC_PASSWORD = 'root_enc_password' + ENCRYPTION_PASSWORD = 'encryption_password' + 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') + case ArchConfigType.USERS: + return tr('Users') + case ArchConfigType.ROOT_ENC_PASSWORD: + return tr('Root encrypted password') + case ArchConfigType.ENCRYPTION_PASSWORD: + return tr('Disk encryption password') + + @dataclass class ArchConfig: version: str | None = None @@ -80,58 +157,84 @@ class ArchConfig: services: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list) - def unsafe_config(self) -> dict[str, Any]: - config: dict[str, list[UserSerialization] | str | None] = {} + def unsafe_config(self) -> dict[ArchConfigType, Any]: + config: dict[ArchConfigType, list[UserSerialization] | str | None] = {} if self.auth_config: if self.auth_config.users: - config['users'] = [user.json() for user in self.auth_config.users] + config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users] if self.auth_config.root_enc_password: - config['root_enc_password'] = self.auth_config.root_enc_password.enc_password + config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password if self.disk_config: disk_encryption = self.disk_config.disk_encryption if disk_encryption and disk_encryption.encryption_password: - config['encryption_password'] = disk_encryption.encryption_password.plaintext + config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext 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..c40ef161 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,15 +44,15 @@ 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) def user_credentials_to_json(self) -> str: - config = self._config.unsafe_config() + cfg = self._config.unsafe_config() - adapter = TypeAdapter(dict[str, Any]) - python_dict = adapter.dump_python(config) + adapter = TypeAdapter(dict[ArchConfigType, Any]) + python_dict = adapter.dump_python(cfg) return json.dumps(python_dict, indent=4, sort_keys=True) def write_debug(self) -> None: @@ -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 From f7a6f70fc858280888fce47c01839e0a407b2c20 Mon Sep 17 00:00:00 2001 From: stinga11 Date: Tue, 12 May 2026 18:21:15 -0400 Subject: [PATCH 02/15] Replace terminal and file manager in Budgie profile (#4533) I think it was a mistake to have made the previous changes to KDE Apps. In retrospect, it seemed like a good idea since the Budgie developer had done it that way in Fedora, which is the distro the developer uses as a reference for Budgie. When you start using it, you realize there's no bridge between the desktop and the KDE Apps, and things like Dolphin recognizes icons and themes, requiring some rather annoying manual configurations. Then, if you want to change them again, you have to change those configurations again. Files don't open by default with the apps either; you have to configure them for that to work. At first, I thought the Budgie packager for Arch had forgotten some stray dependency, but with some free time, I tested it with Fedora 44, and to my surprise, it has exactly the same problems, which is completely unacceptable for a final stable release. I suppose he'll make the necessary changes in the near future, but right now, it's a disaster. --- archinstall/default_profiles/desktops/budgie.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/archinstall/default_profiles/desktops/budgie.py b/archinstall/default_profiles/desktops/budgie.py index f156a53b..da147851 100644 --- a/archinstall/default_profiles/desktops/budgie.py +++ b/archinstall/default_profiles/desktops/budgie.py @@ -18,12 +18,13 @@ class BudgieProfile(Profile): return [ 'materia-gtk-theme', 'budgie', - 'konsole', - 'dolphin', + 'mate-terminal', + 'nemo', + 'nemo-fileroller', 'papirus-icon-theme', ] @property @override def default_greeter_type(self) -> GreeterType: - return GreeterType.Sddm + return GreeterType.LightdmSlick From 3dec5025c303d73c2258872bbba3fd4cbfb5b2d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 08:21:44 +1000 Subject: [PATCH 03/15] Update dependency arch/python-pydantic to v2.13.4 (#4534) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9fee23fa..3bbc7acb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ ] dependencies = [ "pyparted==3.13.0", - "pydantic==2.12.5", + "pydantic==2.13.4", "cryptography==48.0.0", "textual==8.2.5", "markdown-it-py==4.0.0", From b95321d38ca7db8f3e0432279bfbd76ba143bb89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 08:33:42 +1000 Subject: [PATCH 04/15] Update dependency mypy to v2.1.0 (#4535) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3bbc7acb..13e0933f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ Source = "https://github.com/archlinux/archinstall" [project.optional-dependencies] log = ["systemd_python==235"] dev = [ - "mypy==2.0.0", + "mypy==2.1.0", "flake8==7.3.0", "pre-commit==4.6.0", "ruff==0.15.12", From 13944f3ccad8ad6ec7de26791a65eab9aed872d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 08:54:05 +1000 Subject: [PATCH 05/15] Update pre-commit hook pre-commit/mirrors-mypy to v2.1.0 (#4538) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cf16130..c573ae04 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: args: [--config=.flake8] fail_fast: true - repo: https://github.com/pre-commit/mirrors-mypy - rev: v2.0.0 + rev: v2.1.0 hooks: - id: mypy args: [ From b81fe955f0c789e371b96ed03c79642d4c607f82 Mon Sep 17 00:00:00 2001 From: HADEON <52324046+h8d13@users.noreply.github.com> Date: Fri, 15 May 2026 03:57:31 +0000 Subject: [PATCH 06/15] Add IWD standalone option to network configuration + fix NM_IWD (#4528) * Add iwd standalone option to network configuration Adds NicType.IWD as a third option alongside NM and NM_IWD: install iwd, write /etc/iwd/main.conf with EnableNetworkConfiguration=true and NameResolvingService=systemd, enable iwd.service + systemd-resolved.service. iwd handles DHCP itself and resolved picks up its DNS via the symlink, so no NetworkManager pulled in. Also fills in parse_arg cases for NM_IWD and IWD so config files round-trip both nic types. Assisted-By: Flint * wire up resolv stub and networkd service * exclude virtual devices, dont harcode iface name instead match 'ether' similar pattern to .network files shipped in /etc/systemd/network * use dedent and rename menu option --- archinstall/lib/models/network.py | 7 ++++ archinstall/lib/network/network_handler.py | 42 ++++++++++++++++++++++ archinstall/lib/network/network_menu.py | 2 ++ 3 files changed, 51 insertions(+) diff --git a/archinstall/lib/models/network.py b/archinstall/lib/models/network.py index 9e61cb56..57a579c3 100644 --- a/archinstall/lib/models/network.py +++ b/archinstall/lib/models/network.py @@ -12,6 +12,7 @@ class NicType(Enum): ISO = 'iso' NM = 'nm' NM_IWD = 'nm_iwd' + IWD = 'iwd' MANUAL = 'manual' def display_msg(self) -> str: @@ -22,6 +23,8 @@ class NicType(Enum): return tr('Use Network Manager (default backend)') case NicType.NM_IWD: return tr('Use Network Manager (iwd backend)') + case NicType.IWD: + return tr('Use standalone iwd') case NicType.MANUAL: return tr('Manual configuration') @@ -131,6 +134,10 @@ class NetworkConfiguration(SubConfig): return cls(NicType.ISO) case NicType.NM: return cls(NicType.NM) + case NicType.NM_IWD: + return cls(NicType.NM_IWD) + case NicType.IWD: + return cls(NicType.IWD) case NicType.MANUAL: nics_arg = config.get('nics', []) if nics_arg: diff --git a/archinstall/lib/network/network_handler.py b/archinstall/lib/network/network_handler.py index 13548584..09d2750c 100644 --- a/archinstall/lib/network/network_handler.py +++ b/archinstall/lib/network/network_handler.py @@ -1,3 +1,5 @@ +import textwrap + from archinstall.lib.installer import Installer from archinstall.lib.models.network import NetworkConfiguration, NicType from archinstall.lib.models.profile import ProfileConfiguration @@ -32,6 +34,13 @@ def install_network_config( _configure_nm_iwd(installation) installation.disable_service('iwd.service') + case NicType.IWD: + installation.add_additional_packages(['iwd']) + _configure_iwd_standalone(installation) + installation.enable_service('iwd.service') + installation.enable_service('systemd-networkd.service') + installation.enable_service('systemd-resolved.service') + case NicType.MANUAL: for nic in network_config.nics: installation.configure_nic(nic) @@ -45,3 +54,36 @@ def _configure_nm_iwd(installation: Installer) -> None: iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf' _ = iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n') + + +def _configure_iwd_standalone(installation: Installer) -> None: + # iwd manages wireless only; systemd-networkd handles wired DHCP. + iwd_conf_dir = installation.target / 'etc/iwd' + iwd_conf_dir.mkdir(parents=True, exist_ok=True) + + main_conf = iwd_conf_dir / 'main.conf' + main_conf_content = textwrap.dedent("""\ + [General] + EnableNetworkConfiguration=true + + [Network] + NameResolvingService=systemd + """) + _ = main_conf.write_text(main_conf_content) + + networkd_dir = installation.target / 'etc/systemd/network' + networkd_dir.mkdir(parents=True, exist_ok=True) + wired_conf = networkd_dir / '20-wired.network' + wired_conf_content = textwrap.dedent("""\ + [Match] + Type=ether + Kind=!* + + [Network] + DHCP=yes + """) + _ = wired_conf.write_text(wired_conf_content) + + resolv = installation.target / 'etc/resolv.conf' + resolv.unlink(missing_ok=True) + resolv.symlink_to('/run/systemd/resolve/stub-resolv.conf') diff --git a/archinstall/lib/network/network_menu.py b/archinstall/lib/network/network_menu.py index 120af19e..88c26dba 100644 --- a/archinstall/lib/network/network_menu.py +++ b/archinstall/lib/network/network_menu.py @@ -202,6 +202,8 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur return NetworkConfiguration(NicType.NM) case NicType.NM_IWD: return NetworkConfiguration(NicType.NM_IWD) + case NicType.IWD: + return NetworkConfiguration(NicType.IWD) case NicType.MANUAL: preset_nics = preset.nics if preset else [] nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show() From 74a1066661f928690c39437598292719f192c16c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 13:58:00 +1000 Subject: [PATCH 07/15] Update dependency ruff to v0.15.13 (#4540) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13e0933f..636c8017 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dev = [ "mypy==2.1.0", "flake8==7.3.0", "pre-commit==4.6.0", - "ruff==0.15.12", + "ruff==0.15.13", "pylint==4.0.5", "pytest==9.0.3", ] From e48ca45b0bc8be6a093bf624b951455c031d3de0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 16:30:33 +1000 Subject: [PATCH 08/15] Update pre-commit hook astral-sh/ruff-pre-commit to v0.15.13 (#4541) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c573ae04..91fd6f33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: ['pre-commit'] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: v0.15.13 hooks: # fix unused imports and sort them - id: ruff From 516a61d8af56031930d8f527119b358960962cd6 Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Fri, 15 May 2026 21:38:17 +1000 Subject: [PATCH 09/15] Enhance log sharing capability (#4526) --- .pre-commit-config.yaml | 1 + archinstall/lib/args.py | 19 ++++-- archinstall/lib/output.py | 64 ++++++++++---------- archinstall/main.py | 43 ++++++++++---- pyproject.toml | 1 + tests/test_share_log.py | 121 ++++++++++++++++++++++++-------------- 6 files changed, 158 insertions(+), 91 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91fd6f33..de2bcef4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,7 @@ repos: additional_dependencies: - pydantic - pytest + - hypothesis - cryptography - textual - repo: local diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index efe9baa2..14aff6c6 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -6,7 +6,7 @@ import urllib.error import urllib.parse from argparse import ArgumentParser, Namespace from dataclasses import dataclass, field -from enum import StrEnum +from enum import Enum, StrEnum from pathlib import Path from typing import Any, Self from urllib.request import Request, urlopen @@ -35,6 +35,10 @@ from archinstall.lib.version import get_version from archinstall.tui.components import tui +class SubCommand(Enum): + SHARE_LOG = 'share-log' + + @p_dataclass class Arguments: config: Path | None = None @@ -58,6 +62,8 @@ class Arguments: advanced: bool = False verbose: bool = False + command: SubCommand | None = None + class ArchConfigType(StrEnum): VERSION = 'version' @@ -365,13 +371,13 @@ class ArchConfig: class ArchConfigHandler: def __init__(self) -> None: self._parser: ArgumentParser = self._define_arguments() - args: Arguments = self._parse_args() - self._args = args + self._add_sub_parsers() + self._args: Arguments = self._parse_args() config = self._parse_config() try: - self._config = ArchConfig.from_config(config, args) + self._config = ArchConfig.from_config(config, self._args) self._config.version = get_version() except ValueError as err: warn(str(err)) @@ -397,8 +403,13 @@ class ArchConfigHandler: def print_help(self) -> None: self._parser.print_help() + def _add_sub_parsers(self) -> None: + subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands') + _ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server') + def _define_arguments(self) -> ArgumentParser: parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument( '-v', '--version', diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 86a47ed1..bc9bc7a3 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -185,6 +185,10 @@ class Logger: def path(self) -> Path: return self._path / 'install.log' + @path.setter + def path(self, value: Path) -> None: + self._path = value + @property def directory(self) -> Path: return self._path @@ -212,6 +216,17 @@ class Logger: level_name = logging.getLevelName(level) f.write(f'[{ts}] - {level_name} - {content}\n') + def get_content(self, max_bytes: int | None = None) -> bytes: + content = self.path.read_bytes() + + if max_bytes is not None: + size = self.path.stat().st_size + + if size > max_bytes: + content = content[-max_bytes:] + + return content + logger = Logger() @@ -295,6 +310,11 @@ def _stylize_output( return f'\033[{ansi}m{text}\033[0m' +def _timestamp() -> str: + now = datetime.now(tz=UTC) + return now.strftime('%Y-%m-%d %H:%M:%S') + + def info( *msgs: str, level: int = logging.INFO, @@ -306,11 +326,6 @@ def info( log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font) -def _timestamp() -> str: - now = datetime.now(tz=UTC) - return now.strftime('%Y-%m-%d %H:%M:%S') - - def debug( *msgs: str, level: int = logging.DEBUG, @@ -368,35 +383,20 @@ def log( def share_install_log( - paste_url: str = 'https://paste.rs', - max_size: int = 10 * 1024 * 1024, - confirm: Callable[[str], bool] = lambda _: True, -) -> int: + paste_url: str, + max_bytes: int | None = None, +) -> str | None: log_path = logger.path if not log_path.exists(): info(f'Log file not found: {log_path}') - return 1 + return None - size = log_path.stat().st_size - if size == 0: + content = logger.get_content(max_bytes=max_bytes) + + if len(content) == 0: info(f'Log file is empty: {log_path}') - return 1 - - if size > max_size: - info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes') - content = log_path.read_bytes()[-max_size:] - else: - content = log_path.read_bytes() - - header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n' - header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n' - header += 'The uploaded paste is public.\n\n' - header += 'Continue?' - - if not confirm(header): - info('Cancelled.') - return 1 + return None try: req = urllib.request.Request(paste_url, data=content) @@ -404,12 +404,10 @@ def share_install_log( url = response.read().decode().strip() except urllib.error.URLError as e: info(f'Upload failed: {e}') - return 1 + return None if not url.startswith('http'): info(f'Unexpected response from {paste_url}: {url[:200]!r}') - return 1 + return None - # raw print so the URL is pipe-friendly (no ANSI colors, no log prefix) - print(url) - return 0 + return url diff --git a/archinstall/main.py b/archinstall/main.py index 9b91681b..f30b01d3 100644 --- a/archinstall/main.py +++ b/archinstall/main.py @@ -8,13 +8,13 @@ import time import traceback from pathlib import Path -from archinstall.lib.args import ArchConfigHandler +from archinstall.lib.args import ArchConfigHandler, SubCommand from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.hardware import SysInfo from archinstall.lib.menu.helpers import Confirmation from archinstall.lib.network.wifi_handler import WifiHandler from archinstall.lib.networking import ping -from archinstall.lib.output import debug, error, info, share_install_log, warn +from archinstall.lib.output import debug, error, info, logger, share_install_log, warn from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.translationhandler import tr, translation_handler @@ -75,17 +75,36 @@ def _list_scripts() -> str: return '\n'.join(lines) -def _tui_confirm(header: str) -> bool: - async def _ask() -> bool: +def _share_log_command() -> None: + paste_url: str = 'https://paste.rs' + log_path = logger.path + max_size = 10 * 1024 * 1024 # max supported size by paste.rs + content = logger.get_content(max_bytes=max_size).decode() + + header = tr('About to upload "{}" to the publicly accessible {}').format(log_path, paste_url) + '\n\n' + header += tr('Do you want to continue?') + + group = MenuItemGroup.yes_no() + group.set_preview_for_all(lambda _: content) + + async def _confirm() -> bool: result = await Confirmation( - group=MenuItemGroup.yes_no(), header=header, allow_skip=False, - preset=False, + group=group, + preview_header='Log content', + preview_location='bottom', ).show() return result.get_value() - return tui.run(_ask) + result = tui.run(_confirm) + + if result is True: + res = share_install_log(paste_url=paste_url, max_bytes=max_size) + if res is not None: + info(tr('Log uploaded successfully. URL: {}').format(res)) + else: + error(tr('Failed to upload log.')) def run() -> int: @@ -94,15 +113,19 @@ def run() -> int: OR straight as a module: python -m archinstall In any case we will be attempting to load the provided script to be run from the scripts/ folder """ - if 'share-log' in sys.argv: - return share_install_log(confirm=_tui_confirm) - arch_config_handler = ArchConfigHandler() if '--help' in sys.argv or '-h' in sys.argv: arch_config_handler.print_help() return 0 + match arch_config_handler.args.command: + case SubCommand.SHARE_LOG: + _share_log_command() + exit(0) + case None: + pass + script = arch_config_handler.get_script() if script == 'list': diff --git a/pyproject.toml b/pyproject.toml index 636c8017..133bda54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dev = [ "ruff==0.15.13", "pylint==4.0.5", "pytest==9.0.3", + "hypothesis>=6.152.4", ] doc = ["sphinx"] diff --git a/tests/test_share_log.py b/tests/test_share_log.py index c1c3ca84..a6706da8 100644 --- a/tests/test_share_log.py +++ b/tests/test_share_log.py @@ -1,94 +1,127 @@ # pylint: disable=redefined-outer-name +import string import urllib.error from io import BytesIO from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st from archinstall.lib.output import share_install_log +urls = st.builds( + '{}://{}.{}/{}'.format, + st.sampled_from(['http', 'https']), + st.text(alphabet=string.ascii_lowercase, min_size=3, max_size=10), + st.sampled_from(['com', 'net', 'org', 'rs']), + st.text(alphabet=string.ascii_lowercase + string.digits, min_size=0, max_size=8), +) -@pytest.fixture() +max_bytes = st.one_of(st.none(), st.integers(min_value=1, max_value=130)) + +random_paths = st.lists( + st.text( + alphabet=string.ascii_lowercase + string.digits, + min_size=1, + max_size=10, + ), + min_size=1, + max_size=5, +).map(lambda parts: Path(*parts)) + + +@pytest.fixture def log_file(tmp_path: Path) -> Path: log_dir = tmp_path / 'archinstall' log_dir.mkdir() return log_dir / 'install.log' -def _fake_logger(log_file: Path) -> MagicMock: - mock = MagicMock() - mock.path = log_file - return mock +# def _fake_logger(log_file: Path) -> MagicMock: +# mock = MagicMock() +# mock.path = log_file +# return mock -def test_file_not_found(tmp_path: Path) -> None: - missing = tmp_path / 'no-such' / 'install.log' - with patch('archinstall.lib.output.logger', _fake_logger(missing)): - assert share_install_log() == 1 +@given(paste_url=urls, max_byte=max_bytes, sub_path=random_paths) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_file_not_found( + tmp_path: Path, + sub_path: Path, + paste_url: str, + max_byte: int | None, +) -> None: + missing_log = tmp_path / sub_path / 'install.log' + + with patch('archinstall.lib.output.logger._path', new=missing_log): + assert share_install_log(paste_url, max_byte) is None -def test_empty_file(log_file: Path) -> None: +@given(paste_url=urls, max_byte=max_bytes) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_empty_file(log_file: Path, paste_url: str, max_byte: int | None) -> None: log_file.write_bytes(b'') - with patch('archinstall.lib.output.logger', _fake_logger(log_file)): - assert share_install_log() == 1 + + with patch('archinstall.lib.output.logger._path', new=log_file.parent): + # with patch('archinstall.lib.output.logger', _fake_logger(log_file)): + assert share_install_log(paste_url, max_byte) is None -def test_user_cancels(log_file: Path) -> None: +@given(paste_url=urls, resp_url=urls, max_byte=max_bytes) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_successful_upload(log_file: Path, resp_url: str, paste_url: str, max_byte: int | None) -> None: log_file.write_text('some log content') - with patch('archinstall.lib.output.logger', _fake_logger(log_file)): - assert share_install_log(confirm=lambda _: False) == 1 - - -def test_successful_upload(log_file: Path) -> None: - log_file.write_text('some log content') - fake_response = BytesIO(b'https://paste.rs/abc.def') + fake_response = BytesIO(resp_url.encode()) with ( - patch('archinstall.lib.output.logger', _fake_logger(log_file)), - patch('urllib.request.urlopen', return_value=fake_response) as mock_open, + patch('archinstall.lib.output.logger._path', new=log_file.parent), + patch('urllib.request.urlopen', return_value=fake_response), ): - result = share_install_log() - - assert result == 0 - req = mock_open.call_args[0][0] - assert req.data == b'some log content' + result = share_install_log(paste_url, max_byte) + assert result == resp_url -def test_truncation(log_file: Path) -> None: - max_size = 100 +@given(paste_url=urls, resp_url=urls, max_byte=max_bytes) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_truncation(log_file: Path, resp_url: str, paste_url: str, max_byte: int | None) -> None: content = b'A' * 50 + b'B' * 80 log_file.write_bytes(content) - fake_response = BytesIO(b'https://paste.rs/abc.def') + fake_response = BytesIO(resp_url.encode()) + + exptected_byte = len(content) if max_byte is None else max_byte with ( - patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('archinstall.lib.output.logger._path', new=log_file.parent), patch('urllib.request.urlopen', return_value=fake_response) as mock_open, ): - result = share_install_log(max_size=max_size) - - assert result == 0 - req = mock_open.call_args[0][0] - assert len(req.data) == max_size - assert req.data == content[-max_size:] + _ = share_install_log(paste_url, max_byte) + req = mock_open.call_args[0][0] + assert len(req.data) == exptected_byte + assert req.data == content[-exptected_byte:] -def test_network_error(log_file: Path) -> None: +@given(paste_url=urls, max_byte=max_bytes) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_network_error(log_file: Path, paste_url: str, max_byte: int | None) -> None: log_file.write_text('some log content') with ( - patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('archinstall.lib.output.logger._path', new=log_file.parent), patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')), ): - assert share_install_log() == 1 + assert share_install_log(paste_url, max_byte) is None -def test_unexpected_response(log_file: Path) -> None: +@given(paste_url=urls, max_byte=max_bytes) +@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_unexpected_response(log_file: Path, paste_url: str, max_byte: int | None) -> None: log_file.write_text('some log content') fake_response = BytesIO(b'ERROR: something went wrong') with ( - patch('archinstall.lib.output.logger', _fake_logger(log_file)), + patch('archinstall.lib.output.logger._path', new=log_file.parent), patch('urllib.request.urlopen', return_value=fake_response), ): - assert share_install_log() == 1 + assert share_install_log(paste_url, max_byte) is None From 42d9113611e51473a9bc32d30cb4b9dd6c1963cf Mon Sep 17 00:00:00 2001 From: utuhiro78 <34818411+utuhiro78@users.noreply.github.com> Date: Sat, 16 May 2026 08:29:27 +0900 Subject: [PATCH 10/15] Update Japanese translation (#4544) --- archinstall/locales/ja/LC_MESSAGES/base.mo | Bin 73831 -> 77304 bytes archinstall/locales/ja/LC_MESSAGES/base.po | 173 ++++++++++++++++++++- 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/archinstall/locales/ja/LC_MESSAGES/base.mo b/archinstall/locales/ja/LC_MESSAGES/base.mo index 96b625c6736d9cfbb9a41f4b467788c4be643657..7d6b89f35c6740d4de8175f15239ebd5fca9d0df 100644 GIT binary patch delta 17999 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 delta 15307 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 Date: Sun, 17 May 2026 03:26:30 +0200 Subject: [PATCH 11/15] Update Italian translation + fix a string (#4545) * Update Italian translation * Added new strings * Improved existing translations * Fix wrong string + Italian translation * Mispelled "respository" -> "repository" --- archinstall/lib/mirror/mirror_menu.py | 2 +- archinstall/locales/base.pot | 159 ++++++++ archinstall/locales/it/LC_MESSAGES/base.mo | Bin 64821 -> 70130 bytes archinstall/locales/it/LC_MESSAGES/base.po | 426 ++++++++++++++++----- 4 files changed, 495 insertions(+), 92 deletions(-) diff --git a/archinstall/lib/mirror/mirror_menu.py b/archinstall/lib/mirror/mirror_menu.py index 5158bffd..275c4df8 100644 --- a/archinstall/lib/mirror/mirror_menu.py +++ b/archinstall/lib/mirror/mirror_menu.py @@ -65,7 +65,7 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]): async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None: edit_result = await Input( - header=tr('Enter a respository name'), + header=tr('Enter a repository name'), allow_skip=True, default_value=preset.name if preset else None, ).show() diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 0b08df29..e1681526 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -2304,3 +2304,162 @@ msgstr "" #, python-brace-format msgid "Choose an option how to give {} access to your hardware" msgstr "" + +#, python-brace-format +msgid "About to upload \"{}\" to the publicly accessible {}" +msgstr "" + +msgid "Do you want to continue?" +msgstr "" + +#, python-brace-format +msgid "Log uploaded successfully. URL: {}" +msgstr "" + +msgid "Failed to upload log." +msgstr "" + +msgid "ArchInstall Language" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Installation Script" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Services" +msgstr "" + +msgid "Custom commands" +msgstr "" + +msgid "Users" +msgstr "" + +msgid "Root encrypted password" +msgstr "" + +msgid "Zram enabled" +msgstr "" + +#, python-brace-format +msgid "Zram algorithm {}" +msgstr "" + +msgid "Bluetooth enabled" +msgstr "" + +#, python-brace-format +msgid "Audio server \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Power management \"{}\"" +msgstr "" + +msgid "Print service enabled" +msgstr "" + +#, python-brace-format +msgid "Firewall \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Extra fonts \"{}\"" +msgstr "" + +msgid "Root password set" +msgstr "" + +#, python-brace-format +msgid "Configured {} user(s)" +msgstr "" + +msgid "U2F set up" +msgstr "" + +#, python-brace-format +msgid "Bootloader \"{}\"" +msgstr "" + +msgid "UKI enabled" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Manual" +msgstr "" + +msgid "Pre-mount" +msgstr "" + +#, python-brace-format +msgid "{} layout" +msgstr "" + +#, python-brace-format +msgid "Devices {}" +msgstr "" + +msgid "LVM set up" +msgstr "" + +#, python-brace-format +msgid "{} encryption" +msgstr "" + +#, python-brace-format +msgid "Btrfs snapshot \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Keyboard layout \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Locale language \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Locale encoding \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Console font \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Mirror regions \"{}\"" +msgstr "" + +#, python-brace-format +msgid "Optional repositories \"{}\"" +msgstr "" + +msgid "Custom servers set up" +msgstr "" + +msgid "Custom repositories set up" +msgstr "" + +msgid "Use standalone iwd" +msgstr "" + +msgid "Color enabled" +msgstr "" + +#, python-brace-format +msgid "{} grphics driver" +msgstr "" + +#, python-brace-format +msgid "{} greeter" +msgstr "" + +msgid "Enter a repository name" +msgstr "" diff --git a/archinstall/locales/it/LC_MESSAGES/base.mo b/archinstall/locales/it/LC_MESSAGES/base.mo index 79d934d42a09dd31cd159321dea69366608193a5..a4166e67fea987d29fb2178c4d252f454b7e95dc 100644 GIT binary patch delta 20840 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, Van Matten\n" "Language: it\n" @@ -56,7 +56,7 @@ msgid "Only packages such as base, base-devel, linux, linux-firmware, efibootmgr msgstr "Vengono installati solo pacchetti come base, base-devel, linux, linux-firmware, efibootmgr e pacchetti di profilo opzionali." msgid "Note: base-devel is no longer installed by default. Add it here if you need build tools." -msgstr "Nota: base-devel non è più installato di default. Aggiungilo qui se ti servono i build tools." +msgstr "Nota: base-devel non è più installato come predefinito. Aggiungilo qui se ti servono i build tools." msgid "If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt." msgstr "Se desideri un browser web, come Firefox o Chromium, puoi specificarlo nel seguente prompt." @@ -118,7 +118,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona per indice quali partizioni eliminare" +"Seleziona per indice le partizioni da eliminare" msgid "" "{}\n" @@ -127,13 +127,13 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona per indice quale partizione montare dove" +"Seleziona per indice la partizione dove montare" msgid " * Partition mount-points are relative to inside the installation, the boot would be /boot as an example." -msgstr " * I punti di mount della partizione sono relativi all'interno dell'installazione, per esempio l'avvio sarebbe /boot." +msgstr " * I punti di montaggio della partizione sono relativi all'interno dell'installazione, per esempio l'avvio sarebbe /boot." msgid "Select where to mount partition (leave blank to remove mountpoint): " -msgstr "Seleziona dove montare la partizione (lascia vuoto per rimuovere il punto di mount): " +msgstr "Seleziona dove montare la partizione (lascia vuoto per rimuovere il punto di montaggio): " msgid "" "{}\n" @@ -142,7 +142,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona quale partizione mascherare per la formattazione" +"Seleziona la partizione da mascherare per la formattazione" msgid "" "{}\n" @@ -151,7 +151,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona quale partizione contrassegnare come crittografata" +"Seleziona la partizione da contrassegnare come crittografata" msgid "" "{}\n" @@ -160,7 +160,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona quale partizione contrassegnare come avviabile" +"Seleziona la partizione da contrassegnare come avviabile" msgid "" "{}\n" @@ -169,7 +169,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona su quale partizione impostare un filesystem" +"Seleziona la partizione su cui impostare un filesystem" msgid "Enter a desired filesystem type for the partition: " msgstr "Inserisci un tipo di filesystem desiderato per la partizione: " @@ -187,7 +187,7 @@ msgid "Select what you wish to do with the selected block devices" msgstr "Seleziona cosa desideri fare con i dispositivi a blocchi selezionati" msgid "This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments" -msgstr "Questo è un elenco di profili pre-programmati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop" +msgstr "Questo è un elenco di profili preprogrammati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop" msgid "Select keyboard layout" msgstr "Seleziona il layout della tastiera" @@ -196,7 +196,7 @@ msgid "Select one of the regions to download packages from" msgstr "Seleziona una delle regioni da cui scaricare i pacchetti" msgid "Select one or more hard drives to use and configure" -msgstr "Seleziona uno o più dischi rigidi da utilizzare e configurare" +msgstr "Seleziona uno o più unità da utilizzare e configurare" msgid "For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options." msgstr "Per la migliore compatibilità con il tuo hardware AMD, potresti voler utilizzare tutte le opzioni open source o AMD / ATI." @@ -220,13 +220,13 @@ msgid "All open-source (default)" msgstr "Tutti gli open source (predefinito)" msgid "Choose which kernels to use or leave blank for default \"{}\"" -msgstr "Scegli quali kernel usare o lascia vuoto per il predefinito \"{}\"" +msgstr "Scegli i kernel da usare o lascia vuoto per il predefinito \"{}\"" msgid "Choose which locale language to use" -msgstr "Scegli quale lingua locale utilizzare" +msgstr "Scegli la lingua da usare" msgid "Choose which locale encoding to use" -msgstr "Scegli quale codifica locale utilizzare" +msgstr "Scegli la codifica da usare" msgid "Select one of the values shown below: " msgstr "Seleziona uno dei valori mostrati di seguito: " @@ -235,7 +235,7 @@ msgid "Select one or more of the options below: " msgstr "Seleziona una o più delle seguenti opzioni " msgid "Adding partition...." -msgstr "Aggiungendo la partizione...." +msgstr "Aggiunta della partizione in corso..." msgid "You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's." msgstr "Devi inserire un tipo di filesystem valido per continuare. Vedi `man parted` per tipi di filesystem validi." @@ -253,13 +253,16 @@ msgid "Mirror region" msgstr "Regione dei mirror" msgid "Locale language" -msgstr "Lingua locale" +msgstr "Lingua" msgid "Locale encoding" -msgstr "Codifica locale" +msgstr "Codifica" + +msgid "Console font" +msgstr "Font della console" msgid "Drive(s)" -msgstr "Dischi" +msgstr "Unità" msgid "Disk layout" msgstr "Layout del disco" @@ -301,7 +304,7 @@ msgid "Automatic time sync (NTP)" msgstr "Sincronizzazione automatica dell'ora (NTP)" msgid "Install ({} config(s) missing)" -msgstr "Installa ({} configurazioni mancanti)" +msgstr "Installa ({} configurazione/i mancante/i)" msgid "" "You decided to skip harddrive selection\n" @@ -309,7 +312,7 @@ msgid "" "WARNING: Archinstall won't check the suitability of this setup\n" "Do you wish to continue?" msgstr "" -"Hai deciso di saltare la selezione del disco rigido\n" +"Hai deciso di saltare la selezione dell'unità\n" "e utilizzerà qualsiasi configurazione dell'unità montata su {} (sperimentale)\n" "ATTENZIONE: Archinstall non verificherà l'idoneità di questa configurazione\n" "Vuoi continuare?" @@ -327,7 +330,7 @@ msgid "Clear/Delete all partitions" msgstr "Cancella/Elimina tutte le partizioni" msgid "Assign mount-point for a partition" -msgstr "Assegna punto di mount per una partizione" +msgstr "Assegna punto di montaggio per una partizione" msgid "Mark/Unmark a partition to be formatted (wipes data)" msgstr "Seleziona/Deseleziona una partizione da formattare (cancella i dati)" @@ -376,7 +379,7 @@ msgid "Enter a encryption password for {}" msgstr "Inserisci una password di crittografia per {}" msgid "Enter disk encryption password (leave blank for no encryption): " -msgstr "Inserisci la password di crittografia del disco (lasciare vuoto per nessuna crittografia): " +msgstr "Inserisci la password di crittografia del disco (lascia vuoto per nessuna crittografia): " msgid "Create a required super-user with sudo privileges: " msgstr "Crea un superuser richiesto con privilegi sudo: " @@ -454,7 +457,7 @@ msgid "Pre-existing pacman lock never exited. Please clean up any existing pacma msgstr "Il lock di pacman preesistente non è mai terminato. Rimuovi ogni sessione pacman esistente prima di usare archinstall." msgid "Choose which optional additional repositories to enable" -msgstr "Scegli quali repository aggiuntivi facoltativi abilitare" +msgstr "Scegli i repository aggiuntivi facoltativi da abilitare" msgid "Add a user" msgstr "Aggiungi un utente" @@ -476,7 +479,7 @@ msgstr "" "Definisci un nuovo utente\n" msgid "User Name : " -msgstr "Nome utente : " +msgstr "Nome utente: " msgid "Should {} be a superuser (sudoer)?" msgstr "{} dovrebbe essere un superuser (sudoer)?" @@ -497,7 +500,7 @@ msgid "" msgstr "" "{}\n" "\n" -"Seleziona su quale partizione impostare i sottovolumi" +"Seleziona la partizione su cui impostare i sottovolumi" msgid "Manage btrfs subvolumes for current partition" msgstr "Gestisci i sottovolumi btrfs per la partizione corrente" @@ -518,7 +521,7 @@ msgid "Save all" msgstr "Salva tutto" msgid "Choose which configuration to save" -msgstr "Scegli quale configurazione salvare" +msgstr "Scegli la configurazione da salvare" msgid "Enter a directory for the configuration(s) to be saved: " msgstr "Inserisci una cartella in cui salvare le configurazioni: " @@ -570,7 +573,7 @@ msgid "Subvolume name " msgstr "Nome del sottovolume " msgid "Subvolume mountpoint" -msgstr "Punto di mount del sottovolume" +msgstr "Punto di montaggio del sottovolume" msgid "Subvolume options" msgstr "Opzioni del sottovolume" @@ -579,10 +582,10 @@ msgid "Save" msgstr "Salva" msgid "Subvolume name :" -msgstr "Nome del sottovolume :" +msgstr "Nome del sottovolume:" msgid "Select a mount point :" -msgstr "Seleziona un punto di mount:" +msgstr "Seleziona un punto di montaggio:" msgid "Select the desired subvolume options " msgstr "Seleziona le opzioni del sottovolume desiderate " @@ -607,10 +610,10 @@ msgid "The selected drives do not have the minimum capacity required for an auto msgstr "Le unità selezionate non hanno la capacità minima richiesta per un suggerimento automatico\n" msgid "Minimum capacity for /home partition: {}GB\n" -msgstr "Capacità minima per la partizione /home: {}GB\n" +msgstr "Capacità minima per la partizione /home: {} GB\n" msgid "Minimum capacity for Arch Linux partition: {}GB" -msgstr "Capacità minima per la partizione Arch Linux: {}GB" +msgstr "Capacità minima per la partizione Arch Linux: {} GB" msgid "Continue" msgstr "Continua" @@ -661,13 +664,13 @@ msgid "Select your desired desktop environment" msgstr "Seleziona l'ambiente desktop desiderato" msgid "A very basic installation that allows you to customize Arch Linux as you see fit." -msgstr "Un'installazione molto semplice che ti consente di personalizzare Arch Linux come meglio credi." +msgstr "Un'installazione molto semplificata che ti consente di personalizzare Arch Linux come meglio credi." msgid "Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb" msgstr "Fornisce una selezione di vari pacchetti server da installare e abilitare, per esempio httpd, nginx, mariadb" msgid "Choose which servers to install, if none then a minimal installation will be done" -msgstr "Scegli quali server installare, se nessuno verrà eseguita un'installazione minima" +msgstr "Scegli i server da installare, se non ne sarà selezionato nessuno, verrà eseguita un'installazione minima" msgid "Installs a minimal system as well as xorg and graphics drivers." msgstr "Installa un sistema minimo oltre a xorg e driver grafici." @@ -682,13 +685,13 @@ msgid "Are you sure you want to reset this setting?" msgstr "Sei sicuro di voler ripristinare questa impostazione?" msgid "Select one or more hard drives to use and configure\n" -msgstr "Seleziona uno o più dischi rigidi da utilizzare e configurare\n" +msgstr "Seleziona uno o più unità da utilizzare e configurare\n" msgid "Any modifications to the existing setting will reset the disk layout!" msgstr "Qualsiasi modifica all'impostazione esistente ripristinerà il layout del disco!" msgid "If you reset the harddrive selection this will also reset the current disk layout. Are you sure?" -msgstr "Se si ripristina la selezione del disco rigido, verrà ripristinato anche il layout del disco corrente. Sei sicuro?" +msgstr "Se ripristini la selezione dell'unità, verrà ripristinato anche il layout del disco corrente. Sei sicuro?" msgid "Save and exit" msgstr "Salva ed esci" @@ -741,7 +744,7 @@ msgid "Select one of the disks or skip and use /mnt as default" msgstr "Seleziona uno dei dischi o salta e usa /mnt come predefinito" msgid "Select which partitions to mark for formatting:" -msgstr "Seleziona quali partizioni contrassegnare per la formattazione:" +msgstr "Seleziona le partizioni da contrassegnare per la formattazione:" msgid "Use HSM to unlock encrypted drive" msgstr "Utilizza HSM per sbloccare l'unità crittografata" @@ -798,14 +801,14 @@ msgid "Configured {} interfaces" msgstr "Interfacce {} configurate" msgid "This option enables the number of parallel downloads that can occur during installation" -msgstr "Questa opzione consente di impostare il numero di download paralleli che possono avvenire durante l'installazione" +msgstr "Questa opzione consente di impostare il numero di download in parallelo che possono avvenire durante l'installazione" msgid "" "Enter the number of parallel downloads to be enabled.\n" " (Enter a value between 1 to {})\n" "Note:" msgstr "" -"Inserisci il numero di download paralleli da abilitare.\n" +"Inserisci il numero di download in parallelo da abilitare.\n" " (Inserisci un valore compreso tra 1 e {})\n" "Nota:" @@ -816,20 +819,33 @@ msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads a msgstr " - Valore minimo : 1 ( Consente 1 download parallelo, consente 2 download alla volta )" msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )" -msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download parallelo, consente solo 1 download alla volta )" +msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download in parallelo, consente solo 1 download alla volta )" #, python-brace-format msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]" msgstr "Input non valido! Riprova con un input valido [da 1 a {max_downloads}, o 0 per disabilitare]." msgid "Parallel Downloads" -msgstr "Download paralleli" +msgstr "Download in parallelo" + +msgid "Pacman" +msgstr "Pacman" + +msgid "Color" +msgstr "Colore" + +#, python-brace-format +msgid "Enter the number of parallel downloads (1-{})" +msgstr "Inserisci il numero di download in parallelo (1-{})" + +msgid "Enable colored output for pacman" +msgstr "Attiva l'output colorato di pacman" msgid "ESC to skip" msgstr "ESC per saltare" msgid "CTRL+C to reset" -msgstr "CTRL+C per resettare" +msgstr "CTRL+C per ripristinare" msgid "TAB to select" msgstr "TAB per selezionare" @@ -861,27 +877,27 @@ msgid "Select one or more devices to use and configure" msgstr "Seleziona uno o più dispositivi da utilizzare e configurare" msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?" -msgstr "Se si ripristina la selezione del disco rigido, verrà ripristinato anche il layout del disco corrente. Sei sicuro?" +msgstr "Se ripristini la selezione del dispositivo, verrà ripristinato anche il layout del disco corrente. Sei sicuro?" msgid "Existing Partitions" msgstr "Partizioni esistenti" msgid "Select a partitioning option" -msgstr "Selezione opzione di partizionamento" +msgstr "Seleziona un'opzione di partizionamento" msgid "Enter the root directory of the mounted devices: " msgstr "Inserisci la cartella principale dei dispositivi montati: " #, python-brace-format msgid "Minimum capacity for /home partition: {}GiB\n" -msgstr "Capacità minima per la partizione /home: {}GiB\n" +msgstr "Capacità minima per la partizione /home: {} GiB\n" #, python-brace-format msgid "Minimum capacity for Arch Linux partition: {}GiB" -msgstr "Capacità minima per la partizione Arch Linux: {}GiB" +msgstr "Capacità minima per la partizione Arch Linux: {} GiB" msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments" -msgstr "Questo è un elenco di profili pre-programmati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop" +msgstr "Questo è un elenco di profiles_bck preprogrammati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop" msgid "Current profile selection" msgstr "Selezione profilo corrente" @@ -890,7 +906,7 @@ msgid "Remove all newly added partitions" msgstr "Elimina tutte le partizioni appena aggiunte" msgid "Assign mountpoint" -msgstr "Assegna punto di mount" +msgstr "Assegna punto di montaggio" msgid "Mark/Unmark to be formatted (wipes data)" msgstr "Seleziona/Deseleziona come da formattare (cancella i dati)" @@ -917,13 +933,13 @@ msgid "This partition is currently encrypted, to format it a filesystem has to b msgstr "Questa partizione è attualmente crittografata, per formattarla è necessario specificare un filesystem" msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example." -msgstr "I punti di mount della partizione sono relativi all'interno dell'installazione, l'avvio sarebbe per esempio /boot." +msgstr "I punti di montaggio della partizione sono relativi all'interno dell'installazione, l'avvio sarebbe per esempio /boot." msgid "If mountpoint /boot is set, then the partition will also be marked as bootable." -msgstr "Se il punto di mount /boot è impostato, anche la partizione sarà contrassegnata come avviabile." +msgstr "Se il punto di montaggio /boot è impostato, anche la partizione sarà contrassegnata come avviabile." msgid "Mountpoint: " -msgstr "Punto di mount: " +msgstr "Punto di montaggio: " msgid "Current free sectors on device {}:" msgstr "Settori attualmente liberi sul dispositivo {}:" @@ -985,7 +1001,7 @@ msgid "Partitions to be encrypted" msgstr "Partizioni da crittografare" msgid "Select disk encryption option" -msgstr "Selezione opzione per crittografia disco" +msgstr "Seleziona un'opzione per la crittografia del disco" msgid "Select a FIDO2 device to use for HSM" msgstr "Seleziona un dispositivo FIDO2 da utilizzare per HSM" @@ -1028,7 +1044,7 @@ msgid "Back" msgstr "Indietro" msgid "Please chose which greeter to install for the chosen profiles: {}" -msgstr "Scegli quale greeter installare per i profili scelti: {}" +msgstr "Scegli il greeter da installare per i profili scelti: {}" #, python-brace-format msgid "Environment type: {}" @@ -1075,7 +1091,7 @@ msgstr "" "Seleziona un driver grafico o lascia vuoto per installare tutti i driver open source" msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" -msgstr "Sway ha bisogno dell’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" +msgstr "Sway richiede l’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" msgid "" "\n" @@ -1093,13 +1109,13 @@ msgid "Greeter" msgstr "Greeter" msgid "Please chose which greeter to install" -msgstr "Scegli quale greeter installare" +msgstr "Scegli il greeter da installare" msgid "This is a list of pre-programmed default_profiles" msgstr "Questo è un elenco di default_profiles preprogrammati" msgid "Disk configuration" -msgstr "Configurazione disco" +msgstr "Configurazione del disco" msgid "Profiles" msgstr "Profili" @@ -1144,7 +1160,7 @@ msgid "" "Enter a directory for the configuration(s) to be saved (tab completion enabled)\n" "Save directory: " msgstr "" -"Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab abilitato)\n" +"Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab attivo)\n" "Cartella di salvataggio: " msgid "" @@ -1166,7 +1182,7 @@ msgid "Mirror regions" msgstr "Regione dei mirror" msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )" -msgstr " - Valore massimo : {} ( Consente {} download paralleli, consente {max_downloads+1} downloads alla volta )" +msgstr " - Valore massimo : {} ( Consente {} download in parallelo, consente {max_downloads+1} download alla volta )" msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]" msgstr "Input non valido! Riprova con un input valido [da 1 a {}, o 0 per disabilitare]." @@ -1208,33 +1224,48 @@ msgstr "Prodotto" msgid "Invalid configuration: {error}" msgstr "Configurazione non valida: {error}" +msgid "Ready to install" +msgstr "Pronto per l'installazione" + +msgid "Disks" +msgstr "Dischi" + +msgid "Packages" +msgstr "Pacchetti" + +msgid "Network" +msgstr "Rete" + +msgid "Locale" +msgstr "Localizzazione" + msgid "Type" msgstr "Tipo" msgid "This option enables the number of parallel downloads that can occur during package downloads" -msgstr "Questa opzione consente di impostare il numero di download paralleli che possono avvenire durante l'installazione" +msgstr "Questa opzione consente di impostare il numero di download in parallelo che possono avvenire durante l'installazione" msgid "" "Enter the number of parallel downloads to be enabled.\n" "\n" "Note:\n" msgstr "" -"Inserisci il numero di download paralleli da abilitare.\n" +"Inserisci il numero di download in parallelo da abilitare.\n" "\n" "Nota:\n" #, python-brace-format msgid " - Maximum recommended value : {} ( Allows {} parallel downloads at a time )" -msgstr " - Valore massimo raccomandato : {} ( Consente {} download paralleli)" +msgstr " - Valore massimo raccomandato : {} ( Consente {} download in parallelo)" msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n" -msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download parallelo, consente solo 1 download alla volta )\n" +msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download in parallelo, consente solo 1 download alla volta )\n" msgid "Invalid input! Try again with a valid input [or 0 to disable]" msgstr "Input non valido! Riprova con un input valido [0 per disabilitare]." msgid "Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" -msgstr "Hyprland ha bisogno dell’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" +msgstr "Hyprland richiede l’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" msgid "" "\n" @@ -1270,7 +1301,7 @@ msgid "Selected profiles: " msgstr "Profili selezionati: " msgid "Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/" -msgstr "La sincronizzazione dell’orario non si sta completando, in attesa, leggi la documentazione alla ricerca di una soluzione: https://archinstall.readthedocs.io/" +msgstr "La sincronizzazione dell’orario non si sta completando, mentre aspetti leggi la documentazione alla ricerca di una soluzione: https://archinstall.readthedocs.io/" msgid "Mark/Unmark as nodatacow" msgstr "Seleziona/Deseleziona come nodatacow" @@ -1322,7 +1353,7 @@ msgid "LVM volumes to be encrypted" msgstr "Volumi LVM da crittografare" msgid "Select which LVM volumes to encrypt" -msgstr "Seleziona volumi LVM da crittografare" +msgstr "Seleziona i volumi LVM da crittografare" msgid "Default layout" msgstr "Layout predefinito" @@ -1361,7 +1392,7 @@ msgid "Seat access" msgstr "Accesso al posto" msgid "Mountpoint" -msgstr "Punto di mount" +msgstr "Punto di montaggio" msgid "HSM" msgstr "HSM" @@ -1479,7 +1510,7 @@ msgid "Directory" msgstr "Cartella" msgid "Enter a directory for the configuration(s) to be saved (tab completion enabled)" -msgstr "Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab abilitato)" +msgstr "Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab attivo)" #, python-brace-format msgid "Do you want to save the configuration file(s) to {}?" @@ -1513,7 +1544,7 @@ msgid "Choose an option to give Hyprland access to your hardware" msgstr "Scegli un’opzione per concedere a Hyprland l'accesso al tuo hardware" msgid "Additional repositories" -msgstr "Repository aggiuntive" +msgstr "Repository aggiuntivi" msgid "NTP" msgstr "NTP" @@ -1543,7 +1574,7 @@ msgid "HSM device" msgstr "Dispositivo HSM" msgid "Some packages could not be found in the repository" -msgstr "Alcuni pacchetti non sono stati trovati nella repository" +msgstr "Alcuni pacchetti non sono stati trovati nel repository" msgid "User" msgstr "Utente" @@ -1564,16 +1595,16 @@ msgid "Select any packages from the below list that should be installed addition msgstr "Seleziona dalla lista sotto i pacchetti aggiuntivi da installare" msgid "Add a custom repository" -msgstr "Aggiungi una repository personalizzata" +msgstr "Aggiungi un repository personalizzato" msgid "Change custom repository" -msgstr "Cambia repository personalizzata" +msgstr "Cambia repository personalizzato" msgid "Delete custom repository" -msgstr "Elimina repository personalizzata" +msgstr "Elimina repository personalizzato" msgid "Repository name" -msgstr "Nome repository" +msgstr "Nome del repository" msgid "Add a custom server" msgstr "Aggiungi un server personalizzato" @@ -1594,7 +1625,7 @@ msgid "Add custom servers" msgstr "Aggiungi server personalizzati" msgid "Add custom repository" -msgstr "Aggiungi repository personalizzate" +msgstr "Aggiungi repository personalizzato" msgid "Loading mirror regions..." msgstr "Caricamento regioni dei mirror in corso..." @@ -1609,7 +1640,7 @@ msgid "Custom servers" msgstr "Server personalizzati" msgid "Custom repositories" -msgstr "Repository personalizzate" +msgstr "Repository personalizzati" msgid "Only ASCII characters are supported" msgstr "Sono supportati solo caratteri ASCII" @@ -1756,7 +1787,7 @@ msgid "Toggle option" msgstr "Opzioni di attivazione" msgid "Scroll Up" -msgstr "Scorri Su" +msgstr "Scorri su" msgid "Scroll Down" msgstr "Scorri giù" @@ -1783,13 +1814,13 @@ msgid "Copy selected text" msgstr "Copia il testo selezionato" msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" -msgstr "labwc ha bisogno dell’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" +msgstr "labwc richiede l’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" msgid "Choose an option to give labwc access to your hardware" msgstr "Scegli un’opzione per concedere a labwc l’accesso al tuo hardware" msgid "niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)" -msgstr "niri ha bisogno dell’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" +msgstr "niri richiede l’accesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)" msgid "Choose an option to give niri access to your hardware" msgstr "Scegli un’opzione per concedere a niri l’accesso al tuo hardware" @@ -1832,7 +1863,7 @@ msgid "Do you want to encrypt the user_credentials.json file?" msgstr "Vuoi criptare il file di user_credentials.json?" msgid "Credentials file encryption password" -msgstr "Password di crittazione del file delle credenziali" +msgstr "Password di crittografia del file delle credenziali" #, python-brace-format msgid "Repositories: {}" @@ -1917,7 +1948,7 @@ msgid "No network connection found" msgstr "Nessuna connessione di rete trovata" msgid "Would you like to connect to a Wifi?" -msgstr "Desideri connetterti ad una Wi-FI?" +msgstr "Desideri connetterti ad una Wi-Fi?" msgid "No wifi interface found" msgstr "Nessuna interfaccia Wi-Fi trovata" @@ -2000,11 +2031,26 @@ msgstr "Usa NetworkManager (backend iwd)" msgid "Firewall" msgstr "Firewall" +msgid "Additional fonts" +msgstr "Font aggiuntivi" + +msgid "Select font packages to install" +msgstr "Seleziona i pacchetti di font da installare" + +msgid "Unicode font coverage for most languages" +msgstr "Copertura Unicode del font per la maggior parte delle lingue" + +msgid "color emoji for browsers and apps" +msgstr "emoji a colori per browser e app" + +msgid "Chinese, Japanese, Korean characters" +msgstr "Caratteri cinesi, giapponesi, coreani" + msgid "Select audio configuration" msgstr "Seleziona la configurazione audio" msgid "Enter credentials file decryption password" -msgstr "Inserici la password di decrittazione del file delle credenziali" +msgstr "Inserisci la password di decrittazione del file delle credenziali" msgid "Enter root password" msgstr "Inserisci la password di root" @@ -2025,7 +2071,7 @@ msgid "Select disks for the installation" msgstr "Seleziona il disco per l'installazione" msgid "Enter a mountpoint" -msgstr "Inserisci un punto di mount" +msgstr "Inserisci un punto di montaggio" #, python-brace-format msgid "Enter a size (default: {}): " @@ -2035,7 +2081,7 @@ msgid "Enter subvolume name" msgstr "Inserisci il nome del sottovolume" msgid "Enter subvolume mountpoint" -msgstr "Inserisci il punto di mount del sottovolume" +msgstr "Inserisci il punto di montaggio del sottovolume" msgid "Select a disk configuration" msgstr "Seleziona una configurazione del disco" @@ -2047,7 +2093,7 @@ msgid "You will use whatever drive-setup is mounted at the specified directory" msgstr "Userai qualunque configurazione del disco che sia montata nella cartella specificata" msgid "WARNING: Archinstall won't check the suitability of this setup" -msgstr "ATTENZIONE: Archinstall non verificherà la compatibilità di di questa configurazione" +msgstr "ATTENZIONE: Archinstall non verificherà la compatibilità di questa configurazione" msgid "Select main filesystem" msgstr "Seleziona il filesystem principale" @@ -2066,22 +2112,22 @@ msgid "Value must be between 1 and {}" msgstr "Il valore deve essere tra 1 e {}" msgid "Select which kernel(s) to install" -msgstr "Scegli quali kernel installare" +msgstr "Seleziona i kernel da installare" msgid "Enter a respository name" msgstr "Inserisci il nome di un repository" msgid "Enter the repository url" -msgstr "Inserisci l'URL della repository" +msgstr "Inserisci l'URL del repository" msgid "Enter server url" msgstr "Inserisci l'URL del server" msgid "Select mirror regions to be enabled" -msgstr "Seleziona quali regioni dei mirror abilitare" +msgstr "Seleziona le regioni dei mirror da abilitare" msgid "Select optional repositories to be enabled" -msgstr "Seleziona quali repository aggiuntive opzionali abilitare" +msgstr "Seleziona i repository opzionali da abilitare" msgid "Select an interface" msgstr "Seleziona un'interfaccia" @@ -2089,11 +2135,17 @@ msgstr "Seleziona un'interfaccia" msgid "Choose network configuration" msgstr "Scegli la configurazione di rete" +msgid "Recommended: Network Manager for desktop, Manual for server" +msgstr "Raccomandato: Network Manager per computer, Manuale per server" + +msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system." +msgstr "Attenzione: nessuna configurazione di rete selezionata. La rete dovrà essere impostata manualmente sul sistema installato." + msgid "No packages found" msgstr "Nessun pacchetto trovato" msgid "Select which greeter to install" -msgstr "Seleziona quale greeter installare" +msgstr "Seleziona il greeter da installare" msgid "Select a profile type" msgstr "Seleziona un tipo di profilo" @@ -2126,6 +2178,9 @@ msgstr "Il profilo desktop selezionato richiede un utente regolare per accedere msgid "Environment type: {} {}" msgstr "Tipo di ambiente: {} {}" +msgid "Input cannot be empty" +msgstr "Il campo non può essere vuoto" + msgid "Recommended" msgstr "Raccomandato" @@ -2156,5 +2211,194 @@ msgstr "Descrizione" msgid "Select a flavor of KDE Plasma to install" msgstr "Seleziona la configurazione di KDE Plasma da installare" -msgid "Input cannot be empty" -msgstr "Il campo non può essere vuoto" +msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games" +msgstr "Sostituto di Arial/Times/Courier, supporta il cirillico per Steam/giochi" + +msgid "wide Unicode coverage, good fallback font" +msgstr "ampia copertura Unicode, ottimo font di riserva" + +#, python-brace-format +msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}" +msgstr "Impostazione accesso U2F: {u2f_config.u2f_login_method.value}" + +#, python-brace-format +msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000" +msgstr "Predefinito: {DEFAULT_ITER_TIME} ms, Intervallo consigliato: 1000-60000" + +#, python-brace-format +msgid "Setting up U2F login: {}" +msgstr "Impostazione accesso U2F: {}" + +#, python-brace-format +msgid "Default: {}ms, Recommended range: 1000-60000" +msgstr "Predefinito: {} ms, Intervallo consigliato: 1000-60000" + +#, python-brace-format +msgid "{} needs access to your seat" +msgstr "{} richiede l’accesso al tuo posto" + +msgid "collection of hardware devices i.e. keyboard, mouse" +msgstr "insieme di dispositivi hardware, ad esempio tastiera e mouse" + +#, python-brace-format +msgid "Choose an option how to give {} access to your hardware" +msgstr "Scegli un’opzione per concedere a {} l’accesso al tuo hardware" + +#, python-brace-format +msgid "About to upload \"{}\" to the publicly accessible {}" +msgstr "\"{}\" sta per essere caricato per essere pubblicamente accessibile {}" + +msgid "Do you want to continue?" +msgstr "Vuoi continuare?" + +#, python-brace-format +msgid "Log uploaded successfully. URL: {}" +msgstr "Log caricato con successo. URL: {}" + +msgid "Failed to upload log." +msgstr "Impossibile caricare il log." + +msgid "ArchInstall Language" +msgstr "Lingua di ArchInstall" + +msgid "Version" +msgstr "Versione" + +msgid "Installation Script" +msgstr "Script di installazione" + +msgid "Application" +msgstr "Applicazione" + +msgid "Services" +msgstr "Servizi" + +msgid "Custom commands" +msgstr "Comandi personalizzati" + +msgid "Users" +msgstr "Utenti" + +msgid "Root encrypted password" +msgstr "Password di root crittografata" + +msgid "Zram enabled" +msgstr "Zram attiva" + +#, python-brace-format +msgid "Zram algorithm {}" +msgstr "Algoritmo Zram {}" + +msgid "Bluetooth enabled" +msgstr "Bluetooth attivo" + +#, python-brace-format +msgid "Audio server \"{}\"" +msgstr "Server audio \"{}\"" + +#, python-brace-format +msgid "Power management \"{}\"" +msgstr "Gestione energetica \"{}\"" + +msgid "Print service enabled" +msgstr "Servizio di stampa attivo" + +#, python-brace-format +msgid "Firewall \"{}\"" +msgstr "Firewall \"{}\"" + +#, python-brace-format +msgid "Extra fonts \"{}\"" +msgstr "Font aggiuntivi \"{}\"" + +msgid "Root password set" +msgstr "Imposta la password di root" + +#, python-brace-format +msgid "Configured {} user(s)" +msgstr "{} utente/i configurato/i" + +msgid "U2F set up" +msgstr "Imposta U2F" + +#, python-brace-format +msgid "Bootloader \"{}\"" +msgstr "Bootloader \"{}\"" + +msgid "UKI enabled" +msgstr "UKI attiva" + +msgid "Default" +msgstr "Predefinito" + +msgid "Manual" +msgstr "Manuale" + +msgid "Pre-mount" +msgstr "Premontaggio" + +#, python-brace-format +msgid "{} layout" +msgstr "Layout {}" + +#, python-brace-format +msgid "Devices {}" +msgstr "Dispositivi {}" + +msgid "LVM set up" +msgstr "Imposta LVM" + +#, python-brace-format +msgid "{} encryption" +msgstr "Crittografia {}" + +#, python-brace-format +msgid "Btrfs snapshot \"{}\"" +msgstr "Snapshot Btrfs \"{}\"" + +#, python-brace-format +msgid "Keyboard layout \"{}\"" +msgstr "Layout della tastiera \"{}\"" + +#, python-brace-format +msgid "Locale language \"{}\"" +msgstr "Lingua \"{}\"" + +#, python-brace-format +msgid "Locale encoding \"{}\"" +msgstr "Codifica \"{}\"" + +#, python-brace-format +msgid "Console font \"{}\"" +msgstr "Font della console \"{}\"" + +#, python-brace-format +msgid "Mirror regions \"{}\"" +msgstr "Regioni dei mirror \"{}\"" + +#, python-brace-format +msgid "Optional repositories \"{}\"" +msgstr "Repository opzionali \"{}\"" + +msgid "Custom servers set up" +msgstr "Imposta server personalizzati" + +msgid "Custom repositories set up" +msgstr "Imposta repository personalizzati" + +msgid "Use standalone iwd" +msgstr "Usa iwd standalone" + +msgid "Color enabled" +msgstr "Colore attivo" + +#, python-brace-format +msgid "{} grphics driver" +msgstr "Driver grafici {}" + +#, python-brace-format +msgid "{} greeter" +msgstr "Greeter {}" + +msgid "Enter a repository name" +msgstr "Inserisci il nome di un repository" From 22bf6e3c35164e9e8d47ce1fac9c63994c43e7a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 15:36:33 +1000 Subject: [PATCH 12/15] Update dependency arch/python-textual to v8.2.6 (#4546) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 133bda54..0f455c30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "pyparted==3.13.0", "pydantic==2.13.4", "cryptography==48.0.0", - "textual==8.2.5", + "textual==8.2.6", "markdown-it-py==4.0.0", "linkify-it-py==2.1.0", ] From dccd0c8ccf8d3212dd398a015c0210a5c9cff77e Mon Sep 17 00:00:00 2001 From: Softer Date: Mon, 18 May 2026 13:10:56 +0300 Subject: [PATCH 13/15] Add translation CI validation (#4519) * Fix broken localization: tr(f-string) never matches translation catalog tr(f'Invalid configuration: {error}') evaluates the f-string before tr() runs, so xgettext extracts the literal placeholder as the msgid while runtime passes the formatted string - the two never match. Switch to tr('...{}').format(...) and update msgid in base.pot. * Add CI validation for translations and pot_tools dev utility Add translation-check workflow with two jobs: - validate-po: msgfmt --check on changed .po files, .mo sync warning, tr(f-string) anti-pattern grep on changed .py files - validate-pot: verify all tr() strings exist in base.pot when .py files change Workflow only triggers on .py/.po/.pot file changes. Add scripts/pot_tools.py developer utility (stats, list, add_missing) for managing base.pot. * Fix code style: use tabs and reformat xgettext arguments Align check_pot_freshness.py and pot_tools.py with project indentation (tabs) and ruff format requirements. Sorry :-) * Replace custom PO parser with msgcmp, drop pot_tools.py Address review feedback: use standard gettext msgcmp instead of hand-rolled parser for base.pot freshness check. Remove pot_tools.py that duplicated locales_generator.sh functionality. * Move translation checks into locales_generator.sh, simplify CI workflow Use msgcmp instead of diff for base.pot validation to avoid failing on legacy stale entries - the same cascading breakage that killed the original workflow (disabled 2023, removed in #4483). * Fix broken .po files: duplicate msgid in Hindi, missing format args in Finnish --- .github/workflows/translation-check.yaml | 22 +++++ archinstall/lib/global_menu.py | 2 +- archinstall/locales/base.pot | 2 +- archinstall/locales/fi/LC_MESSAGES/base.po | 4 +- archinstall/locales/hi/LC_MESSAGES/base.po | 3 - archinstall/locales/locales_generator.sh | 99 +++++++++++++++++----- 6 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/translation-check.yaml diff --git a/.github/workflows/translation-check.yaml b/.github/workflows/translation-check.yaml new file mode 100644 index 00000000..15be16cf --- /dev/null +++ b/.github/workflows/translation-check.yaml @@ -0,0 +1,22 @@ +name: Translation validation +on: + push: + paths: + - 'archinstall/**/*.py' + - 'archinstall/locales/**' + - '.github/workflows/translation-check.yaml' + pull_request: + paths: + - 'archinstall/**/*.py' + - 'archinstall/locales/**' + - '.github/workflows/translation-check.yaml' +jobs: + translations: + name: Validate translations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Install gettext + run: sudo apt-get update && sudo apt-get install -y gettext + - name: Run translation checks + run: bash archinstall/locales/locales_generator.sh check diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index eac936bd..1e18890f 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -502,7 +502,7 @@ class GlobalMenu(AbstractMenu[None]): return text[:-1] # remove last new line if error := self._validate_bootloader(): - return tr(f'Invalid configuration: {error}') + return tr('Invalid configuration: {}').format(error) self.sync_all_to_config() summary = ConfigurationOutput(self._arch_config).as_summary() diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index e1681526..4db4554c 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -1245,7 +1245,7 @@ msgid "Product" msgstr "" #, python-brace-format -msgid "Invalid configuration: {error}" +msgid "Invalid configuration: {}" msgstr "" msgid "Ready to install" diff --git a/archinstall/locales/fi/LC_MESSAGES/base.po b/archinstall/locales/fi/LC_MESSAGES/base.po index 31cf6af2..9534151e 100644 --- a/archinstall/locales/fi/LC_MESSAGES/base.po +++ b/archinstall/locales/fi/LC_MESSAGES/base.po @@ -2190,7 +2190,7 @@ msgstr "Valittu työpöydän profiili vaati tavallisen käyttäjän kirjautumise #, python-brace-format msgid "Environment type: {} {}" -msgstr "Ympäristötyyppi: {}" +msgstr "Ympäristötyyppi: {} {}" msgid "Input cannot be empty" msgstr "Syöttö ei voi olla tyhjä" @@ -2245,4 +2245,4 @@ msgstr "Määritetään U2F laitetta käyttäjälle: {}" #, python-brace-format msgid "Default: {}ms, Recommended range: 1000-60000" -msgstr "Oletusarvo: 10000ms, suositeltu 1000-60000ms" +msgstr "Oletusarvo: {}ms, suositeltu 1000-60000ms" diff --git a/archinstall/locales/hi/LC_MESSAGES/base.po b/archinstall/locales/hi/LC_MESSAGES/base.po index 9d64d477..3cfe309f 100644 --- a/archinstall/locales/hi/LC_MESSAGES/base.po +++ b/archinstall/locales/hi/LC_MESSAGES/base.po @@ -1502,9 +1502,6 @@ msgstr "सक्षम" msgid "Disabled" msgstr "अक्षम" -msgid "Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues" -msgstr "कृपया इस समस्या (और फ़ाइल) को https://github.com/archlinux/archinstall/issues पर सबमिट करें।" - msgid "Mirror name" msgstr "मिरर का नाम" diff --git a/archinstall/locales/locales_generator.sh b/archinstall/locales/locales_generator.sh index 7ce2ea19..18977dfc 100755 --- a/archinstall/locales/locales_generator.sh +++ b/archinstall/locales/locales_generator.sh @@ -1,48 +1,109 @@ #!/usr/bin/env bash set -euo pipefail -cd $(dirname "$0")/.. +cd "$(dirname "$0")/.." -function update_lang() { - file=${1} +usage() { + echo "Usage: ${0} " + echo "" + echo "Commands:" + echo " all Regenerate base.pot and update all languages" + echo " Regenerate base.pot and update a single language" + echo " check Run translation validation checks" + echo " -h, --help Show this help" +} +generate_pot() { + find . -type f -iname '*.py' | sort \ + | xargs xgettext --no-location --omit-header --keyword='tr' \ + -d base -o locales/base.pot +} + +update_lang() { + local file=${1} echo "Updating: ${file}" + local path path=$(dirname "${file}") msgmerge --quiet --no-location --width 512 --backup none --update "${file}" locales/base.pot msgfmt -o "${path}/base.mo" "${file}" } - -function generate_all() { +cmd_generate_all() { + generate_pot for file in $(find locales/ -name "base.po"); do update_lang "${file}" done } -function generate_single_lang() { - lang_file="locales/${1}/LC_MESSAGES/base.po" - +cmd_generate_single() { + local lang_file="locales/${1}/LC_MESSAGES/base.po" if [ ! -f "${lang_file}" ]; then echo "Language files not found: ${lang_file}" exit 1 fi - + generate_pot update_lang "${lang_file}" } +cmd_check_po_syntax() { + echo "Checking .po syntax..." + local failed=0 + while IFS= read -r po; do + if ! msgfmt --check --output-file=/dev/null "$po" 2>&1; then + echo "FAIL: $po" + failed=1 + fi + done < <(find locales/ -name '*.po') + if [ "$failed" -eq 1 ]; then + echo "ERROR: some .po files have syntax errors" >&2 + return 1 + fi + echo "All .po files passed syntax check." +} + +cmd_check_no_tr_fstring() { + echo "Checking for tr(f-string) anti-pattern..." + if grep -rnE "tr\(\s*f['\"]" . --include='*.py'; then + echo "ERROR: use tr('...{}').format(...) instead of tr(f'...')" >&2 + return 1 + fi + echo "No tr(f-string) anti-pattern found." +} + +cmd_check_pot_freshness() { + # msgcmp (not diff) because base.pot carries legacy stale entries from + # --join-existing; diff would always fail until a full cleanup is done. + echo "Checking base.pot for missing strings..." + find . -type f -iname '*.py' | sort \ + | xargs xgettext --no-location --omit-header --keyword='tr' \ + -d base -o /tmp/generated.pot + if ! msgcmp --use-untranslated locales/base.pot /tmp/generated.pot; then + echo "ERROR: base.pot is missing strings - run: locales_generator.sh all" >&2 + return 1 + fi + echo "base.pot contains all translatable strings." +} + +cmd_check() { + local failed=0 + cmd_check_po_syntax || failed=1 + cmd_check_no_tr_fstring || failed=1 + cmd_check_pot_freshness || failed=1 + if [ "$failed" -eq 1 ]; then + echo "Some translation checks failed." >&2 + exit 1 + fi + echo "All translation checks passed." +} if [ $# -eq 0 ]; then - echo "Usage: ${0} " - echo "Special case 'all' for builds all languages." + usage exit 1 fi -lang=${1} - -# Update the base file containing all translatable strings -find . -type f -iname "*.py" | xargs xgettext --join-existing --no-location --omit-header --keyword='tr' -d base -o locales/base.pot - -case "${lang}" in - "all") generate_all;; - *) generate_single_lang "${lang}" +case "${1}" in + check) cmd_check ;; + all) cmd_generate_all ;; + -h|--help) usage ;; + *) cmd_generate_single "${1}" ;; esac From 5fd2f9b62786fd8641dd0460ece0fc195b957a30 Mon Sep 17 00:00:00 2001 From: Clinton Phillips Date: Mon, 18 May 2026 23:36:28 -0400 Subject: [PATCH 14/15] fix: restrict EFI partition permissions with fmask/dmask=0077 (#4506) * fix: restrict EFI partition permissions with fmask/dmask=0077 Mount the ESP with fmask=0077 and dmask=0077 to prevent world-readable files like /efi/loader/random-seed. Closes #4241 * fix(efi): collapse fmask/dmask dedup to dict.fromkeys one-liner Per @Torxed's review feedback. Same semantics as the previous loop (dedupe by exact-string match) but shorter. dict.fromkeys preserves insertion order, where set() would not. * fix(efi): drop defensive list wrap per review The list() copy on line 378 was load-bearing only if options were mutated downstream, but the EFI branch reassigns options via dict.fromkeys() (line 381) and the non-EFI branch passes through to mount() without mutating. Drop the copy. --- archinstall/lib/installer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 801eaee2..d0afefe7 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -375,7 +375,12 @@ class Installer: # it would be none if it's btrfs as the subvolumes will have the mountpoints defined if part_mod.mountpoint: target = self.target / part_mod.relative_mountpoint - mount(part_mod.dev_path, target, options=part_mod.mount_options) + options = part_mod.mount_options + + if part_mod.is_efi(): + options = list(dict.fromkeys(options + ['fmask=0077', 'dmask=0077'])) + + mount(part_mod.dev_path, target, options=options) elif part_mod.fs_type == FilesystemType.BTRFS: # Only mount BTRFS subvolumes that have mountpoints specified subvols_with_mountpoints = [sv for sv in part_mod.btrfs_subvols if sv.mountpoint is not None] From ac4bc442deb7f176751d08cbd1f22813c9bdc7fe Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Wed, 20 May 2026 01:48:03 +1000 Subject: [PATCH 15/15] Refactor logging and formatting logic (#4542) * Refactor logging and formatting * Refactor logging and formatting * Refactor logging and formatting * Updaet --- archinstall/applications/audio.py | 2 +- archinstall/applications/bluetooth.py | 2 +- archinstall/applications/firewall.py | 2 +- archinstall/applications/fonts.py | 2 +- archinstall/applications/power_management.py | 2 +- archinstall/applications/print_service.py | 2 +- archinstall/default_profiles/desktop.py | 2 +- archinstall/default_profiles/server.py | 2 +- archinstall/lib/args.py | 2 +- .../authentication/authentication_handler.py | 2 +- .../lib/authentication/authentication_menu.py | 4 +- archinstall/lib/boot.py | 2 +- archinstall/lib/command.py | 2 +- archinstall/lib/configuration.py | 3 +- archinstall/lib/crypt.py | 2 +- archinstall/lib/disk/device_handler.py | 2 +- archinstall/lib/disk/disk_menu.py | 13 +- archinstall/lib/disk/encryption_menu.py | 6 +- archinstall/lib/disk/fido.py | 2 +- archinstall/lib/disk/filesystem.py | 2 +- archinstall/lib/disk/luks.py | 2 +- archinstall/lib/disk/lvm.py | 2 +- archinstall/lib/disk/partitioning_menu.py | 4 +- archinstall/lib/disk/utils.py | 2 +- archinstall/lib/general/general_menu.py | 2 +- archinstall/lib/global_menu.py | 8 +- archinstall/lib/hardware.py | 2 +- archinstall/lib/installer.py | 2 +- archinstall/lib/locale/utils.py | 2 +- archinstall/lib/{output.py => log.py} | 198 ++---------------- archinstall/lib/menu/abstract_menu.py | 2 +- archinstall/lib/menu/menu_helper.py | 4 +- archinstall/lib/mirror/mirror_handler.py | 2 +- archinstall/lib/mirror/mirror_menu.py | 4 +- archinstall/lib/models/bootloader.py | 2 +- archinstall/lib/models/device.py | 2 +- archinstall/lib/models/mirrors.py | 2 +- archinstall/lib/models/network.py | 2 +- archinstall/lib/network/wifi_handler.py | 2 +- archinstall/lib/network/wpa_supplicant.py | 2 +- archinstall/lib/networking.py | 2 +- archinstall/lib/packages/packages.py | 2 +- archinstall/lib/packages/util.py | 2 +- archinstall/lib/pacman/pacman.py | 2 +- archinstall/lib/plugins.py | 2 +- archinstall/lib/profile/profiles_handler.py | 2 +- archinstall/lib/translationhandler.py | 2 +- archinstall/lib/utils/format.py | 148 +++++++++++++ archinstall/lib/utils/util.py | 10 +- archinstall/main.py | 2 +- archinstall/scripts/guided.py | 2 +- archinstall/scripts/minimal.py | 2 +- archinstall/scripts/only_hd.py | 2 +- archinstall/tui/components.py | 2 +- tests/test_share_log.py | 16 +- 55 files changed, 253 insertions(+), 251 deletions(-) rename archinstall/lib/{output.py => log.py} (50%) create mode 100644 archinstall/lib/utils/format.py diff --git a/archinstall/applications/audio.py b/archinstall/applications/audio.py index b749fe67..e5d79718 100644 --- a/archinstall/applications/audio.py +++ b/archinstall/applications/audio.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING from archinstall.lib.hardware import SysInfo +from archinstall.lib.log import debug from archinstall.lib.models.application import Audio, AudioConfiguration from archinstall.lib.models.users import User -from archinstall.lib.output import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/applications/bluetooth.py b/archinstall/applications/bluetooth.py index a0ffbc75..6bd4999e 100644 --- a/archinstall/applications/bluetooth.py +++ b/archinstall/applications/bluetooth.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from archinstall.lib.output import debug +from archinstall.lib.log import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/applications/firewall.py b/archinstall/applications/firewall.py index e450dbc6..dadaa05b 100644 --- a/archinstall/applications/firewall.py +++ b/archinstall/applications/firewall.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING +from archinstall.lib.log import debug from archinstall.lib.models.application import Firewall, FirewallConfiguration -from archinstall.lib.output import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/applications/fonts.py b/archinstall/applications/fonts.py index 3fa54c67..73eb326e 100644 --- a/archinstall/applications/fonts.py +++ b/archinstall/applications/fonts.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING +from archinstall.lib.log import debug from archinstall.lib.models.application import FontsConfiguration -from archinstall.lib.output import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/applications/power_management.py b/archinstall/applications/power_management.py index b9f32b05..744411f3 100644 --- a/archinstall/applications/power_management.py +++ b/archinstall/applications/power_management.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING +from archinstall.lib.log import debug from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration -from archinstall.lib.output import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/applications/print_service.py b/archinstall/applications/print_service.py index 3e4913f0..fbe28f89 100644 --- a/archinstall/applications/print_service.py +++ b/archinstall/applications/print_service.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from archinstall.lib.output import debug +from archinstall.lib.log import debug if TYPE_CHECKING: from archinstall.lib.installer import Installer diff --git a/archinstall/default_profiles/desktop.py b/archinstall/default_profiles/desktop.py index daf70831..b70f9f11 100644 --- a/archinstall/default_profiles/desktop.py +++ b/archinstall/default_profiles/desktop.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Self, override from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult +from archinstall.lib.log import info from archinstall.lib.menu.helpers import Selection -from archinstall.lib.output import info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType diff --git a/archinstall/default_profiles/server.py b/archinstall/default_profiles/server.py index bdd3d079..0e2506e0 100644 --- a/archinstall/default_profiles/server.py +++ b/archinstall/default_profiles/server.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Self, override from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult +from archinstall.lib.log import info from archinstall.lib.menu.helpers import Selection -from archinstall.lib.output import info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index 14aff6c6..e3729504 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -14,6 +14,7 @@ from urllib.request import Request, urlopen from pydantic.dataclasses import dataclass as p_dataclass from archinstall.lib.crypt import decrypt +from archinstall.lib.log import debug, error, logger, warn from archinstall.lib.menu.util import get_password from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration from archinstall.lib.models.authentication import AuthenticationConfiguration @@ -28,7 +29,6 @@ from archinstall.lib.models.packages import Repository from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User, UserSerialization -from archinstall.lib.output import debug, error, logger, warn from archinstall.lib.plugins import load_plugin from archinstall.lib.translationhandler import Language, tr, translation_handler from archinstall.lib.version import get_version diff --git a/archinstall/lib/authentication/authentication_handler.py b/archinstall/lib/authentication/authentication_handler.py index 3a3b8cd4..bae192fd 100644 --- a/archinstall/lib/authentication/authentication_handler.py +++ b/archinstall/lib/authentication/authentication_handler.py @@ -3,9 +3,9 @@ from pathlib import Path from typing import TYPE_CHECKING from archinstall.lib.command import SysCommandWorker +from archinstall.lib.log import debug, info from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.users import User -from archinstall.lib.output import debug, info from archinstall.lib.translationhandler import tr if TYPE_CHECKING: diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 5453099f..497b925b 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -6,9 +6,9 @@ from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.util import get_password from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod from archinstall.lib.models.users import Password, User -from archinstall.lib.output import FormattedOutput from archinstall.lib.translationhandler import tr from archinstall.lib.user.user_menu import select_users +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]): users: list[User] | None = item.value if users: - return FormattedOutput.as_table(users) + return as_table(users) return None def _prev_root_pwd(self, item: MenuItem) -> str | None: diff --git a/archinstall/lib/boot.py b/archinstall/lib/boot.py index b1e8c888..e8fa4c88 100644 --- a/archinstall/lib/boot.py +++ b/archinstall/lib/boot.py @@ -6,7 +6,7 @@ from typing import ClassVar, Self from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.exceptions import SysCallError -from archinstall.lib.output import error +from archinstall.lib.log import error class Boot: diff --git a/archinstall/lib/command.py b/archinstall/lib/command.py index b99854d2..b0d45754 100644 --- a/archinstall/lib/command.py +++ b/archinstall/lib/command.py @@ -11,7 +11,7 @@ from types import TracebackType from typing import Any, Self, override from archinstall.lib.exceptions import RequirementError, SysCallError -from archinstall.lib.output import debug, error, logger +from archinstall.lib.log import debug, error, logger from archinstall.lib.utils.encoding import clear_vt100_escape_codes diff --git a/archinstall/lib/configuration.py b/archinstall/lib/configuration.py index c40ef161..f0096872 100644 --- a/archinstall/lib/configuration.py +++ b/archinstall/lib/configuration.py @@ -8,11 +8,12 @@ from pydantic import TypeAdapter from archinstall.lib.args import ArchConfig, ArchConfigType from archinstall.lib.crypt import encrypt +from archinstall.lib.log import debug, logger, warn from archinstall.lib.menu.helpers import Confirmation, Selection from archinstall.lib.menu.util import get_password, prompt_dir from archinstall.lib.models.network import NetworkConfiguration -from archinstall.lib.output import as_key_value_pair, debug, logger, warn from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.format import as_key_value_pair from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType diff --git a/archinstall/lib/crypt.py b/archinstall/lib/crypt.py index 99fa62d6..b7ad4edf 100644 --- a/archinstall/lib/crypt.py +++ b/archinstall/lib/crypt.py @@ -6,7 +6,7 @@ from pathlib import Path from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives.kdf.argon2 import Argon2id -from archinstall.lib.output import debug +from archinstall.lib.log import debug libcrypt = ctypes.CDLL('libcrypt.so') diff --git a/archinstall/lib/disk/device_handler.py b/archinstall/lib/disk/device_handler.py index 2e51d3d2..360cbfd1 100644 --- a/archinstall/lib/disk/device_handler.py +++ b/archinstall/lib/disk/device_handler.py @@ -15,6 +15,7 @@ from archinstall.lib.disk.utils import ( umount, ) from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat +from archinstall.lib.log import debug, error, info, log from archinstall.lib.models.device import ( DEFAULT_ITER_TIME, BDevice, @@ -35,7 +36,6 @@ from archinstall.lib.models.device import ( _PartitionInfo, ) from archinstall.lib.models.users import Password -from archinstall.lib.output import debug, error, info, log from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT diff --git a/archinstall/lib/disk/disk_menu.py b/archinstall/lib/disk/disk_menu.py index 26aa145d..f8e8e2e3 100644 --- a/archinstall/lib/disk/disk_menu.py +++ b/archinstall/lib/disk/disk_menu.py @@ -5,6 +5,7 @@ from typing import override from archinstall.lib.disk.device_handler import device_handler from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu from archinstall.lib.disk.partitioning_menu import manual_partitioning +from archinstall.lib.log import debug from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table from archinstall.lib.menu.util import prompt_dir @@ -35,8 +36,8 @@ from archinstall.lib.models.device import ( Unit, _DeviceInfo, ) -from archinstall.lib.output import FormattedOutput, debug from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -221,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]): for mod in device_mods: # create partition table - partition_table = FormattedOutput.as_table(mod.partitions) + partition_table = as_table(mod.partitions) output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n' output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe) @@ -230,7 +231,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]): # create btrfs table btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols] for partition in btrfs_partitions: - output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n' + output_btrfs += as_table(partition.btrfs_subvols) + '\n' output = output_partition + output_btrfs return output.rstrip() @@ -246,12 +247,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]): output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg()) for vol_gp in lvm_config.vol_groups: - pv_table = FormattedOutput.as_table(vol_gp.pvs) + pv_table = as_table(vol_gp.pvs) output += '{}:\n{}'.format(tr('Physical volumes'), pv_table) output += f'\nVolume Group: {vol_gp.name}' - lvm_volumes = FormattedOutput.as_table(vol_gp.volumes) + lvm_volumes = as_table(vol_gp.volumes) output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes) return output @@ -302,7 +303,7 @@ async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | N dev = device_handler.get_device(device.path) if dev and dev.partition_infos: - return FormattedOutput.as_table(dev.partition_infos) + return as_table(dev.partition_infos) return None if preset is None: diff --git a/archinstall/lib/disk/encryption_menu.py b/archinstall/lib/disk/encryption_menu.py index f5d4f095..53aa73fe 100644 --- a/archinstall/lib/disk/encryption_menu.py +++ b/archinstall/lib/disk/encryption_menu.py @@ -17,8 +17,8 @@ from archinstall.lib.models.device import ( PartitionModification, ) from archinstall.lib.models.users import Password -from archinstall.lib.output import FormattedOutput from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -199,7 +199,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]): def _prev_partitions(self, item: MenuItem) -> str | None: if item.value: output = tr('Partitions to be encrypted') + '\n' - output += FormattedOutput.as_table(item.value) + output += as_table(item.value) return output.rstrip() return None @@ -207,7 +207,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]): def _prev_lvm_vols(self, item: MenuItem) -> str | None: if item.value: output = tr('LVM volumes to be encrypted') + '\n' - output += FormattedOutput.as_table(item.value) + output += as_table(item.value) return output.rstrip() return None diff --git a/archinstall/lib/disk/fido.py b/archinstall/lib/disk/fido.py index 0538417f..6392b55c 100644 --- a/archinstall/lib/disk/fido.py +++ b/archinstall/lib/disk/fido.py @@ -4,9 +4,9 @@ from typing import ClassVar from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.exceptions import SysCallError +from archinstall.lib.log import error, info from archinstall.lib.models.device import Fido2Device from archinstall.lib.models.users import Password -from archinstall.lib.output import error, info from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 1afa8c6e..2c51b09d 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -13,6 +13,7 @@ from archinstall.lib.disk.lvm import ( lvm_vol_reduce, ) from archinstall.lib.disk.utils import udev_sync +from archinstall.lib.log import debug, info from archinstall.lib.models.device import ( DiskEncryption, DiskLayoutConfiguration, @@ -27,7 +28,6 @@ from archinstall.lib.models.device import ( Size, Unit, ) -from archinstall.lib.output import debug, info class FilesystemHandler: diff --git a/archinstall/lib/disk/luks.py b/archinstall/lib/disk/luks.py index 275222f8..5d62abc8 100644 --- a/archinstall/lib/disk/luks.py +++ b/archinstall/lib/disk/luks.py @@ -7,9 +7,9 @@ from types import TracebackType from archinstall.lib.command import SysCommand, SysCommandWorker, run from archinstall.lib.disk.utils import get_lsblk_info, umount from archinstall.lib.exceptions import DiskError, SysCallError +from archinstall.lib.log import debug, info from archinstall.lib.models.device import DEFAULT_ITER_TIME from archinstall.lib.models.users import Password -from archinstall.lib.output import debug, info from archinstall.lib.utils.util import generate_password diff --git a/archinstall/lib/disk/lvm.py b/archinstall/lib/disk/lvm.py index 34ae9a26..c2c5b6a6 100644 --- a/archinstall/lib/disk/lvm.py +++ b/archinstall/lib/disk/lvm.py @@ -7,6 +7,7 @@ from typing import Literal, overload from archinstall.lib.command import SysCommand, SysCommandWorker from archinstall.lib.disk.utils import udev_sync from archinstall.lib.exceptions import SysCallError +from archinstall.lib.log import debug from archinstall.lib.models.device import ( LvmGroupInfo, LvmPVInfo, @@ -17,7 +18,6 @@ from archinstall.lib.models.device import ( Size, Unit, ) -from archinstall.lib.output import debug def _lvm_info( diff --git a/archinstall/lib/disk/partitioning_menu.py b/archinstall/lib/disk/partitioning_menu.py index a524cf99..b8ddd231 100644 --- a/archinstall/lib/disk/partitioning_menu.py +++ b/archinstall/lib/disk/partitioning_menu.py @@ -19,8 +19,8 @@ from archinstall.lib.models.device import ( Size, Unit, ) -from archinstall.lib.output import FormattedOutput from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]): sector_size = device_info.sector_size text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n' - free_space_table = FormattedOutput.as_table([free_space]) + free_space_table = as_table([free_space]) prompt = text + free_space_table + '\n' max_sectors = free_space.length.format_size(Unit.sectors, sector_size) diff --git a/archinstall/lib/disk/utils.py b/archinstall/lib/disk/utils.py index 808ff451..900445e1 100644 --- a/archinstall/lib/disk/utils.py +++ b/archinstall/lib/disk/utils.py @@ -4,8 +4,8 @@ from pydantic import BaseModel from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import DiskError, SysCallError +from archinstall.lib.log import debug, info, warn from archinstall.lib.models.device import LsblkInfo -from archinstall.lib.output import debug, info, warn class LsblkOutput(BaseModel): diff --git a/archinstall/lib/general/general_menu.py b/archinstall/lib/general/general_menu.py index be5498d8..dcf7d8aa 100644 --- a/archinstall/lib/general/general_menu.py +++ b/archinstall/lib/general/general_menu.py @@ -1,8 +1,8 @@ from enum import Enum from archinstall.lib.locale.utils import list_timezones +from archinstall.lib.log import warn from archinstall.lib.menu.helpers import Confirmation, Input, Selection -from archinstall.lib.output import warn from archinstall.lib.translationhandler import Language, tr from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 1e18890f..45a0e3b4 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -27,11 +27,11 @@ from archinstall.lib.models.packages import Repository from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.network.network_menu import select_network -from archinstall.lib.output import FormattedOutput from archinstall.lib.packages.packages import list_available_packages, select_additional_packages from archinstall.lib.pacman.config import PacmanConfig from archinstall.lib.pacman.pacman_menu import PacmanMenu from archinstall.lib.translationhandler import Language, tr, translation_handler +from archinstall.lib.utils.format import as_table from archinstall.tui.components import tui from archinstall.tui.menu_item import MenuItem, MenuItemGroup @@ -299,7 +299,7 @@ class GlobalMenu(AbstractMenu[None]): if item.value: network_config: NetworkConfiguration = item.value if network_config.type == NicType.MANUAL: - output = FormattedOutput.as_table(network_config.nics) + output = as_table(network_config.nics) else: output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}' @@ -321,7 +321,7 @@ class GlobalMenu(AbstractMenu[None]): output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n' if auth_config.users: - output += FormattedOutput.as_table(auth_config.users) + '\n' + output += as_table(auth_config.users) + '\n' if auth_config.u2f_config: u2f_config = auth_config.u2f_config @@ -612,7 +612,7 @@ class GlobalMenu(AbstractMenu[None]): if mirror_config.custom_repositories: title = tr('Custom repositories') - table = FormattedOutput.as_table(mirror_config.custom_repositories) + table = as_table(mirror_config.custom_repositories) output += f'{title}:\n\n{table}' return output.strip() diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 2cfdb706..f1587a3c 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -6,8 +6,8 @@ from typing import Self from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import SysCallError +from archinstall.lib.log import debug from archinstall.lib.networking import enrich_iface_types, list_interfaces -from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d0afefe7..bf7d91ba 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -29,6 +29,7 @@ from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError, from archinstall.lib.hardware import SysInfo from archinstall.lib.linux_path import LPath from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout +from archinstall.lib.log import debug, error, info, log, logger, warn from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.models.application import ZramAlgorithm from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration @@ -52,7 +53,6 @@ from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel from archinstall.lib.models.packages import Repository from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.models.users import User -from archinstall.lib.output import debug, error, info, log, logger, warn from archinstall.lib.packages.packages import installed_package from archinstall.lib.pacman.config import PacmanConfig from archinstall.lib.pacman.pacman import Pacman diff --git a/archinstall/lib/locale/utils.py b/archinstall/lib/locale/utils.py index 497e1fcb..31946028 100644 --- a/archinstall/lib/locale/utils.py +++ b/archinstall/lib/locale/utils.py @@ -3,7 +3,7 @@ from pathlib import Path from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import ServiceException, SysCallError -from archinstall.lib.output import error +from archinstall.lib.log import error from archinstall.lib.utils.util import running_from_iso diff --git a/archinstall/lib/output.py b/archinstall/lib/log.py similarity index 50% rename from archinstall/lib/output.py rename to archinstall/lib/log.py index bc9bc7a3..bd060e99 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/log.py @@ -3,183 +3,18 @@ import os import sys import urllib.error import urllib.request -from collections.abc import Callable -from dataclasses import asdict, is_dataclass -from datetime import UTC, datetime from enum import Enum 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 - - -class FormattedOutput: - @staticmethod - def _get_values( - o: DataclassInstance, - class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument] - filter_list: list[str] = [], - ) -> dict[str, Any]: - """ - the original values returned a dataclass as dict thru the call to some specific methods - this version allows thru the parameter class_formatter to call a dynamically selected formatting method. - Can transmit a filter list to the class_formatter, - """ - if class_formatter: - # if invoked per reference it has to be a standard function or a classmethod. - # A method of an instance does not make sense - if callable(class_formatter): - return class_formatter(o, filter_list) - # if is invoked by name we restrict it to a method of the class. No need to mess more - elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)): - func = getattr(o, class_formatter) - return func(filter_list) - - raise ValueError('Unsupported formatting call') - elif hasattr(o, 'table_data'): - return o.table_data() - elif hasattr(o, 'json'): - return o.json() - elif is_dataclass(o): - return asdict(o) - else: - return o.__dict__ # type: ignore[unreachable] - - @classmethod - def as_table( - cls, - obj: list[Any], - class_formatter: str | Callable | None = None, # type: ignore[type-arg] - filter_list: list[str] = [], - capitalize: bool = False, - ) -> str: - """variant of as_table (subtly different code) which has two additional parameters - filter which is a list of fields which will be shown - class_formatter a special method to format the outgoing data - - A general comment, the format selected for the output (a string where every data record is separated by newline) - is for compatibility with a print statement - As_table_filter can be a drop in replacement for as_table - """ - raw_data = [cls._get_values(o, class_formatter, filter_list) for o in obj] - - # determine the maximum column size - column_width: dict[str, int] = {} - for o in raw_data: - for k, v in o.items(): - if not filter_list or k in filter_list: - column_width.setdefault(k, 0) - column_width[k] = max([column_width[k], len(str(v)), len(k)]) - - if not filter_list: - filter_list = list(column_width.keys()) - - # create the header lines - output = '' - key_list = [] - for key in filter_list: - width = column_width[key] - key = key.replace('!', '').replace('_', ' ') - - if capitalize: - key = key.capitalize() - - key_list.append(unicode_ljust(key, width)) - - output += ' | '.join(key_list) + '\n' - output += '-' * len(output) + '\n' - - # create the data lines - for record in raw_data: - obj_data = [] - for key in filter_list: - width = column_width.get(key, len(key)) - value = record.get(key, '') - - if '!' in key: - value = '*' * len(value) - - if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()): - obj_data.append(unicode_rjust(str(value), width)) - else: - obj_data.append(unicode_ljust(str(value), width)) - - output += ' | '.join(obj_data) + '\n' - - return output - - @staticmethod - def as_columns(entries: list[str], cols: int) -> str: - """ - Will format a list into a given number of columns - """ - chunks = [] - output = '' - - for i in range(0, len(entries), cols): - chunks.append(entries[i : i + cols]) - - for row in chunks: - out_fmt = '{: <30} ' * len(row) - output += out_fmt.format(*row) + '\n' - - 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: - try: - import systemd.journal # type: ignore[import-not-found] - except ModuleNotFoundError: - return - - log_adapter = logging.getLogger('archinstall') - log_fmt = logging.Formatter('[%(levelname)s]: %(message)s') - log_ch = systemd.journal.JournalHandler() - log_ch.setFormatter(log_fmt) - log_adapter.addHandler(log_ch) - log_adapter.setLevel(logging.DEBUG) - - log_adapter.log(level, message) +from archinstall.lib.utils.util import timestamp class Logger: - def __init__(self, path: Path = Path('/var/log/archinstall')) -> None: - self._path = path + def __init__(self, path: Path | None = None) -> None: + if path is None: + path = Path('/var/log/archinstall') + + self._path: Path = path @property def path(self) -> Path: @@ -212,7 +47,7 @@ class Logger: self._check_permissions() with self.path.open('a') as f: - ts = _timestamp() + ts = timestamp() level_name = logging.getLevelName(level) f.write(f'[{ts}] - {level_name} - {content}\n') @@ -310,9 +145,20 @@ def _stylize_output( return f'\033[{ansi}m{text}\033[0m' -def _timestamp() -> str: - now = datetime.now(tz=UTC) - return now.strftime('%Y-%m-%d %H:%M:%S') +def journal_log(message: str, level: int = logging.DEBUG) -> None: + try: + import systemd.journal # type: ignore[import-not-found] + except ModuleNotFoundError: + return + + log_adapter = logging.getLogger('archinstall') + log_fmt = logging.Formatter('[%(levelname)s]: %(message)s') + log_ch = systemd.journal.JournalHandler() + log_ch.setFormatter(log_fmt) + log_adapter.addHandler(log_ch) + log_adapter.setLevel(logging.DEBUG) + + log_adapter.log(level, message) def info( @@ -376,7 +222,7 @@ def log( if _supports_color(): text = _stylize_output(text, fg, bg, reset, font) - Journald.log(text, level=level) + journal_log(text, level=level) if level != logging.DEBUG: print(text) diff --git a/archinstall/lib/menu/abstract_menu.py b/archinstall/lib/menu/abstract_menu.py index 590cabf1..c37f5657 100644 --- a/archinstall/lib/menu/abstract_menu.py +++ b/archinstall/lib/menu/abstract_menu.py @@ -2,8 +2,8 @@ from enum import Enum from types import TracebackType from typing import Any, Self, override +from archinstall.lib.log import error from archinstall.lib.menu.helpers import Selection -from archinstall.lib.output import error from archinstall.lib.translationhandler import tr from archinstall.tui.components import InstanceRunnable from archinstall.tui.menu_item import MenuItem, MenuItemGroup diff --git a/archinstall/lib/menu/menu_helper.py b/archinstall/lib/menu/menu_helper.py index 6ca3b953..0d9a4032 100644 --- a/archinstall/lib/menu/menu_helper.py +++ b/archinstall/lib/menu/menu_helper.py @@ -1,4 +1,4 @@ -from archinstall.lib.output import FormattedOutput +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup @@ -32,7 +32,7 @@ class MenuHelper[ValueT]: display_data: dict[str, ValueT | str | None] = {} if data: - table = FormattedOutput.as_table(data) + table = as_table(data) rows = table.split('\n') # these are the header rows of the table diff --git a/archinstall/lib/mirror/mirror_handler.py b/archinstall/lib/mirror/mirror_handler.py index ef0ba1a9..8117605d 100644 --- a/archinstall/lib/mirror/mirror_handler.py +++ b/archinstall/lib/mirror/mirror_handler.py @@ -2,10 +2,10 @@ import time import urllib.parse from pathlib import Path +from archinstall.lib.log import debug, info from archinstall.lib.models import MirrorRegion from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3 from archinstall.lib.networking import fetch_data_from_url -from archinstall.lib.output import debug, info from archinstall.lib.pathnames import MIRRORLIST diff --git a/archinstall/lib/mirror/mirror_menu.py b/archinstall/lib/mirror/mirror_menu.py index 275c4df8..6c5de14a 100644 --- a/archinstall/lib/mirror/mirror_menu.py +++ b/archinstall/lib/mirror/mirror_menu.py @@ -13,8 +13,8 @@ from archinstall.lib.models.mirrors import ( SignOption, ) from archinstall.lib.models.packages import Repository -from archinstall.lib.output import FormattedOutput from archinstall.lib.translationhandler import tr +from archinstall.lib.utils.format import as_table from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import ResultType @@ -281,7 +281,7 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]): return None custom_mirrors: list[CustomRepository] = item.value - output = FormattedOutput.as_table(custom_mirrors) + output = as_table(custom_mirrors) return output.strip() def _prev_custom_servers(self, item: MenuItem) -> str | None: diff --git a/archinstall/lib/models/bootloader.py b/archinstall/lib/models/bootloader.py index 0dbd4268..bd6bce7e 100644 --- a/archinstall/lib/models/bootloader.py +++ b/archinstall/lib/models/bootloader.py @@ -3,8 +3,8 @@ from dataclasses import dataclass from enum import Enum from typing import Any, Self, override +from archinstall.lib.log import warn from archinstall.lib.models.config import SubConfig -from archinstall.lib.output import warn from archinstall.lib.translationhandler import tr diff --git a/archinstall/lib/models/device.py b/archinstall/lib/models/device.py index a21f3c6f..a50f7d8e 100644 --- a/archinstall/lib/models/device.py +++ b/archinstall/lib/models/device.py @@ -12,9 +12,9 @@ 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.log import debug 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 ENC_IDENTIFIER = 'ainst' diff --git a/archinstall/lib/models/mirrors.py b/archinstall/lib/models/mirrors.py index 3ded2a9a..1d678328 100644 --- a/archinstall/lib/models/mirrors.py +++ b/archinstall/lib/models/mirrors.py @@ -9,10 +9,10 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override from pydantic import BaseModel, ValidationInfo, field_validator, model_validator +from archinstall.lib.log import debug 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: diff --git a/archinstall/lib/models/network.py b/archinstall/lib/models/network.py index 57a579c3..bcbd288a 100644 --- a/archinstall/lib/models/network.py +++ b/archinstall/lib/models/network.py @@ -3,8 +3,8 @@ from dataclasses import dataclass, field from enum import Enum from typing import NotRequired, Self, TypedDict, override +from archinstall.lib.log import debug from archinstall.lib.models.config import SubConfig -from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr diff --git a/archinstall/lib/network/wifi_handler.py b/archinstall/lib/network/wifi_handler.py index 4c858601..b5590343 100644 --- a/archinstall/lib/network/wifi_handler.py +++ b/archinstall/lib/network/wifi_handler.py @@ -5,9 +5,9 @@ from typing import assert_never, override from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import SysCallError +from archinstall.lib.log import debug from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig -from archinstall.lib.output import debug from archinstall.lib.translationhandler import tr from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui from archinstall.tui.menu_item import MenuItem, MenuItemGroup diff --git a/archinstall/lib/network/wpa_supplicant.py b/archinstall/lib/network/wpa_supplicant.py index 935e9b4d..17ed3227 100644 --- a/archinstall/lib/network/wpa_supplicant.py +++ b/archinstall/lib/network/wpa_supplicant.py @@ -1,8 +1,8 @@ from dataclasses import dataclass, field from pathlib import Path +from archinstall.lib.log import debug from archinstall.lib.models.network import WifiNetwork -from archinstall.lib.output import debug @dataclass diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index e950a697..97701e89 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -13,7 +13,7 @@ from urllib.parse import urlencode from urllib.request import urlopen from archinstall.lib.exceptions import DownloadTimeout, SysCallError -from archinstall.lib.output import debug, error, info +from archinstall.lib.log import debug, error, info from archinstall.lib.pacman.pacman import Pacman diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index a7422986..5b276f76 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -1,9 +1,9 @@ from functools import lru_cache from archinstall.lib.exceptions import SysCallError +from archinstall.lib.log import debug from archinstall.lib.menu.helpers import Loading, Notify, Selection from archinstall.lib.models.packages import AvailablePackage, LocalPackage, PackageGroup, Repository -from archinstall.lib.output import debug from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.translationhandler import tr from archinstall.tui.menu_item import MenuItem, MenuItemGroup diff --git a/archinstall/lib/packages/util.py b/archinstall/lib/packages/util.py index 335d312e..134592bb 100644 --- a/archinstall/lib/packages/util.py +++ b/archinstall/lib/packages/util.py @@ -1,6 +1,6 @@ from functools import lru_cache -from archinstall.lib.output import debug +from archinstall.lib.log import debug from archinstall.lib.packages.packages import check_package_upgrade diff --git a/archinstall/lib/pacman/pacman.py b/archinstall/lib/pacman/pacman.py index 83f8b40d..c78f6145 100644 --- a/archinstall/lib/pacman/pacman.py +++ b/archinstall/lib/pacman/pacman.py @@ -5,7 +5,7 @@ from pathlib import Path from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import RequirementError -from archinstall.lib.output import error, info, warn +from archinstall.lib.log import error, info, warn from archinstall.lib.pathnames import PACMAN_CONF from archinstall.lib.plugins import plugins from archinstall.lib.translationhandler import tr diff --git a/archinstall/lib/plugins.py b/archinstall/lib/plugins.py index 919fcf23..ac8383b1 100644 --- a/archinstall/lib/plugins.py +++ b/archinstall/lib/plugins.py @@ -7,7 +7,7 @@ import urllib.request from importlib import metadata from pathlib import Path -from archinstall.lib.output import error, info, warn +from archinstall.lib.log import error, info, warn from archinstall.lib.version import get_version plugins = {} diff --git a/archinstall/lib/profile/profiles_handler.py b/archinstall/lib/profile/profiles_handler.py index c75f95c0..d937a520 100644 --- a/archinstall/lib/profile/profiles_handler.py +++ b/archinstall/lib/profile/profiles_handler.py @@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, NotRequired, TypedDict from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile from archinstall.lib.hardware import GfxDriver, GfxPackage +from archinstall.lib.log import debug, error, info from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.networking import fetch_data_from_url -from archinstall.lib.output import debug, error, info from archinstall.lib.translationhandler import tr if TYPE_CHECKING: diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index 093ca6de..c2736f35 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -9,7 +9,7 @@ from typing import override from archinstall.lib.command import SysCommand from archinstall.lib.exceptions import SysCallError -from archinstall.lib.output import debug +from archinstall.lib.log import debug from archinstall.lib.utils.util import running_from_iso diff --git a/archinstall/lib/utils/format.py b/archinstall/lib/utils/format.py new file mode 100644 index 00000000..2ac8c377 --- /dev/null +++ b/archinstall/lib/utils/format.py @@ -0,0 +1,148 @@ +from collections.abc import Callable +from dataclasses import asdict, is_dataclass +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 + + +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() + + +def as_columns(entries: list[str], cols: int) -> str: + """ + Will format a list into a given number of columns + """ + chunks: list[list[str]] = [] + output = '' + + for i in range(0, len(entries), cols): + chunks.append(entries[i : i + cols]) + + for row in chunks: + out_fmt = '{: <30} ' * len(row) + output += out_fmt.format(*row) + '\n' + + return output + + +def _get_values( + o: DataclassInstance, + class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument] + filter_list: list[str] = [], +) -> dict[str, Any]: + """ + the original values returned a dataclass as dict thru the call to some specific methods + this version allows thru the parameter class_formatter to call a dynamically selected formatting method. + Can transmit a filter list to the class_formatter, + """ + if class_formatter: + # if invoked per reference it has to be a standard function or a classmethod. + # A method of an instance does not make sense + if callable(class_formatter): + return class_formatter(o, filter_list) + # if is invoked by name we restrict it to a method of the class. No need to mess more + elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)): + func = getattr(o, class_formatter) + return func(filter_list) + + raise ValueError('Unsupported formatting call') + elif hasattr(o, 'table_data'): + return o.table_data() + elif hasattr(o, 'json'): + return o.json() + elif is_dataclass(o): + return asdict(o) + else: + return o.__dict__ # type: ignore[unreachable] + + +def as_table( + obj: list[Any], + class_formatter: str | Callable | None = None, # type: ignore[type-arg] + filter_list: list[str] = [], + capitalize: bool = False, +) -> str: + """variant of as_table (subtly different code) which has two additional parameters + filter which is a list of fields which will be shown + class_formatter a special method to format the outgoing data + + A general comment, the format selected for the output (a string where every data record is separated by newline) + is for compatibility with a print statement + As_table_filter can be a drop in replacement for as_table + """ + raw_data = [_get_values(o, class_formatter, filter_list) for o in obj] + + # determine the maximum column size + column_width: dict[str, int] = {} + for o in raw_data: + for k, v in o.items(): + if not filter_list or k in filter_list: + column_width.setdefault(k, 0) + column_width[k] = max([column_width[k], len(str(v)), len(k)]) + + if not filter_list: + filter_list = list(column_width.keys()) + + # create the header lines + output = '' + key_list = [] + for key in filter_list: + width = column_width[key] + key = key.replace('!', '').replace('_', ' ') + + if capitalize: + key = key.capitalize() + + key_list.append(unicode_ljust(key, width)) + + output += ' | '.join(key_list) + '\n' + output += '-' * len(output) + '\n' + + # create the data lines + for record in raw_data: + obj_data = [] + for key in filter_list: + width = column_width.get(key, len(key)) + value = record.get(key, '') + + if '!' in key: + value = '*' * len(value) + + if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()): + obj_data.append(unicode_rjust(str(value), width)) + else: + obj_data.append(unicode_ljust(str(value), width)) + + output += ' | '.join(obj_data) + '\n' + + return output diff --git a/archinstall/lib/utils/util.py b/archinstall/lib/utils/util.py index 574cc8d5..f94ca18c 100644 --- a/archinstall/lib/utils/util.py +++ b/archinstall/lib/utils/util.py @@ -1,8 +1,14 @@ import secrets import string +from datetime import UTC, datetime -from archinstall.lib.output import FormattedOutput from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT +from archinstall.lib.utils.format import as_columns + + +def timestamp() -> str: + now = datetime.now(tz=UTC) + return now.strftime('%Y-%m-%d %H:%M:%S') def running_from_iso() -> bool: @@ -36,7 +42,7 @@ def format_cols(items: list[str], header: str | None = None) -> str: else: col = 4 - text += FormattedOutput.as_columns(items, col) + text += as_columns(items, col) # remove whitespaces on each row text = '\n'.join(t.strip() for t in text.split('\n')) return text diff --git a/archinstall/main.py b/archinstall/main.py index f30b01d3..e38b3d48 100644 --- a/archinstall/main.py +++ b/archinstall/main.py @@ -11,10 +11,10 @@ from pathlib import Path from archinstall.lib.args import ArchConfigHandler, SubCommand from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.hardware import SysInfo +from archinstall.lib.log import debug, error, info, logger, share_install_log, warn from archinstall.lib.menu.helpers import Confirmation from archinstall.lib.network.wifi_handler import WifiHandler from archinstall.lib.networking import ping -from archinstall.lib.output import debug, error, info, logger, share_install_log, warn from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.pacman.pacman import Pacman from archinstall.lib.translationhandler import tr, translation_handler diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index aab02cfd..6f484910 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -12,13 +12,13 @@ from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.general.general_menu import PostInstallationAction, select_post_installation from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands +from archinstall.lib.log import debug, error, info from archinstall.lib.menu.util import delayed_warning from archinstall.lib.mirror.mirror_handler import MirrorListHandler from archinstall.lib.models import Bootloader from archinstall.lib.models.device import DiskLayoutType, EncryptionType from archinstall.lib.models.users import User from archinstall.lib.network.network_handler import install_network_config -from archinstall.lib.output import debug, error, info from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.translationhandler import tr diff --git a/archinstall/scripts/minimal.py b/archinstall/scripts/minimal.py index ee4b49c7..c6eda03d 100644 --- a/archinstall/scripts/minimal.py +++ b/archinstall/scripts/minimal.py @@ -4,12 +4,12 @@ from archinstall.lib.configuration import ConfigurationOutput from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.installer import Installer +from archinstall.lib.log import debug, error, info from archinstall.lib.menu.util import delayed_warning from archinstall.lib.models import Bootloader from archinstall.lib.models.profile import ProfileConfiguration from archinstall.lib.models.users import Password, User from archinstall.lib.network.network_handler import install_network_config -from archinstall.lib.output import debug, error, info from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.translationhandler import tr from archinstall.tui.components import tui diff --git a/archinstall/scripts/only_hd.py b/archinstall/scripts/only_hd.py index 5df72965..c7349c51 100644 --- a/archinstall/scripts/only_hd.py +++ b/archinstall/scripts/only_hd.py @@ -7,8 +7,8 @@ from archinstall.lib.disk.filesystem import FilesystemHandler from archinstall.lib.disk.utils import disk_layouts from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.installer import Installer +from archinstall.lib.log import debug, error from archinstall.lib.menu.util import delayed_warning -from archinstall.lib.output import debug, error from archinstall.lib.translationhandler import tr from archinstall.tui.components import tui diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py index 9fecf3c6..efbb64f9 100644 --- a/archinstall/tui/components.py +++ b/archinstall/tui/components.py @@ -19,7 +19,7 @@ from textual.widgets.option_list import Option from textual.widgets.selection_list import Selection from textual.worker import WorkerCancelled -from archinstall.lib.output import debug +from archinstall.lib.log import debug from archinstall.lib.translationhandler import tr from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.result import Result, ResultType diff --git a/tests/test_share_log.py b/tests/test_share_log.py index a6706da8..1a19ba26 100644 --- a/tests/test_share_log.py +++ b/tests/test_share_log.py @@ -9,7 +9,7 @@ import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from archinstall.lib.output import share_install_log +from archinstall.lib.log import share_install_log urls = st.builds( '{}://{}.{}/{}'.format, @@ -55,7 +55,7 @@ def test_file_not_found( ) -> None: missing_log = tmp_path / sub_path / 'install.log' - with patch('archinstall.lib.output.logger._path', new=missing_log): + with patch('archinstall.lib.log.logger._path', new=missing_log): assert share_install_log(paste_url, max_byte) is None @@ -64,8 +64,8 @@ def test_file_not_found( def test_empty_file(log_file: Path, paste_url: str, max_byte: int | None) -> None: log_file.write_bytes(b'') - with patch('archinstall.lib.output.logger._path', new=log_file.parent): - # with patch('archinstall.lib.output.logger', _fake_logger(log_file)): + with patch('archinstall.lib.log.logger._path', new=log_file.parent): + # with patch('archinstall.lib.log.logger', _fake_logger(log_file)): assert share_install_log(paste_url, max_byte) is None @@ -76,7 +76,7 @@ def test_successful_upload(log_file: Path, resp_url: str, paste_url: str, max_by fake_response = BytesIO(resp_url.encode()) with ( - patch('archinstall.lib.output.logger._path', new=log_file.parent), + patch('archinstall.lib.log.logger._path', new=log_file.parent), patch('urllib.request.urlopen', return_value=fake_response), ): result = share_install_log(paste_url, max_byte) @@ -93,7 +93,7 @@ def test_truncation(log_file: Path, resp_url: str, paste_url: str, max_byte: int exptected_byte = len(content) if max_byte is None else max_byte with ( - patch('archinstall.lib.output.logger._path', new=log_file.parent), + patch('archinstall.lib.log.logger._path', new=log_file.parent), patch('urllib.request.urlopen', return_value=fake_response) as mock_open, ): _ = share_install_log(paste_url, max_byte) @@ -108,7 +108,7 @@ def test_network_error(log_file: Path, paste_url: str, max_byte: int | None) -> log_file.write_text('some log content') with ( - patch('archinstall.lib.output.logger._path', new=log_file.parent), + patch('archinstall.lib.log.logger._path', new=log_file.parent), patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')), ): assert share_install_log(paste_url, max_byte) is None @@ -121,7 +121,7 @@ def test_unexpected_response(log_file: Path, paste_url: str, max_byte: int | Non fake_response = BytesIO(b'ERROR: something went wrong') with ( - patch('archinstall.lib.output.logger._path', new=log_file.parent), + patch('archinstall.lib.log.logger._path', new=log_file.parent), patch('urllib.request.urlopen', return_value=fake_response), ): assert share_install_log(paste_url, max_byte) is None