diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index c61bc5a1..20ff37cf 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -22,6 +22,7 @@ from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import NetworkConfiguration 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 @@ -73,7 +74,7 @@ class ArchConfig: kernels: list[str] = field(default_factory=lambda: ['linux']) ntp: bool = True packages: list[str] = field(default_factory=list) - parallel_downloads: int = 0 + pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default) timezone: str = 'UTC' services: list[str] = field(default_factory=list) custom_commands: list[str] = field(default_factory=list) @@ -104,7 +105,7 @@ class ArchConfig: 'kernels': self.kernels, 'ntp': self.ntp, 'packages': self.packages, - 'parallel_downloads': self.parallel_downloads, + 'pacman_config': self.pacman_config.json(), 'swap': self.swap, 'timezone': self.timezone, 'services': self.services, @@ -209,8 +210,10 @@ class ArchConfig: if packages := args_config.get('packages', []): arch_config.packages = packages - if parallel_downloads := args_config.get('parallel_downloads', 0): - arch_config.parallel_downloads = parallel_downloads + if pacman_config := args_config.get('pacman_config', None): + arch_config.pacman_config = PacmanConfiguration.parse_arg(pacman_config) + elif parallel_downloads := args_config.get('parallel_downloads', 0): + arch_config.pacman_config = PacmanConfiguration(parallel_downloads=int(parallel_downloads)) swap_arg = args_config.get('swap') if swap_arg is not None: diff --git a/archinstall/lib/general/__init__.py b/archinstall/lib/general/__init__.py index 21add834..7c082d84 100644 --- a/archinstall/lib/general/__init__.py +++ b/archinstall/lib/general/__init__.py @@ -1,5 +1,4 @@ from archinstall.lib.general.general_menu import ( - add_number_of_parallel_downloads, select_archinstall_language, select_hostname, select_ntp, @@ -8,7 +7,6 @@ from archinstall.lib.general.general_menu import ( from archinstall.lib.general.system_menu import select_driver, select_kernel, select_swap __all__ = [ - 'add_number_of_parallel_downloads', 'select_archinstall_language', 'select_driver', 'select_hostname', diff --git a/archinstall/lib/general/general_menu.py b/archinstall/lib/general/general_menu.py index b6cf9a75..ad85c14b 100644 --- a/archinstall/lib/general/general_menu.py +++ b/archinstall/lib/general/general_menu.py @@ -3,7 +3,6 @@ from enum import Enum from archinstall.lib.locale.utils import list_timezones from archinstall.lib.menu.helpers import Confirmation, Input, Selection from archinstall.lib.output import warn -from archinstall.lib.pathnames import PACMAN_CONF from archinstall.lib.translationhandler import Language, tr from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType @@ -126,55 +125,6 @@ async def select_archinstall_language(languages: list[Language], preset: Languag raise ValueError('Language selection not handled') -async def add_number_of_parallel_downloads(preset: int = 1) -> int | None: - max_recommended = 5 - - header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n' - header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n\n' - header += tr('Enter the number of parallel downloads to be enabled') - - def validator(s: str) -> str | None: - try: - value = int(s) - - if 1 <= value <= max_recommended: - return None - - return tr('Value must be between 1 and {}').format(max_recommended) - except Exception: - return tr('Please enter a valid number') - - result = await Input( - header=header, - allow_skip=True, - allow_reset=True, - validator_callback=validator, - default_value=str(preset), - ).show() - - downloads = 1 - - match result.type_: - case ResultType.Skip: - return preset - case ResultType.Reset: - return downloads - case ResultType.Selection: - downloads = int(result.get_value()) - - with PACMAN_CONF.open() as f: - pacman_conf = f.read().split('\n') - - with PACMAN_CONF.open('w') as fwrite: - for line in pacman_conf: - if 'ParallelDownloads' in line: - fwrite.write(f'ParallelDownloads = {downloads}\n') - else: - fwrite.write(f'{line}\n') - - return downloads - - async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction: header = 'Installation completed' if elapsed_time is not None: diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index bbd530a8..6e4b6720 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -7,7 +7,7 @@ from archinstall.lib.authentication.authentication_menu import AuthenticationMen from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu from archinstall.lib.configuration import save_config from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu -from archinstall.lib.general.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone +from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone from archinstall.lib.general.system_menu import select_kernel, select_swap from archinstall.lib.hardware import SysInfo from archinstall.lib.locale.locale_menu import LocaleMenu @@ -22,11 +22,13 @@ from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import NetworkConfiguration, NicType 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.tui.ui.components import tui from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup @@ -38,11 +40,13 @@ class GlobalMenu(AbstractMenu[None]): arch_config: ArchConfig, mirror_list_handler: MirrorListHandler | None = None, skip_boot: bool = False, + advanced: bool = False, title: str | None = None, ) -> None: self._arch_config = arch_config self._mirror_list_handler = mirror_list_handler self._skip_boot = skip_boot + self._advanced = advanced self._uefi = SysInfo.has_uefi() menu_options = self._get_menu_options() @@ -138,11 +142,11 @@ class GlobalMenu(AbstractMenu[None]): key='network_config', ), MenuItem( - text=tr('Parallel Downloads'), - action=add_number_of_parallel_downloads, - value=1, - preview_action=self._prev_parallel_dw, - key='parallel_downloads', + text=tr('Pacman'), + action=self._pacman_configuration, + value=PacmanConfiguration.default(), + preview_action=self._prev_pacman_config, + key='pacman_config', ), MenuItem( text=tr('Additional packages'), @@ -413,10 +417,18 @@ class GlobalMenu(AbstractMenu[None]): return f'{tr("Hostname")}: {item.value}' return None - def _prev_parallel_dw(self, item: MenuItem) -> str | None: - if item.value is not None: - return f'{tr("Parallel Downloads")}: {item.value}' - return None + async def _pacman_configuration(self, preset: PacmanConfiguration) -> PacmanConfiguration | None: + return await PacmanMenu(preset, advanced=self._advanced).show() + + def _prev_pacman_config(self, item: MenuItem) -> str | None: + if not item.value: + return None + config: PacmanConfiguration = item.value + output = '' + if self._advanced: + output += '{}: {}\n'.format(tr('Parallel Downloads'), config.parallel_downloads) + output += '{}: {}'.format(tr('Color'), config.color) + return output def _prev_kernel(self, item: MenuItem) -> str | None: if item.value: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index ff56e2e1..43077c63 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -48,6 +48,7 @@ from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import MirrorConfiguration from archinstall.lib.models.network import Nic 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 @@ -886,6 +887,7 @@ class Installer: mkinitcpio: bool = True, hostname: str | None = None, locale_config: LocaleConfiguration | None = LocaleConfiguration.default(), + pacman_config: PacmanConfiguration | None = None, ) -> None: if self._disk_config.lvm_config: lvm = 'lvm2' @@ -932,6 +934,9 @@ class Installer: pacman_conf.persist() + if pacman_config: + pacman_conf.configure(pacman_config) + # Periodic TRIM may improve the performance and longevity of SSDs whilst # having no adverse effect on other devices. Most distributions enable # periodic TRIM by default. diff --git a/archinstall/lib/models/pacman.py b/archinstall/lib/models/pacman.py new file mode 100644 index 00000000..5d271ea9 --- /dev/null +++ b/archinstall/lib/models/pacman.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass +from typing import Self, TypedDict + +from archinstall.lib.translationhandler import tr + + +class PacmanConfigSerialization(TypedDict): + parallel_downloads: int + color: bool + + +@dataclass +class PacmanConfiguration: + parallel_downloads: int = 5 + color: bool = True + + @classmethod + def default(cls) -> Self: + return cls() + + def json(self) -> PacmanConfigSerialization: + return { + 'parallel_downloads': self.parallel_downloads, + 'color': self.color, + } + + def preview(self) -> str: + color_str = str(self.color) + output = '{}: {}\n'.format(tr('Parallel Downloads'), self.parallel_downloads) + output += '{}: {}'.format(tr('Color'), color_str) + return output + + @classmethod + def parse_arg(cls, args: PacmanConfigSerialization) -> Self: + config = cls.default() + + if 'parallel_downloads' in args: + config.parallel_downloads = int(args['parallel_downloads']) + if 'color' in args: + config.color = bool(args['color']) + + return config diff --git a/archinstall/lib/pacman/config.py b/archinstall/lib/pacman/config.py index 1d361542..5785b319 100644 --- a/archinstall/lib/pacman/config.py +++ b/archinstall/lib/pacman/config.py @@ -2,6 +2,7 @@ import re from pathlib import Path from archinstall.lib.models.packages import Repository +from archinstall.lib.models.pacman import PacmanConfiguration from archinstall.lib.pathnames import PACMAN_CONF @@ -50,5 +51,23 @@ class PacmanConfig: f.writelines(content) def persist(self) -> None: - if self._repositories and self._config_remote_path: + if self._config_remote_path: PACMAN_CONF.copy(self._config_remote_path, preserve_metadata=True) + + def configure(self, pacman_config: PacmanConfiguration) -> None: + """Apply PacmanConfiguration (Color, ParallelDownloads) to the target system's pacman.conf.""" + if not self._config_remote_path or not self._config_remote_path.exists(): + return + + content = self._config_remote_path.read_text().splitlines() + result = [] + + for line in content: + if re.match(r'^#?\s*ParallelDownloads', line): + result.append(f'ParallelDownloads = {pacman_config.parallel_downloads}') + elif re.match(r'^#?\s*Color\s*$', line): + result.append('Color' if pacman_config.color else '#Color') + else: + result.append(line) + + self._config_remote_path.write_text('\n'.join(result) + '\n') diff --git a/archinstall/lib/pacman/pacman_menu.py b/archinstall/lib/pacman/pacman_menu.py new file mode 100644 index 00000000..010c3f6d --- /dev/null +++ b/archinstall/lib/pacman/pacman_menu.py @@ -0,0 +1,117 @@ +from typing import override + +from archinstall.lib.menu.abstract_menu import AbstractSubMenu +from archinstall.lib.menu.helpers import Confirmation, Input +from archinstall.lib.models.pacman import PacmanConfiguration +from archinstall.lib.pathnames import PACMAN_CONF +from archinstall.lib.translationhandler import tr +from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup +from archinstall.tui.ui.result import ResultType + + +class PacmanMenu(AbstractSubMenu[PacmanConfiguration]): + def __init__( + self, + pacman_conf: PacmanConfiguration, + advanced: bool = False, + ): + self._pacman_conf = pacman_conf + self._advanced = advanced + menu_options = self._define_menu_options() + + self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True) + super().__init__( + self._item_group, + config=self._pacman_conf, + allow_reset=True, + ) + + def _define_menu_options(self) -> list[MenuItem]: + return [ + MenuItem( + text=tr('Parallel Downloads'), + action=select_parallel_downloads, + value=self._pacman_conf.parallel_downloads, + preview_action=lambda item: str(item.get_value()), + key='parallel_downloads', + enabled=self._advanced, + ), + MenuItem( + text=tr('Color'), + action=select_color, + value=self._pacman_conf.color, + preview_action=lambda item: str(item.get_value()), + key='color', + ), + ] + + @override + async def show(self) -> PacmanConfiguration | None: + config = await super().show() + + if config is None: + return PacmanConfiguration.default() + + _apply_to_live(config.parallel_downloads) + + return config + + +def _apply_to_live(parallel_downloads: int) -> None: + """Apply ParallelDownloads to live system pacman.conf for faster installation.""" + with PACMAN_CONF.open() as f: + pacman_conf = f.read().split('\n') + + with PACMAN_CONF.open('w') as fwrite: + for line in pacman_conf: + if 'ParallelDownloads' in line: + fwrite.write(f'ParallelDownloads = {parallel_downloads}\n') + else: + fwrite.write(f'{line}\n') + + +async def select_parallel_downloads(preset: int = 5) -> int | None: + max_recommended = 10 + + header = tr('Enter the number of parallel downloads (1-{})').format(max_recommended) + + def validator(s: str) -> str | None: + try: + value = int(s) + if 1 <= value <= max_recommended: + return None + return tr('Value must be between 1 and {}').format(max_recommended) + except Exception: + return tr('Please enter a valid number') + + result = await Input( + header=header, + allow_skip=True, + allow_reset=True, + validator_callback=validator, + default_value=str(preset), + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return 5 + case ResultType.Selection: + return int(result.get_value()) + + +async def select_color(preset: bool = True) -> bool | None: + result = await Confirmation( + header=tr('Enable colored output for pacman'), + preset=preset, + allow_skip=True, + ).show() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Reset: + return True + case ResultType.Selection: + return result.get_value() diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 27fa4eee..9d61ca77 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -830,6 +830,19 @@ msgstr "" msgid "Parallel Downloads" msgstr "" +msgid "Pacman" +msgstr "" + +msgid "Color" +msgstr "" + +#, python-brace-format +msgid "Enter the number of parallel downloads (1-{})" +msgstr "" + +msgid "Enable colored output for pacman" +msgstr "" + msgid "ESC to skip" msgstr "" @@ -2184,3 +2197,36 @@ msgstr "" #, python-brace-format msgid "Environment type: {} {}" msgstr "" + +msgid "Input cannot be empty" +msgstr "" + +msgid "Recommended" +msgstr "" + +msgid "Package" +msgstr "" + +msgid "Curated selection of KDE Plasma packages" +msgstr "" + +msgid "Dependencies" +msgstr "" + +msgid "Package group" +msgstr "" + +msgid "Extensive KDE Plasma installation" +msgstr "" + +msgid "Packages in group" +msgstr "" + +msgid "Minimal KDE Plasma installation" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Select a flavor of KDE Plasma to install" +msgstr "" diff --git a/archinstall/locales/uk/LC_MESSAGES/base.mo b/archinstall/locales/uk/LC_MESSAGES/base.mo index ad130139..1cb241b9 100644 Binary files a/archinstall/locales/uk/LC_MESSAGES/base.mo and b/archinstall/locales/uk/LC_MESSAGES/base.mo differ diff --git a/archinstall/locales/uk/LC_MESSAGES/base.po b/archinstall/locales/uk/LC_MESSAGES/base.po index 5ad4bcb3..04695709 100644 --- a/archinstall/locales/uk/LC_MESSAGES/base.po +++ b/archinstall/locales/uk/LC_MESSAGES/base.po @@ -823,6 +823,19 @@ msgstr "Некоректне введення! Повторіть спробу msgid "Parallel Downloads" msgstr "Паралельні Завантаження" +msgid "Pacman" +msgstr "Pacman" + +msgid "Color" +msgstr "Колір" + +#, python-brace-format +msgid "Enter the number of parallel downloads (1-{})" +msgstr "Введіть кількість паралельних завантажень (1-{})" + +msgid "Enable colored output for pacman" +msgstr "Увімкнути кольоровий вивід для pacman" + msgid "ESC to skip" msgstr "ESC, щоб пропустити" @@ -2119,3 +2132,46 @@ msgstr "Надійність пароля: Сильна" msgid "The selected desktop profile requires a regular user to log in via the greeter" msgstr "Обраний профіль робочого столу вимагає входу звичайного користувача через менеджер входу" + +#, fuzzy, python-brace-format +msgid "Environment type: {} {}" +msgstr "Тип середовища: {}" + +#, fuzzy +msgid "Input cannot be empty" +msgstr "Значення не може бути пустим" + +msgid "Recommended" +msgstr "" + +#, fuzzy +msgid "Package" +msgstr "Група пакетів:" + +msgid "Curated selection of KDE Plasma packages" +msgstr "" + +msgid "Dependencies" +msgstr "" + +#, fuzzy +msgid "Package group" +msgstr "Група пакетів:" + +msgid "Extensive KDE Plasma installation" +msgstr "" + +#, fuzzy +msgid "Packages in group" +msgstr "Група пакетів:" + +msgid "Minimal KDE Plasma installation" +msgstr "" + +#, fuzzy +msgid "Description" +msgstr "Шифрування диска" + +#, fuzzy +msgid "Select a flavor of KDE Plasma to install" +msgstr "Оберіть завантажувач для встановлення" diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 5683ab9e..601ca7bc 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -39,12 +39,10 @@ def show_menu( arch_config_handler.config, mirror_list_handler, arch_config_handler.args.skip_boot, + advanced=arch_config_handler.args.advanced, title=title_text, ) - if not arch_config_handler.args.advanced: - global_menu.set_enabled('parallel_downloads', False) - result: ArchConfig | None = tui.run(global_menu) if result is None: sys.exit(0) @@ -106,6 +104,7 @@ def perform_installation( mkinitcpio=run_mkinitcpio, hostname=arch_config_handler.config.hostname, locale_config=locale_config, + pacman_config=config.pacman_config, ) if mirror_config := config.mirror_config: diff --git a/tests/test_args.py b/tests/test_args.py index 1b052ec0..4cbad41b 100644 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -22,6 +22,7 @@ from archinstall.lib.models.locale import LocaleConfiguration from archinstall.lib.models.mirrors import CustomRepository, CustomServer, MirrorConfiguration, MirrorRegion, SignCheck, SignOption from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType 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 from archinstall.lib.profile.profiles_handler import profile_handler @@ -234,7 +235,7 @@ def test_config_file_parsing( kernels=['linux-zen'], ntp=True, packages=['firefox'], - parallel_downloads=66, + pacman_config=PacmanConfiguration(parallel_downloads=66), swap=ZramConfiguration(enabled=False), timezone='UTC', services=['service_1', 'service_2'],