Add Pacman settings submenu with Color and ParallelDownloads (#4404)

* Add Pacman settings submenu with Color and ParallelDownloads

Replace the standalone Parallel Downloads menu item with a Pacman
submenu containing ParallelDownloads (default 5) and Color (default on).
Settings are applied to both live and target system pacman.conf.
Hidden behind --advanced flag.

Backward compatible with old configs using "parallel_downloads" key.

* Skip _apply_to_live when user exits Pacman menu without changes

* Use TypedDict for PacmanConfiguration serialization

* Show Pacman menu by default, keep ParallelDownloads behind --advanced
This commit is contained in:
Softer 2026-04-13 11:37:51 +03:00 committed by GitHub
parent bf9b9cb7c1
commit 80da3f14a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 319 additions and 71 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "Оберіть завантажувач для встановлення"

View File

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

View File

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