Merge remote-tracking branch 'origin/master' into refactor-profiles

This commit is contained in:
Daniel Girtler 2026-05-04 14:31:10 +10:00
commit 94e5eee2f2
77 changed files with 4419 additions and 3659 deletions

View File

@ -1,28 +0,0 @@
#on:
# push:
# paths:
# - 'archinstall/locales/**'
# pull_request:
# paths:
# - 'archinstall/locales/**'
#name: Verify local_generate script was run on translation changes
#jobs:
# translation-check:
# runs-on: ubuntu-latest
# container:
# image: archlinux/archlinux:latest
# steps:
# - uses: actions/checkout@v4
# - run: pacman --noconfirm -Syu python git diffutils
# - name: Verify all translation scripts are up to date
# run: |
# cd ..
# cp -r archinstall archinstall_orig
# cd archinstall/archinstall/locales
# bash locales_generator.sh 1> /dev/null
# cd ../../..
# git diff \
# --quiet --no-index --name-only \
# archinstall_orig/archinstall/locales \
# archinstall/archinstall/locales \
# || (echo "Translation files have not been updated after translation, please run ./locales_generator.sh once more and commit" && exit 1)

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.11
rev: v0.15.12
hooks:
# fix unused imports and sort them
- id: ruff
@ -31,7 +31,7 @@ repos:
args: [--config=.flake8]
fail_fast: true
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.1
rev: v1.20.2
hooks:
- id: mypy
args: [

View File

@ -179,7 +179,7 @@ This can be done by installing `pacman -S arch-install-scripts util-linux` local
# losetup --partscan --show ./testimage.img
# pip install --upgrade archinstall
# python -m archinstall --script guided
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd
This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
@ -199,8 +199,8 @@ You may want to boot an ISO image in a VM to test `archinstall` in there.
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw
```
@ -209,8 +209,8 @@ HINT: For espeakup support
qemu-system-x86_64 -enable-kvm \
-machine q35,accel=kvm -device intel-iommu \
-cpu host -m 4096 -boot order=d \
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
-device intel-hda -device hda-duplex,audiodev=snd0 \
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native
@ -219,6 +219,10 @@ qemu-system-x86_64 -enable-kvm \
# FAQ
## AUR
`archinstall` will not offer or bundle AUR helpers or AUR packages due to a current consensus. This is not any individual developers decision. The reasons and discussions for this stance on the topic can be found on our mailing list thread: [(optional) AUR helper in archinstall](https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/VYOULH2GOJLFM2BXOFLWH3D754YXFPSL/).
## Keyring out-of-date
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213.

View File

@ -4,8 +4,8 @@ from archinstall.default_profiles.profile import DisplayServerType, GreeterType,
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.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -9,7 +9,7 @@ class BudgieProfile(Profile):
'Budgie',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
display_server=DisplayServerType.Wayland,
)
@property
@ -18,12 +18,12 @@ class BudgieProfile(Profile):
return [
'materia-gtk-theme',
'budgie',
'mate-terminal',
'nemo',
'konsole',
'dolphin',
'papirus-icon-theme',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.LightdmSlick
return GreeterType.Sddm

View File

@ -1,26 +0,0 @@
from typing import override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
class CutefishProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Cutefish',
ProfileType.DesktopEnv,
support_gfx_driver=True,
display_server=DisplayServerType.Xorg,
)
@property
@override
def packages(self) -> list[str]:
return [
'cutefish',
'noto-fonts',
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.Sddm

View File

@ -5,8 +5,8 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerTyp
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.packages.packages import available_package, package_group_info
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PlasmaFlavor(StrEnum):

View File

@ -2,8 +2,8 @@ from enum import Enum
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class SeatAccess(Enum):

View File

@ -4,8 +4,8 @@ from archinstall.default_profiles.profile import Profile, ProfileType, SelectRes
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.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -17,8 +17,8 @@ from archinstall.lib.models.application import (
PrintServiceConfiguration,
)
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):

View File

@ -21,6 +21,7 @@ from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguratio
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.package_types import DEFAULT_KERNEL
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
@ -29,7 +30,7 @@ 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
from archinstall.tui.ui.components import tui
from archinstall.tui.components import tui
@p_dataclass
@ -71,7 +72,7 @@ class ArchConfig:
auth_config: AuthenticationConfiguration | None = None
swap: ZramConfiguration | None = None
hostname: str = 'archlinux'
kernels: list[str] = field(default_factory=lambda: ['linux'])
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
ntp: bool = True
packages: list[str] = field(default_factory=list)
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)

View File

@ -53,7 +53,7 @@ class AuthenticationHandler:
def _add_u2f_entry(self, file: Path, entry: str) -> None:
if not file.exists():
debug(f'File does not exist: {file}')
return None
return
content = file.read_text().splitlines()
@ -81,7 +81,7 @@ class AuthenticationHandler:
install_session.pacman.strap('pam-u2f')
print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
# https://developers.yubico.com/pam-u2f/
u2f_auth_file = install_session.target / 'etc/u2f_mappings'

View File

@ -9,8 +9,8 @@ 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.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):

View File

@ -5,8 +5,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):

View File

@ -0,0 +1,86 @@
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from archinstall.lib.hardware import SysInfo
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration
class BootloaderValidationFailureKind(Enum):
LimineNonFatBoot = auto()
LimineLayout = auto()
BootloaderRequiresUefi = auto()
EfistubNonFatBoot = auto()
@dataclass(frozen=True)
class BootloaderValidationFailure:
kind: BootloaderValidationFailureKind
description: str
def validate_bootloader_layout(
bootloader_config: BootloaderConfiguration | None,
disk_config: DiskLayoutConfiguration | None,
) -> BootloaderValidationFailure | None:
"""Validate bootloader configuration against disk layout.
Returns a failure with a human-readable description if the configuration
would produce an unbootable system, or None if it is valid.
"""
if not (bootloader_config and disk_config):
return None
bootloader = bootloader_config.bootloader
if bootloader == Bootloader.NO_BOOTLOADER:
return None
if bootloader.is_uefi_only() and not SysInfo.has_uefi():
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.BootloaderRequiresUefi,
description=f'{bootloader.value} requires a UEFI system.',
)
boot_part = next(
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
None,
)
if bootloader == Bootloader.Efistub:
# The UEFI firmware reads the kernel directly from the boot partition,
# which must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.EfistubNonFatBoot,
description='Efistub does not support booting with a non-FAT boot partition.',
)
if bootloader == Bootloader.Limine:
# Limine reads its config and kernels from the boot partition, which
# must be FAT.
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
description='Limine does not support booting with a non-FAT boot partition.',
)
# When the ESP is the boot partition but mounted outside /boot and
# UKI is disabled, kernels end up on the root filesystem which
# Limine cannot access.
if not bootloader_config.uki:
efi_part = next(
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
None,
)
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
return BootloaderValidationFailure(
kind=BootloaderValidationFailureKind.LimineLayout,
description=(
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
'enable UKI or add a separate /boot partition to install Limine.'
),
)
return None

View File

@ -44,8 +44,8 @@ class SysCommandWorker:
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started: float | None = None
self.ended: float | None = None
self.started = False
self.ended = False
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
@ -117,7 +117,7 @@ class SysCommandWorker:
def is_alive(self) -> bool:
self.poll()
if self.started and self.ended is None:
if self.started and not self.ended:
return True
return False
@ -173,11 +173,11 @@ class SysCommandWorker:
self.peak(output)
self._trace_log += output
except OSError:
self.ended = time.time()
self.ended = True
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = time.time()
self.ended = True
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
@ -215,7 +215,7 @@ class SysCommandWorker:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = time.time()
self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True

View File

@ -10,10 +10,12 @@ from archinstall.lib.args import ArchConfig
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.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ConfigurationOutput:
@ -58,10 +60,77 @@ class ConfigurationOutput:
debug(' -- Chosen configuration --')
debug(self.user_config_to_json())
async def confirm_config(self) -> bool:
def as_summary(self) -> str:
"""
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]] = []
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)))
bl_config = self._config.bootloader_config
if bl_config and bl_config.bootloader != Bootloader.NO_BOOTLOADER:
rows.append((tr('Bootloader'), bl_config.bootloader.value))
kernels = self._config.kernels
if kernels:
rows.append((tr('Kernel'), ', '.join(kernels)))
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))
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)
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
header = f'{tr("The specified configuration will be applied")}. '
header += tr('Would you like to continue?') + '\n'
if show_install_warnings:
header += self._render_install_warnings()
group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda x: self.user_config_to_json())
@ -79,6 +148,22 @@ class ConfigurationOutput:
return True
def get_install_warnings(self) -> list[str]:
warnings: list[str] = []
if not isinstance(self._config.network_config, NetworkConfiguration):
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
return warnings
def _render_install_warnings(self) -> str:
warnings = self.get_install_warnings()
if not warnings:
return ''
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
def _is_valid_path(self, dest_path: Path) -> bool:
dest_path_ok = dest_path.exists() and dest_path.is_dir()
if not dest_path_ok:

View File

@ -250,7 +250,7 @@ class DeviceHandler:
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
# Force create
options.append('-F')
case FilesystemType.FAT12 | FilesystemType.FAT16 | FilesystemType.FAT32:
case _ if fs_type.is_fat():
mkfs_type = 'fat'
# Set FAT size
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))

View File

@ -37,8 +37,8 @@ from archinstall.lib.models.device import (
)
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@dataclass

View File

@ -19,8 +19,8 @@ from archinstall.lib.models.device import (
from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
@ -375,7 +375,7 @@ async def select_lvm_vols_to_encrypt(
async def select_iteration_time(preset: int | None = None) -> int | None:
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n'
header += tr('Higher values increase security but slow down boot time') + '\n'
header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n'
header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n'
def validate_iter_time(value: str) -> str | None:
try:

View File

@ -21,8 +21,8 @@ from archinstall.lib.models.device import (
)
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class FreeSpace:

View File

@ -6,7 +6,7 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import prompt_dir
from archinstall.lib.models.device import SubvolumeModification
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.result import ResultType
from archinstall.tui.result import ResultType
class SubvolumeMenu(ListManager[SubvolumeModification]):

View File

@ -4,8 +4,8 @@ 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.translationhandler import Language, tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PostInstallationAction(Enum):

View File

@ -3,29 +3,24 @@ from typing import assert_never
from archinstall.lib.hardware import GfxDriver, SysInfo
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration
from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
async def select_kernel(preset: list[str] = []) -> list[str]:
async def select_kernel(preset: list[Kernel] = []) -> list[Kernel]:
"""
Asks the user to select a kernel for system.
:return: The string as a selected kernel
:rtype: string
"""
kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
default_kernel = 'linux'
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset)
group.set_default_by_value(DEFAULT_KERNEL)
group.set_focus_by_value(DEFAULT_KERNEL)
items = [MenuItem(k, value=k) for k in kernels]
group = MenuItemGroup(items, sort_items=True)
group.set_default_by_value(default_kernel)
group.set_focus_by_value(default_kernel)
group.set_selected_by_value(preset)
result = await Selection[str](
result = await Selection[Kernel](
group,
header=tr('Select which kernel(s) to install'),
allow_skip=True,

View File

@ -5,7 +5,8 @@ from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.configuration import save_config
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import ConfigurationOutput, save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
from archinstall.lib.general.system_menu import select_kernel, select_swap
@ -17,10 +18,11 @@ from archinstall.lib.mirror.mirror_menu import MirrorMenu
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.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification
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.package_types import DEFAULT_KERNEL
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
@ -30,8 +32,8 @@ from archinstall.lib.packages.packages import list_available_packages, select_ad
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
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
class GlobalMenu(AbstractMenu[None]):
@ -102,7 +104,7 @@ class GlobalMenu(AbstractMenu[None]):
),
MenuItem(
text=tr('Kernels'),
value=['linux'],
value=[DEFAULT_KERNEL],
action=select_kernel,
preview_action=self._prev_kernel,
mandatory=True,
@ -460,8 +462,6 @@ class GlobalMenu(AbstractMenu[None]):
if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
return None
bootloader = bootloader_config.bootloader
if disk_config := self._item_group.find_by_key('disk_config').value:
for layout in disk_config.device_modifications:
if root_partition := layout.get_root_partition():
@ -486,16 +486,11 @@ class GlobalMenu(AbstractMenu[None]):
if efi_partition is None:
return 'EFI system partition (ESP) not found'
if efi_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat():
return 'ESP must be formatted as a FAT filesystem'
if bootloader == Bootloader.Limine:
if boot_partition.fs_type not in [FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32]:
return 'Limine does not support booting with a non-FAT boot partition'
elif bootloader == Bootloader.Refind:
if not self._uefi:
return 'rEFInd can only be used on UEFI systems'
if failure := validate_bootloader_layout(bootloader_config, disk_config):
return failure.description
return None
@ -509,7 +504,11 @@ class GlobalMenu(AbstractMenu[None]):
if error := self._validate_bootloader():
return tr(f'Invalid configuration: {error}')
return None
self.sync_all_to_config()
summary = ConfigurationOutput(self._arch_config).as_summary()
if summary:
return f'{tr("Ready to install")}\n\n{summary}'
return tr('Ready to install')
def _prev_profile(self, item: MenuItem) -> str | None:
profile_config: ProfileConfiguration | None = item.value

View File

@ -68,6 +68,19 @@ class GfxDriver(Enum):
case _:
return False
def is_nvidia_proprietary(self) -> bool:
"""
True for Nvidia drivers that ship proprietary userspace components.
Currently only NvidiaOpenKernel (nvidia-open-dkms): open kernel module
paired with proprietary userspace. NvidiaOpenSource (nouveau) is fully
open and works with Sway, so it is excluded.
"""
match self:
case GfxDriver.NvidiaOpenKernel:
return True
case _:
return False
def packages_text(self) -> str:
pkg_names = [p.value for p in self.gfx_packages()]
text = tr('Installed packages') + ':\n'

View File

@ -12,6 +12,7 @@ from types import TracebackType
from typing import Any, Self
from archinstall.lib.boot import Boot
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.command import SysCommand, run
from archinstall.lib.disk.fido import Fido2
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
@ -30,7 +31,7 @@ from archinstall.lib.linux_path import LPath
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models.application import ZramAlgorithm
from archinstall.lib.models.bootloader import Bootloader
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import (
DiskEncryption,
DiskLayoutConfiguration,
@ -47,6 +48,7 @@ from archinstall.lib.models.device import (
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.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
@ -59,7 +61,12 @@ from archinstall.lib.plugins import plugins
from archinstall.lib.translationhandler import tr
# Any package that the Installer() is responsible for (optional and the default ones)
__packages__ = ['base', 'sudo', 'linux-firmware', 'linux', 'linux-lts', 'linux-zen', 'linux-hardened']
# https://github.com/archlinux/archinstall/issues/4368
# mkinitcpio is listed explicitly so pacstrap installs it deterministically. Otherwise
# pacman picks the first initramfs provider from the host's pacman.conf, which on non-Arch
# hosts (EndeavourOS prefers dracut, etc.) breaks the installer's mkinitcpio() and
# _config_uki() methods that assume mkinitcpio is present in the chroot.
__packages__ = ['base', 'sudo', 'linux-firmware', 'mkinitcpio'] + [k.value for k in Kernel]
# Additional packages that are installed if the user is running the Live ISO with accessibility tools enabled
__accessibility_packages__ = ['brltty', 'espeakup', 'alsa-utils']
@ -78,8 +85,8 @@ class Installer:
`Installer()` is the wrapper for most basic installation steps.
It also wraps :py:func:`~archinstall.Installer.pacstrap` among other things.
"""
self._base_packages = base_packages or __packages__[:3]
self.kernels = kernels or ['linux']
self._base_packages = base_packages or __packages__[:4]
self.kernels = kernels or [DEFAULT_KERNEL.value]
self._disk_config = disk_config
self._disk_encryption = disk_config.disk_encryption or DiskEncryption(EncryptionType.NO_ENCRYPTION)
@ -182,10 +189,10 @@ class Installer:
if not skip_ntp:
info(tr('Waiting for time sync (timedatectl show) to complete.'))
started_wait = time.time()
started_wait = time.monotonic()
notified = False
while True:
if not notified and time.time() - started_wait > 5:
if not notified and time.monotonic() - started_wait > 5:
notified = True
warn(tr('Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/'))
@ -395,7 +402,7 @@ class Installer:
def _mount_luks_partition(self, part_mod: PartitionModification, luks_handler: Luks2) -> None:
if not luks_handler.mapper_dev:
return None
return
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.btrfs_subvols:
# Only mount BTRFS subvolumes that have mountpoints specified
@ -738,6 +745,9 @@ class Installer:
return self.run_command(cmd, peek_output=peek_output)
def _chroot_argv(self, *args: str) -> list[str]:
return ['arch-chroot', '-S', str(self.target), *args]
def drop_to_shell(self) -> None:
subprocess.check_call(f'arch-chroot {self.target}', shell=True)
@ -986,17 +996,7 @@ class Installer:
}
for config_name, mountpoint in snapper.items():
command = [
'arch-chroot',
'-S',
str(self.target),
'snapper',
'--no-dbus',
'-c',
config_name,
'create-config',
mountpoint,
]
command = self._chroot_argv('snapper', '--no-dbus', '-c', config_name, 'create-config', mountpoint)
try:
SysCommand(command, peek_output=True)
@ -1335,13 +1335,7 @@ class Installer:
boot_dir = Path('/boot')
command = [
'arch-chroot',
'-S',
str(self.target),
'grub-install',
'--debug',
]
command = self._chroot_argv('grub-install', '--debug')
if SysInfo.has_uefi():
if not efi_partition:
@ -1466,6 +1460,14 @@ class Installer:
elif not efi_partition.mountpoint:
raise ValueError('EFI partition is not mounted')
# Safety net for programmatic callers that bypass GlobalMenu and
# guided.py validation.
if failure := validate_bootloader_layout(
BootloaderConfiguration(bootloader=Bootloader.Limine, uki=uki_enabled),
self._disk_config,
):
raise DiskError(failure.description)
info(f'Limine EFI partition: {efi_partition.dev_path}')
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
@ -1476,15 +1478,11 @@ class Installer:
if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT'
boot_limine_path = self.target / 'boot' / 'limine'
boot_limine_path.mkdir(parents=True, exist_ok=True)
config_path = boot_limine_path / 'limine.conf'
else:
efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf'
config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True)
@ -1917,16 +1915,17 @@ class Installer:
if not handled_by_plugin:
info(f'Creating user {user.username}')
cmd = 'useradd -m'
cmd = self._chroot_argv('useradd', '-m')
if user.sudo:
cmd += ' -G wheel'
cmd += ['-G', 'wheel']
cmd += f' {user.username}'
cmd += ['--', user.username]
try:
self.arch_chroot(cmd)
except SysCallError as err:
run(cmd)
except CalledProcessError as err:
debug(f'Error creating user {user.username}: {err}')
raise SystemError(f'Could not create user inside installation: {err}')
for plugin in plugins.values():
@ -1937,7 +1936,11 @@ class Installer:
self.set_user_password(user)
for group in user.groups:
self.arch_chroot(f'gpasswd -a {user.username} {group}')
cmd = self._chroot_argv('gpasswd', '-a', user.username, group)
try:
run(cmd)
except CalledProcessError as err:
warn(f'Failed to add {user.username} to group {group}: {err}')
if user.sudo:
self.enable_sudo(user)
@ -1952,7 +1955,7 @@ class Installer:
return False
input_data = f'{user.username}:{enc_password}'.encode()
cmd = ['arch-chroot', '-S', str(self.target), 'chpasswd', '--encrypted']
cmd = self._chroot_argv('chpasswd', '--encrypted')
try:
run(cmd, input_data=input_data)
@ -1964,7 +1967,7 @@ class Installer:
def user_set_shell(self, user: str, shell: str) -> bool:
info(f'Setting shell for {user} to {shell}')
cmd = ['arch-chroot', '-S', str(self.target), 'chsh', '-s', shell, user]
cmd = self._chroot_argv('chsh', '-s', shell, user)
try:
run(cmd)
return True
@ -1974,7 +1977,7 @@ class Installer:
def chown(self, owner: str, path: str, options: list[str] | None = None) -> bool:
options = options or []
cmd = ['arch-chroot', '-S', str(self.target), 'chown', *options, owner, path]
cmd = self._chroot_argv('chown', *options, '--', owner, path)
try:
run(cmd)
return True
@ -1985,12 +1988,10 @@ class Installer:
def set_vconsole(self, locale_config: LocaleConfiguration) -> None:
# use the already set kb layout
kb_vconsole: str = locale_config.kb_layout
# this is the default used in ISO other option for hdpi screens TER16x32
# can be checked using
# zgrep "CONFIG_FONT" /proc/config.gz
# https://wiki.archlinux.org/title/Linux_console#Fonts
font_vconsole = locale_config.console_font
font_vconsole = 'default8x16'
if font_vconsole.startswith('ter-'):
self.pacman.strap(['terminus-font'])
# Ensure /etc exists
vconsole_dir: Path = self.target / 'etc'

View File

@ -1,12 +1,12 @@
from typing import override
from archinstall.lib.locale.utils import list_keyboard_languages, list_locales, set_kb_layout
from archinstall.lib.locale.utils import list_console_fonts, list_keyboard_languages, list_locales, set_kb_layout
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
@ -47,6 +47,13 @@ class LocaleMenu(AbstractSubMenu[LocaleConfiguration]):
preview_action=lambda item: item.get_value(),
key='sys_enc',
),
MenuItem(
text=tr('Console font'),
action=select_console_font,
value=self._locale_conf.console_font,
preview_action=lambda item: item.get_value(),
key='console_font',
),
]
@override
@ -140,3 +147,25 @@ async def select_kb_layout(preset: str | None = None) -> str | None:
return preset
case _:
raise ValueError('Unhandled return type')
async def select_console_font(preset: str | None = None) -> str | None:
fonts = list_console_fonts()
items = [MenuItem(f, value=f) for f in fonts]
group = MenuItemGroup(items, sort_items=False)
group.set_focus_by_value(preset)
result = await Selection[str](
header=tr('Console font'),
group=group,
enable_filter=True,
).show()
match result.type_:
case ResultType.Selection:
return result.get_value()
case ResultType.Skip:
return preset
case _:
raise ValueError('Unhandled return type')

View File

@ -1,3 +1,6 @@
from functools import lru_cache
from pathlib import Path
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import ServiceException, SysCallError
from archinstall.lib.output import error
@ -26,6 +29,13 @@ def list_locales() -> list[str]:
return locales
@lru_cache
def list_console_fonts() -> list[str]:
directory = Path('/usr/share/kbd/consolefonts')
fonts = {path.name.split('.')[0] for path in directory.glob('*.gz')}
return sorted(fonts)
def list_x11_keyboard_languages() -> list[str]:
return (
SysCommand(

View File

@ -5,10 +5,9 @@ from typing import Any, Self, override
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import error
from archinstall.lib.translationhandler import tr
from archinstall.tui.types import Chars
from archinstall.tui.ui.components import InstanceRunnable
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.components import InstanceRunnable
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
CONFIG_KEY = '__config__'
@ -154,7 +153,7 @@ class AbstractSubMenu[ValueT](AbstractMenu[ValueT]):
auto_cursor: bool = True,
allow_reset: bool = False,
):
back_text = f'{Chars.Right_arrow} ' + tr('Back')
back_text = ' ' + tr('Back')
item_group.add_item(MenuItem(text=back_text))
super().__init__(

View File

@ -4,9 +4,9 @@ from typing import Any, Literal, override
from textual.validation import ValidationResult, Validator
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
from archinstall.tui.ui.menu_item import MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType
from archinstall.tui.components import InputInfo, InputScreen, LoadingScreen, NotifyScreen, OptionListScreen, SelectListScreen, TableSelectionScreen
from archinstall.tui.menu_item import MenuItemGroup
from archinstall.tui.result import Result, ResultType
class Selection[ValueT]:

View File

@ -4,8 +4,8 @@ from typing import cast
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.menu.menu_helper import MenuHelper
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ListManager[ValueT]:

View File

@ -1,5 +1,5 @@
from archinstall.lib.output import FormattedOutput
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
class MenuHelper[ValueT]:

View File

@ -5,8 +5,8 @@ from pathlib import Path
from archinstall.lib.menu.helpers import Confirmation, Input
from archinstall.lib.models.users import Password, PasswordStrength
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.components import InputInfo, InputInfoType, tui
from archinstall.tui.ui.result import ResultType
from archinstall.tui.components import InputInfo, InputInfoType, tui
from archinstall.tui.result import ResultType
async def get_password(

View File

@ -15,8 +15,8 @@ from archinstall.lib.models.mirrors import (
from archinstall.lib.models.packages import Repository
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class CustomMirrorRepositoriesList(ListManager[CustomRepository]):

View File

@ -25,6 +25,13 @@ class Bootloader(Enum):
case _:
return False
def is_uefi_only(self) -> bool:
match self:
case Bootloader.Systemd | Bootloader.Efistub | Bootloader.Refind:
return True
case _:
return False
def json(self) -> str:
return self.value

View File

@ -802,6 +802,9 @@ class FilesystemType(StrEnum):
def is_crypto(self) -> bool:
return self == FilesystemType.CRYPTO_LUKS
def is_fat(self) -> bool:
return self in (FilesystemType.FAT12, FilesystemType.FAT16, FilesystemType.FAT32)
@property
def parted_value(self) -> str:
return self.value + '(v1)' if self == FilesystemType.LINUX_SWAP else self.value

View File

@ -10,6 +10,11 @@ class LocaleConfiguration:
kb_layout: str
sys_lang: str
sys_enc: str
# this is the default used in ISO other option for hdpi screens TER16x32
# can be checked using
# zgrep "CONFIG_FONT" /proc/config.gz
# https://wiki.archlinux.org/title/Linux_console#Font
console_font: str = 'default8x16'
@classmethod
def default(cls) -> Self:
@ -23,12 +28,14 @@ class LocaleConfiguration:
'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang,
'sys_enc': self.sys_enc,
'console_font': 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)
output += '{}: {}'.format(tr('Locale encoding'), self.sys_enc)
output += '{}: {}\n'.format(tr('Locale encoding'), self.sys_enc)
output += '{}: {}'.format(tr('Console font'), self.console_font)
return output
def _load_config(self, args: dict[str, str]) -> None:
@ -38,6 +45,8 @@ class LocaleConfiguration:
self.sys_enc = args['sys_enc']
if 'kb_layout' in args:
self.kb_layout = args['kb_layout']
if 'console_font' in args:
self.console_font = args['console_font']
@classmethod
def parse_arg(cls, args: dict[str, Any]) -> Self:

View File

@ -0,0 +1,12 @@
from enum import StrEnum, auto
from typing import Final
class Kernel(StrEnum):
LINUX = auto()
LINUX_LTS = 'linux-lts'
LINUX_ZEN = 'linux-zen'
LINUX_HARDENED = 'linux-hardened'
DEFAULT_KERNEL: Final = Kernel.LINUX

View File

@ -36,67 +36,66 @@ class PasswordStrength(Enum):
case PasswordStrength.STRONG:
return 'green'
@classmethod
def strength(cls, password: str) -> Self:
@staticmethod
def strength(password: str) -> PasswordStrength:
digit = any(character.isdigit() for character in password)
upper = any(character.isupper() for character in password)
lower = any(character.islower() for character in password)
symbol = any(not character.isalnum() for character in password)
return cls._check_password_strength(digit, upper, lower, symbol, len(password))
return PasswordStrength._check_password_strength(digit, upper, lower, symbol, len(password))
@classmethod
@staticmethod
def _check_password_strength(
cls,
digit: bool,
upper: bool,
lower: bool,
symbol: bool,
length: int,
) -> Self:
) -> PasswordStrength:
# suggested evaluation
# https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163
if digit and upper and lower and symbol:
match length:
case num if 13 <= num:
return cls.STRONG
return PasswordStrength.STRONG
case num if 11 <= num <= 12:
return cls.MODERATE
return PasswordStrength.MODERATE
case num if 7 <= num <= 10:
return cls.WEAK
return PasswordStrength.WEAK
case num if num <= 6:
return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK
elif digit and upper and lower:
match length:
case num if 14 <= num:
return cls.STRONG
return PasswordStrength.STRONG
case num if 11 <= num <= 13:
return cls.MODERATE
return PasswordStrength.MODERATE
case num if 7 <= num <= 10:
return cls.WEAK
return PasswordStrength.WEAK
case num if num <= 6:
return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK
elif upper and lower:
match length:
case num if 15 <= num:
return cls.STRONG
return PasswordStrength.STRONG
case num if 12 <= num <= 14:
return cls.MODERATE
return PasswordStrength.MODERATE
case num if 7 <= num <= 11:
return cls.WEAK
return PasswordStrength.WEAK
case num if num <= 6:
return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK
elif lower or upper:
match length:
case num if 18 <= num:
return cls.STRONG
return PasswordStrength.STRONG
case num if 14 <= num <= 17:
return cls.MODERATE
return PasswordStrength.MODERATE
case num if 9 <= num <= 13:
return cls.WEAK
return PasswordStrength.WEAK
case num if num <= 8:
return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK
return cls.VERY_WEAK
return PasswordStrength.VERY_WEAK
UserSerialization = TypedDict(

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.models.network import NetworkConfiguration, Nic, NicType
from archinstall.lib.networking import list_interfaces
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ManualNetworkConfig(ListManager[Nic]):
@ -172,14 +172,17 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
"""
items = [MenuItem(n.display_msg(), value=n) for n in NicType]
group = MenuItemGroup(items, sort_items=True)
group = MenuItemGroup(items, sort_items=False)
if preset:
group.set_selected_by_value(preset.type)
header = tr('Choose network configuration') + '\n'
header += tr('Recommended: Network Manager for desktop, Manual for server') + '\n'
result = await Selection[NicType](
group,
header=tr('Choose network configuration'),
header=header,
allow_reset=True,
allow_skip=True,
).show()

View File

@ -9,9 +9,9 @@ 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.ui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType
from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import Result, ResultType
@dataclass

View File

@ -46,12 +46,12 @@ class DownloadTimer:
self.previous_handler = signal.signal(signal.SIGALRM, self.raise_timeout) # type: ignore[assignment]
self.previous_timer = signal.alarm(self.timeout)
self.start_time = time.time()
self.start_time = time.monotonic()
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
if self.start_time:
time_delta = time.time() - self.start_time
time_delta = time.monotonic() - self.start_time
signal.alarm(0)
self.time = time_delta
if self.timeout > 0:
@ -165,7 +165,7 @@ def build_icmp(payload: bytes) -> bytes:
def ping(hostname: str, timeout: int = 5) -> int:
watchdog = select.epoll()
started = time.time()
started = time.monotonic()
random_identifier = f'archinstall-{random.randint(1000, 9999)}'.encode()
# Create a raw socket (requires root, which should be fine on archiso)
@ -180,7 +180,7 @@ def ping(hostname: str, timeout: int = 5) -> int:
# Gracefully wait for X amount of time
# for a ICMP response or exit with no latency
while latency == -1 and time.time() - started < timeout:
while latency == -1 and time.monotonic() - started < timeout:
try:
for _fileno, _event in watchdog.poll(0.1):
response, _ = icmp_socket.recvfrom(1024)
@ -188,7 +188,7 @@ def ping(hostname: str, timeout: int = 5) -> int:
# Check if it's an Echo Reply (ICMP type 0)
if icmp_type == 0 and response[-len(random_identifier) :] == random_identifier:
latency = round((time.time() - started) * 1000)
latency = round((time.monotonic() - started) * 1000)
break
except OSError as e:
debug(f'Error: {e}')

View File

@ -133,7 +133,7 @@ class Journald:
try:
import systemd.journal # type: ignore[import-not-found]
except ModuleNotFoundError:
return None
return
log_adapter = logging.getLogger('archinstall')
log_fmt = logging.Formatter('[%(levelname)s]: %(message)s')

View File

@ -6,8 +6,8 @@ from archinstall.lib.models.packages import AvailablePackage, LocalPackage, Pack
from archinstall.lib.output import debug
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
def installed_package(package: str) -> LocalPackage | None:

View File

@ -29,11 +29,11 @@ class Pacman:
if pacman_db_lock.exists():
warn(tr('Pacman is already running, waiting maximum 10 minutes for it to terminate.'))
started = time.time()
started = time.monotonic()
while pacman_db_lock.exists():
time.sleep(0.25)
if time.time() - started > (60 * 10):
if time.monotonic() - started > (60 * 10):
error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.'))
sys.exit(1)

View File

@ -5,8 +5,8 @@ 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
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class PacmanMenu(AbstractSubMenu[PacmanConfiguration]):

View File

@ -7,8 +7,8 @@ from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
@ -95,7 +95,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
driver = await select_driver(preset=preset)
if driver and 'Sway' in profile.current_selection_names():
if driver.is_nvidia():
if driver.is_nvidia_proprietary():
header = tr('The proprietary Nvidia driver is not supported by Sway.') + '\n'
header += tr('It is likely that you will run into issues, are you okay with that?') + '\n'
@ -105,8 +105,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
preset=False,
).show()
if result.get_value():
return preset
return driver if result.get_value() else preset
return driver
@ -114,7 +113,7 @@ class ProfileMenu(AbstractSubMenu[ProfileConfiguration]):
if item.value:
driver = item.get_value().value
packages = item.get_value().packages_text()
return f'Driver: {driver}\n{packages}'
return f'{tr("Graphics driver")}: {driver}\n{packages}'
return None
def _prev_greeter(self, item: MenuItem) -> str | None:

View File

@ -6,8 +6,8 @@ from archinstall.lib.menu.list_manager import ListManager
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.users import User
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem
from archinstall.tui.ui.result import ResultType
from archinstall.tui.menu_item import MenuItem
from archinstall.tui.result import ResultType
class UserList(ListManager[User]):

View File

@ -252,6 +252,9 @@ msgstr ""
msgid "Locale encoding"
msgstr ""
msgid "Console font"
msgstr ""
msgid "Drive(s)"
msgstr ""
@ -1245,6 +1248,21 @@ msgstr ""
msgid "Invalid configuration: {error}"
msgstr ""
msgid "Ready to install"
msgstr ""
msgid "Disks"
msgstr ""
msgid "Packages"
msgstr ""
msgid "Network"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Type"
msgstr ""
@ -2174,6 +2192,14 @@ msgstr ""
msgid "Choose network configuration"
msgstr ""
msgid "Recommended: Network Manager for desktop, Manual for server"
msgstr ""
msgid ""
"Warning: no network configuration selected. Network will need to be set up "
"manually on the installed system."
msgstr ""
msgid "No packages found"
msgstr ""
@ -2251,3 +2277,19 @@ msgstr ""
msgid "wide Unicode coverage, good fallback font"
msgstr ""
#, python-brace-format
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
msgstr ""
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr ""
#, python-brace-format
msgid "Setting up U2F login: {}"
msgstr ""
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,7 @@ msgid "Only packages such as base, base-devel, linux, linux-firmware, efibootmgr
msgstr "Solo paquetes como base, base-devel, linux, linux-firmware, efibootmgr y paquetes opcionales de perfil se instalan."
msgid "Note: base-devel is no longer installed by default. Add it here if you need build tools."
msgstr "Nota: base-devel ya no viene instalado por defecto. Agreguelo aquí si necesita herramientas de build."
msgstr "Nota: base-devel ya no viene instalado por defecto. Agreguelo aquí si necesita herramientas de compilación."
msgid "If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt."
msgstr "Si desea un navegador web, como firefox o chromium, puede especificarlo en el siguiente mensaje."
@ -173,7 +173,7 @@ msgid "Enter a desired filesystem type for the partition: "
msgstr "Ingrese un tipo de sistema de archivos deseado para la partición: "
msgid "Archinstall language"
msgstr "Idioma de Archinstall"
msgstr "Idioma de archinstall"
msgid "Wipe all selected drives and use a best-effort default partition layout"
msgstr "Borrar todas las unidades seleccionadas y usar un diseño de partición predeterminado de mejor esfuerzo"
@ -197,7 +197,7 @@ msgid "Select one or more hard drives to use and configure"
msgstr "Seleccione uno o más discos duros para usar y configurar"
msgid "For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options."
msgstr "Para obtener la mejor compatibilidad con su hardware AMD, es posible que desee utilizar las opciones de código abierto o AMD / ATI."
msgstr "Para obtener la mejor compatibilidad con su hardware AMD, es posible que desee utilizar las opciones de código abierto o AMD/ATI."
msgid "For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n"
msgstr "Para obtener la mejor compatibilidad con su hardware Intel, es posible que desee utilizar las opciones de código abierto o de Intel.\n"
@ -218,7 +218,7 @@ msgid "All open-source (default)"
msgstr "Todos de código abierto (predeterminado)"
msgid "Choose which kernels to use or leave blank for default \"{}\""
msgstr "Elija qué kernel usar o déjelo en blanco para usar el kernel \"{}\" predeterminado"
msgstr "Elija qué núcleo usar o deje en blanco para usar el núcleo \"{}\" predeterminado"
msgid "Choose which locale language to use"
msgstr "Elija qué idioma local usar"
@ -382,7 +382,7 @@ msgid "Create a required super-user with sudo privileges: "
msgstr "Crear un super-usuario requerido con privilegios sudo: "
msgid "Enter root password (leave blank to disable root): "
msgstr "Ingrese la contraseña de root (deje en blanco para deshabilitar root): "
msgstr "Ingrese la contraseña de root (deje en blanco para desactivar root): "
msgid "Password for user \"{}\": "
msgstr "Contraseña para el usuario “{}”: "
@ -454,7 +454,7 @@ msgid "Pre-existing pacman lock never exited. Please clean up any existing pacma
msgstr "El bloqueo de pacman preexistente nunca se cerró. Limpie cualquier sesión de pacman existente antes de usar archinstall."
msgid "Choose which optional additional repositories to enable"
msgstr "Elija qué repositorios adicionales opcionales habilitar"
msgstr "Elija qué repositorios adicionales opcionales activar"
msgid "Add a user"
msgstr "Añadir un usuario"
@ -489,7 +489,7 @@ msgid "No network configuration"
msgstr "Sin configuración de red"
msgid "Set desired subvolumes on a btrfs partition"
msgstr "Establecer los subvolúmenes deseados en una partición btrfs"
msgstr "Establecer los subvolúmenes deseados en una partición BTRFS"
msgid ""
"{}\n"
@ -501,7 +501,7 @@ msgstr ""
"Seleccione en qué partición configurar los subvolúmenes"
msgid "Manage btrfs subvolumes for current partition"
msgstr "Administrar subvolúmenes btrfs para la partición actual"
msgstr "Administrar subvolúmenes BTRFS para la partición actual"
msgid "No configuration"
msgstr "Sin configuración"
@ -650,7 +650,7 @@ msgid "Manual configuration"
msgstr "Configuración manual"
msgid "Mark/Unmark a partition as compressed (btrfs only)"
msgstr "Marcar/Desmarcar una partición como comprimida (solo btrfs)"
msgstr "Marcar/Desmarcar una partición como comprimida (solo BTRFS)"
msgid "The password you are using seems to be weak, are you sure you want to use it?"
msgstr "La contraseña que está utilizando parece ser débil, ¿está seguro de que desea usarla?"
@ -665,7 +665,7 @@ msgid "A very basic installation that allows you to customize Arch Linux as you
msgstr "Una instalación muy básica que te permite personalizar Arch Linux como mejor te parezca."
msgid "Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb"
msgstr "Proporciona una selección de varios paquetes de servidor para instalar y habilitar, p.e. httpd, nginx, mariadb"
msgstr "Proporciona una selección de varios paquetes de servidor para instalar y activar, p.ej. httpd, nginx, mariadb"
msgid "Choose which servers to install, if none then a minimal installation will be done"
msgstr "Elija qué servidores instalar, si no hay ninguno, se realizará una instalación mínima"
@ -799,45 +799,45 @@ msgid "Configured {} interfaces"
msgstr "Interfaces {} configuradas"
msgid "This option enables the number of parallel downloads that can occur during installation"
msgstr "Esta opción habilita la cantidad de descargas paralelas que pueden ocurrir durante la instalación"
msgstr "Esta opción activa la cantidad de descargas paralelas que pueden ocurrir durante la instalación"
msgid ""
"Enter the number of parallel downloads to be enabled.\n"
" (Enter a value between 1 to {})\n"
"Note:"
msgstr ""
"Ingrese el número de descargas paralelas que se habilitarán.\n"
"Ingrese el número de descargas paralelas que se activarán.\n"
" (Ingrese un valor entre 1 y {})\n"
"Nota:"
msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {} downloads at a time )"
msgstr " - Valor máximo : {} ( Habilita {} descargas paralelas, permite {} descargas simultáneas )"
msgstr " - Valor máximo : {} (Permitir {} descargas paralelas, permite {} descargas simultáneas)"
msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads at a time )"
msgstr " - Valor mínimo : 1 ( Habilita 1 descarga paralela, permite 2 descargas simultáneas )"
msgstr " - Valor mínimo : 1 (Permitir 1 descarga paralela, permite 2 descargas simultáneas)"
msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )"
msgstr " - Deshabilitar/Predeterminado : 0 ( Deshabilita la descarga paralela, permite solo 1 descarga simultánea )"
msgstr " - Desactivar/Predeterminado : 0 (Desactiva la descarga paralela, permite solo 1 descarga simultánea)"
#, python-brace-format
msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [1 a {max_downloads}, o 0 para deshabilitar]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [1 a {max_downloads}, o 0 para desactivar]"
msgid "Parallel Downloads"
msgstr "Descargas paralelas"
msgid "Pacman"
msgstr ""
msgstr "Pacman"
msgid "Color"
msgstr ""
msgstr "Color"
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Enter the number of parallel downloads (1-{})"
msgstr "Ingrese el número de descargas paralelas que desea habilitar"
msgstr "Ingrese el número de descargas paralelas (1-{})"
msgid "Enable colored output for pacman"
msgstr ""
msgstr "Activa salida a color para pacman"
msgid "ESC to skip"
msgstr "ESC para omitir"
@ -975,7 +975,7 @@ msgid "Higher values increase security but slow down boot time"
msgstr "Valores más grandes mejoran la seguridad pero aumentan el tiempo de arranque"
msgid "Default: 10000ms, Recommended range: 1000-60000"
msgstr "Por defecto: 10000ms, Rango recomendado 1000-60000"
msgstr "Predeterminado: 10000ms, Rango recomendado 1000-60000"
msgid "Iteration time cannot be empty"
msgstr "Tiempo de iteración no puede estar vacío"
@ -1073,10 +1073,10 @@ msgid "Packages to be install with this profile (space separated, leave blank to
msgstr "Paquetes que se instalarán con este perfil (separados por espacios, déjelo en blanco para omitir): "
msgid "Services to be enabled with this profile (space separated, leave blank to skip): "
msgstr "Servicios que se habilitarán con este perfil (separados por espacios, deje en blanco para omitir): "
msgstr "Servicios que se activarán con este perfil (separados por espacios, deje en blanco para omitir): "
msgid "Should this profile be enabled for installation?"
msgstr "¿Debería habilitarse este perfil para la instalación?"
msgstr "¿Debería activarse este perfil para la instalación?"
msgid "Create your own"
msgstr "Crear tu propio"
@ -1158,7 +1158,7 @@ msgid ""
"Enter a directory for the configuration(s) to be saved (tab completion enabled)\n"
"Save directory: "
msgstr ""
"Ingrese un directorio para guardar las configuraciones (completar con tabulación habilitado)\n"
"Ingrese un directorio para guardar las configuraciones (completar con tabulación activado)\n"
"Guardar directorio: "
msgid ""
@ -1180,10 +1180,10 @@ msgid "Mirror regions"
msgstr "Regiones de espejos"
msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )"
msgstr " - Valor máximo : {} ( Habilita {} descargas paralelas, permite {max_downloads+1} descargas simultáneas )"
msgstr " - Valor máximo : {} (Permitir {} descargas paralelas, permite {max_downloads+1} descargas simultáneas)"
msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [1 a {}, o 0 para deshabilitar]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [1 a {}, o 0 para desactivar]"
msgid "Locales"
msgstr "Localidades"
@ -1226,26 +1226,26 @@ msgid "Type"
msgstr "Tipo"
msgid "This option enables the number of parallel downloads that can occur during package downloads"
msgstr "Esta opción habilita la cantidad de descargas paralelas que pueden ocurrir durante las descargas de paquetes"
msgstr "Esta opción activa la cantidad de descargas paralelas que pueden ocurrir durante las descargas de paquetes"
msgid ""
"Enter the number of parallel downloads to be enabled.\n"
"\n"
"Note:\n"
msgstr ""
"Ingrese el número de descargas paralelas que se habilitarán.\n"
"Ingrese el número de descargas paralelas que se activarán.\n"
"\n"
"Nota:\n"
#, python-brace-format
msgid " - Maximum recommended value : {} ( Allows {} parallel downloads at a time )"
msgstr " - Valor máximo recomendado : {} ( Permite {} descargas paralelas simultáneas )"
msgstr " - Valor máximo recomendado : {} (Permite {} descargas paralelas simultáneas)"
msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n"
msgstr " - Deshabilitar/Predeterminado : 0 ( Deshabilita la descarga paralela, permite solo 1 descarga simultánea )\n"
msgstr " - Desactivar/Predeterminado : 0 (Desactiva la descarga paralela, permite solo 1 descarga simultánea)\n"
msgid "Invalid input! Try again with a valid input [or 0 to disable]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [o 0 para deshabilitar]"
msgstr "¡Entrada no válida! Intente nuevamente con una entrada válida [o 0 para desactivar]"
msgid "Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "Hyprland necesita acceso a su asiento (colección de dispositivos de hardware, es decir, teclado, mouse, etc.)"
@ -1263,10 +1263,10 @@ msgid "All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...
msgstr "Todos los valores introducidos pueden tener como sufijo una unidad: %, B, KB, KiB, MB, MiB..."
msgid "Would you like to use unified kernel images?"
msgstr "¿Le gustaría utilizar imágenes del kernel unificadas?"
msgstr "¿Le gustaría utilizar imágenes del núcleo unificadas?"
msgid "Unified kernel images"
msgstr "Imágenes del kernel unificadas"
msgstr "Imágenes del núcleo unificadas"
msgid "Waiting for time sync (timedatectl show) to complete."
msgstr "Esperando a que se complete la sincronización de la hora (timedatectl show)."
@ -1449,7 +1449,7 @@ msgid "Enter your gateway (router) IP address (leave blank for none)"
msgstr "Ingrese la dirección IP de su puerta de enlace (enrutador) (deje en blanco si no hay ninguna)"
msgid "Gateway address"
msgstr "Dirección de puerta de enlace"
msgstr "Dirección de la puerta de enlace"
msgid "Enter your DNS servers with space separated (leave blank for none)"
msgstr "Ingrese sus servidores DNS separados por espacios (deje en blanco si no hay ninguno)"
@ -1464,7 +1464,7 @@ msgid "Kernel"
msgstr "Núcleo"
msgid "UEFI is not detected and some options are disabled"
msgstr "No se detecta UEFI y algunas opciones están deshabilitadas"
msgstr "No se detecta UEFI y algunas opciones están desactivadas"
msgid "Info"
msgstr "Info"
@ -1494,17 +1494,17 @@ msgid "Directory"
msgstr "Directorio"
msgid "Enter a directory for the configuration(s) to be saved (tab completion enabled)"
msgstr "Ingrese un directorio para guardar las configuraciones (autocompletado de tabulación habilitado)"
msgstr "Ingrese un directorio para guardar las configuraciones (autocompletado de tabulación activado)"
#, python-brace-format
msgid "Do you want to save the configuration file(s) to {}?"
msgstr "¿Desea guardar los archivos de configuración en {}?"
msgid "Enabled"
msgstr "Habilitado"
msgstr "Activado"
msgid "Disabled"
msgstr "Deshabilitado"
msgstr "Desactivado"
msgid "Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"
msgstr "Envíe este problema (y archivo) a https://github.com/archlinux/archinstall/issues"
@ -1681,137 +1681,121 @@ msgid "Exit search mode"
msgstr "Salir del modo de búsqueda"
msgid "General"
msgstr ""
msgstr "General"
#, fuzzy
msgid "Navigation"
msgstr "Guardar configuración"
msgstr "Navegación"
#, fuzzy
msgid "Selection"
msgstr "Seleccione regiones"
msgstr "Selección"
msgid "Search"
msgstr ""
msgstr "Buscar"
msgid "Down"
msgstr ""
msgstr "Abajo"
msgid "Up"
msgstr ""
msgstr "Arriba"
#, fuzzy
msgid "Confirm"
msgstr "Confirmar y salir"
msgstr "Confirmar"
#, fuzzy
msgid "Focus right"
msgstr "Mover a la derecha"
msgstr "Enfocar a la derecha"
#, fuzzy
msgid "Focus left"
msgstr "Mover a la izquierda"
msgstr "Enfocar a la izquierda"
msgid "Toggle"
msgstr ""
msgstr "Alternar"
#, fuzzy
msgid "Show/Hide help"
msgstr "Mostrar ayuda"
msgstr "Mostrar/Ocultar ayuda"
msgid "Quit"
msgstr ""
msgstr "Salir"
msgid "First"
msgstr ""
msgstr "Primero"
msgid "Last"
msgstr ""
msgstr "Último"
#, fuzzy
msgid "Select"
msgstr "Seleccione regiones"
msgstr "Seleccionar"
msgid "Page Up"
msgstr ""
msgstr "Subir página"
msgid "Page Down"
msgstr ""
msgstr "Bajar página"
#, fuzzy
msgid "Page up"
msgstr "Grupo de paquetes:"
msgstr "Subir página"
#, fuzzy
msgid "Page down"
msgstr "Bajar"
msgstr "Bajar página"
msgid "Page Left"
msgstr ""
msgstr "Página izquierda"
#, fuzzy
msgid "Page Right"
msgstr "Mover a la derecha"
msgstr "Página derecha"
msgid "Cursor up"
msgstr ""
msgstr "Subir"
#, fuzzy
msgid "Cursor down"
msgstr "Bajar"
msgstr "Cursor hacia abajo"
#, fuzzy
msgid "Cursor right"
msgstr "Mover a la derecha"
msgstr "Cursor hacia la derecha"
#, fuzzy
msgid "Cursor left"
msgstr "Mover a la izquierda"
msgstr "Cursor hacia la izquierda"
msgid "Top"
msgstr ""
msgstr "Arriba"
msgid "Bottom"
msgstr ""
msgstr "Abajo"
msgid "Home"
msgstr ""
msgstr "Inicio"
#, fuzzy
msgid "End"
msgstr "Habilitado"
msgstr "Final"
#, fuzzy
msgid "Toggle option"
msgstr "Opciones del subvolumen"
msgstr "Opción de alternancia"
msgid "Scroll Up"
msgstr ""
msgstr "Mover Arriba"
#, fuzzy
msgid "Scroll Down"
msgstr "Bajar en vista previa"
msgstr "Deslizar hacia abajo"
msgid "Scroll Left"
msgstr ""
msgstr "Mover hacia la izquierda"
msgid "Scroll Right"
msgstr ""
msgstr "Mover hacia la derecha"
msgid "Scroll Home"
msgstr ""
msgstr "Ir al Inicio"
msgid "Scroll End"
msgstr ""
msgstr "Ir al Final"
msgid "Focus Next"
msgstr ""
msgstr "Enfocar a la siguiente"
msgid "Focus Previous"
msgstr ""
msgstr "Enfocar a la anterior"
msgid "Copy selected text"
msgstr ""
msgstr "Copiar texto seleccionado"
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "labwc necesita acceso a su asiento (colección de dispositivos de hardware, es decir, teclado, ratón, etc.)"
@ -1838,7 +1822,7 @@ msgid "Reboot system"
msgstr "Reiniciar el sistema"
msgid "chroot into installation for post-installation configurations"
msgstr "chroot en la instalación para configuraciones posteriores a la instalación"
msgstr "Hacer chroot en la instalación para configuraciones posteriores a la instalación"
msgid "Installation completed"
msgstr "Instalación completada"
@ -1907,7 +1891,7 @@ msgstr "Sudo sin contraseña: "
#, python-brace-format
msgid "Btrfs snapshot type: {}"
msgstr "Tipo de snapshot Btrfs: {}"
msgstr "Tipo de instantánea BTRFS: {}"
msgid "Syncing the system..."
msgstr "Sincronizando el sistema..."
@ -1932,7 +1916,7 @@ msgid "U2F Login Method"
msgstr "Método de inicio de sesión U2F"
msgid "Enable passwordless sudo?"
msgstr "Habilitar sudo sin contraseña?"
msgstr "¿Activar sudo sin contraseña?"
#, python-brace-format
msgid "Setting up U2F device for user: {}"
@ -1948,25 +1932,25 @@ msgid "No network connection found"
msgstr "No se encontró una conección de red"
msgid "Would you like to connect to a Wifi?"
msgstr "¿Desea conectarse a Wifi?"
msgstr "¿Desea conectarse al Wi-Fi?"
msgid "No wifi interface found"
msgstr "No se encontró ninguna interfaz de wifi"
msgstr "No se encontró ninguna interfaz Wi-Fi"
msgid "Select wifi network to connect to"
msgstr "Seleccione una red de wifi para conectarse"
msgstr "Seleccione una red Wi-Fi para conectarse"
msgid "Scanning wifi networks..."
msgstr "Buscando redes de wifi..."
msgstr "Buscando redes Wi-Fi..."
msgid "No wifi networks found"
msgstr "No se encontró redes de wifi"
msgstr "No se encontró redes Wi-Fi"
msgid "Failed setting up wifi"
msgstr "Error configurando wifi"
msgstr "Error al configurar el Wi-Fi"
msgid "Enter wifi password"
msgstr "Ingrese contraseña de wifi"
msgstr "Ingrese la contraseña del Wi-Fi"
msgid "Ok"
msgstr "Ok"
@ -2017,7 +2001,7 @@ msgid "Compression algorithm"
msgstr "Algoritmo de compresión"
msgid "Only packages such as base, sudo, linux, linux-firmware, efibootmgr and optional profile packages are installed."
msgstr "Solo paquetes como base, base-devel, linux, linux-firmware, efibootmgr y paquetes opcionales de perfil se instalan."
msgstr "Solo paquetes como base, sudo, linux, linux-firmware, efibootmgr y paquetes opcionales de perfil se instalan."
msgid "Select zram compression algorithm:"
msgstr "Seleccione algorimto de compresión zram:"
@ -2031,22 +2015,20 @@ msgstr "Usar gestor de Red (backend iwd)"
msgid "Firewall"
msgstr "Cortafuegos"
#, fuzzy
msgid "Additional fonts"
msgstr "Repositorios adicionales"
msgstr "Fuentes adicionales"
#, fuzzy
msgid "Select font packages to install"
msgstr "Seleccione el gestor de arranque que desea instalar"
msgstr "Seleccione los paquetes de fuentes que desea instalar"
msgid "Unicode font coverage for most languages"
msgstr ""
msgstr "Fuente unicode cubierta para la mayoría de idiomas"
msgid "color emoji for browsers and apps"
msgstr ""
msgstr "Emoji de colores para navegadores y aplicaciones"
msgid "Chinese, Japanese, Korean characters"
msgstr ""
msgstr "Carácteres Chino, Japonés, Koreano"
msgid "Select audio configuration"
msgstr "Seleccionar configuración de audio"
@ -2107,7 +2089,7 @@ msgid "Select timezone"
msgstr "Seleccione una zona horaria"
msgid "Enter the number of parallel downloads to be enabled"
msgstr "Ingrese el número de descargas paralelas que desea habilitar"
msgstr "Ingrese el número de descargas paralelas que desea activar"
#, python-brace-format
msgid "Value must be between 1 and {}"
@ -2126,10 +2108,10 @@ msgid "Enter server url"
msgstr "Ingrese la URL del servidor"
msgid "Select mirror regions to be enabled"
msgstr "Seleccione las regiones espejo que se habilitarán"
msgstr "Seleccione las regiones espejo que se activarán"
msgid "Select optional repositories to be enabled"
msgstr "Seleccione repositorios opcionales para habilitar"
msgstr "Seleccione repositorios opcionales para activar"
msgid "Select an interface"
msgstr "Seleccione una interfaz"
@ -2170,54 +2152,56 @@ msgstr "Seguridad de la contraseña: Fuerte"
msgid "The selected desktop profile requires a regular user to log in via the greeter"
msgstr "El perfil de escritorio seleccionado requiere que un usuario estándar inicie sesión a través de la pantalla de bienvenida"
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Environment type: {} {}"
msgstr "Tipo de entorno: {}"
msgstr "Tipo de entorno: {} {}"
#, fuzzy
msgid "Input cannot be empty"
msgstr "El valor no puede estár vacío"
msgstr "La entrada no debe estar vacía"
msgid "Recommended"
msgstr ""
msgstr "Recomendado"
#, fuzzy
msgid "Package"
msgstr "Grupo de paquetes:"
msgstr "Paquete"
msgid "Curated selection of KDE Plasma packages"
msgstr ""
msgstr "Selección curada de paquetes KDE Plasma"
msgid "Dependencies"
msgstr ""
msgstr "Dependencias"
#, fuzzy
msgid "Package group"
msgstr "Grupo de paquetes:"
msgstr "Grupo de paquetes"
msgid "Extensive KDE Plasma installation"
msgstr ""
msgstr "Instalación KDE Plasma Extensible"
#, fuzzy
msgid "Packages in group"
msgstr "Grupo de paquetes:"
msgstr "Paquetes en grupo"
msgid "Minimal KDE Plasma installation"
msgstr ""
msgstr "Instalación minima de KDE Plasma"
#, fuzzy
msgid "Description"
msgstr "Cifrado de disco"
msgstr "Descripción"
#, fuzzy
msgid "Select a flavor of KDE Plasma to install"
msgstr "Seleccione el gestor de arranque que desea instalar"
msgstr "Seleccione una variante de KDE Plasma para instalar"
msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
msgstr ""
msgstr "Sustitución de Arial/Times/Courier, compatibilidad con caracteres cirílicos para Steam/juegos"
msgid "wide Unicode coverage, good fallback font"
msgstr ""
msgstr "Amplia cobertura Unicode, buena fuente de reserva"
#, python-brace-format
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
msgstr "Configurando Inicio U2F: {u2f_config.u2f_login_method.value}"
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr "Predeterminado: {DEFAULT_ITER_TIME}ms, Rango recomendado: 1000-60000"
#~ msgid "Add :"
#~ msgstr "Añadir :"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,10 +63,10 @@ msgid "Write additional packages to install (space separated, leave blank to ski
msgstr "追加でインストールするパッケージを書く(スペースで区切る。無記入でスキップ): "
msgid "Copy ISO network configuration to installation"
msgstr "ISO ネットワーク設定をインストール環境にコピー"
msgstr "ISO ネットワーク設定をインストール環境にコピー"
msgid "Use NetworkManager (necessary for configuring internet graphically in GNOME and KDE)"
msgstr "NetworkManager を使用GNOME と KDE でインターネットをグラフィカルに設定するのに必要)"
msgstr "ネットワークマネージャーを使用GNOME と KDE でインターネットをグラフィカルに設定するのに必要)"
msgid "Select one network interface to configure"
msgstr "設定するネットワークインターフェイスを 1 つ選択"
@ -224,7 +224,7 @@ msgid "Choose which locale language to use"
msgstr "使用するロケール言語を選択"
msgid "Choose which locale encoding to use"
msgstr "使用するロケール エンコーディングを選択"
msgstr "使用するロケールエンコーディングを選択"
msgid "Select one of the values shown below: "
msgstr "以下の値のいずれかを選択: "
@ -256,6 +256,9 @@ msgstr "ロケール言語"
msgid "Locale encoding"
msgstr "ロケールエンコーディング"
msgid "Console font"
msgstr "コンソールフォント"
msgid "Drive(s)"
msgstr "ドライブ"
@ -449,7 +452,7 @@ msgid "Pacman is already running, waiting maximum 10 minutes for it to terminate
msgstr "Pacman はすでに実行されており、終了するまで最大 10 分間待機します。"
msgid "Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall."
msgstr "既存の pacman のロックが終了しませんでした。Archinstall を使用する前に、既存の pacman セッションをクリーンアップしてください。"
msgstr "既存の pacman のロックが終了しませんでした。archinstall を使用する前に、既存の pacman セッションをすべてクリーンアップしてください。"
msgid "Choose which optional additional repositories to enable"
msgstr "オプションで有効にする追加リポジトリを選択"
@ -823,6 +826,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 でスキップ"
@ -950,7 +966,7 @@ msgid "Encryption type"
msgstr "暗号化のタイプ"
msgid "Iteration time"
msgstr "反復時間"
msgstr "イテレーション時間"
msgid "Enter iteration time for LUKS encryption (in milliseconds)"
msgstr "LUKS 暗号化のイテレーション時間(ミリ秒単位)を入力"
@ -1030,7 +1046,7 @@ msgstr "選択したプロファイルにインストールするグリーター
#, python-brace-format
msgid "Environment type: {}"
msgstr "環境タイプ: {}"
msgstr "環境タイプ: {}"
msgid "The proprietary Nvidia driver is not supported by Sway. It is likely that you will run into issues, are you okay with that?"
msgstr "プロプライエタリの Nvidia ドライバーは Sway ではサポートされていません。問題が発生する可能性がありますが、よろしいですか?"
@ -1173,7 +1189,7 @@ msgid "Locales"
msgstr "ロケール"
msgid "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)"
msgstr "NetworkManager を使用GNOME と KDE でインターネットをグラフィカルに設定するのに必要)"
msgstr "ネットワークマネージャーを使用GNOME と KDE でインターネットをグラフィカルに設定するのに必要)"
msgid "Total: {} / {}"
msgstr "全体: {} / {}"
@ -1206,6 +1222,21 @@ msgstr "製品"
msgid "Invalid configuration: {error}"
msgstr "無効な設定: {error}"
msgid "Ready to install"
msgstr "インストール準備完了"
msgid "Disks"
msgstr "ディスク"
msgid "Packages"
msgstr "パッケージ"
msgid "Network"
msgstr "ネットワーク"
msgid "Locale"
msgstr "ロケール"
msgid "Type"
msgstr "タイプ"
@ -1296,7 +1327,7 @@ msgid "LVM disk encryption with more than 2 partitions is currently not supporte
msgstr "パーティションが2個を超える場合の LVM ディスク暗号化は、現在サポートしていません"
msgid "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE Plasma)"
msgstr "NetworkManager を使用するGNOME と KDE Plasma でインターネットをグラフィカルに設定するのに必要)"
msgstr "ネットワークマネージャーを使用GNOME と KDE Plasma でインターネットをグラフィカルに設定するのに必要)"
msgid "Select a LVM option"
msgstr "LVM のオプションを選択"
@ -1550,7 +1581,7 @@ msgid "The specified configuration will be applied"
msgstr "指定された設定が適用されます"
msgid "Wipe"
msgstr "ワイプ"
msgstr "消去"
msgid "Mark/Unmark as XBOOTLDR"
msgstr "XBOOTLDR としてマーク/マーク解除"
@ -1663,6 +1694,123 @@ msgstr "検索モードを開始"
msgid "Exit search mode"
msgstr "検索モードを終了"
msgid "General"
msgstr "一般"
msgid "Navigation"
msgstr "ナビゲーション"
msgid "Selection"
msgstr "選択"
msgid "Search"
msgstr "検索"
msgid "Down"
msgstr "下へ"
msgid "Up"
msgstr "上へ"
msgid "Confirm"
msgstr "確認"
msgid "Focus right"
msgstr "右にフォーカス"
msgid "Focus left"
msgstr "左にフォーカス"
msgid "Toggle"
msgstr "切り替え"
msgid "Show/Hide help"
msgstr "ヘルプを表示/隠す"
msgid "Quit"
msgstr "終了"
msgid "First"
msgstr "最初"
msgid "Last"
msgstr "最後"
msgid "Select"
msgstr "選択"
msgid "Page Up"
msgstr "上のページへ"
msgid "Page Down"
msgstr "下のページへ"
msgid "Page up"
msgstr "上のページへ"
msgid "Page down"
msgstr "下のページへ"
msgid "Page Left"
msgstr "左のページへ"
msgid "Page Right"
msgstr "右のページへ"
msgid "Cursor up"
msgstr "カーソルを上へ"
msgid "Cursor down"
msgstr "カースルを下へ"
msgid "Cursor right"
msgstr "カーソルを右へ"
msgid "Cursor left"
msgstr "カーソルを左へ"
msgid "Top"
msgstr "一番上へ"
msgid "Bottom"
msgstr "一番下へ"
msgid "Home"
msgstr "ホーム"
msgid "End"
msgstr "最後へ"
msgid "Toggle option"
msgstr "オプションを切り替え"
msgid "Scroll Up"
msgstr "上にスクロール"
msgid "Scroll Down"
msgstr "下にスクロール"
msgid "Scroll Left"
msgstr "左にスクロール"
msgid "Scroll Right"
msgstr "右にスクロール"
msgid "Scroll Home"
msgstr "ホームにスクロール"
msgid "Scroll End"
msgstr "一番下にスクロール"
msgid "Focus Next"
msgstr "次にフォーカス"
msgid "Focus Previous"
msgstr "前にフォーカス"
msgid "Copy selected text"
msgstr "選択したテキストをコピー"
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "labwc はお使いの Seatキーボード、マウスなどのハードウェアデバイス群にアクセスする必要があります"
@ -1873,14 +2021,29 @@ msgid "Select zram compression algorithm:"
msgstr "zram 圧縮アルゴリズムを選択:"
msgid "Use Network Manager (default backend)"
msgstr "NetworkManagerデフォルトのバックエンドを使用する"
msgstr "Network Manager を使用(デフォルトのバックエンド)"
msgid "Use Network Manager (iwd backend)"
msgstr "NetworkManager (iwd バックエンド) を使用する"
msgstr "Network Manager を使用iwd バックエンド)"
msgid "Firewall"
msgstr "ファイアウォール"
msgid "Additional fonts"
msgstr "追加フォント"
msgid "Select font packages to install"
msgstr "インストールするフォントパッケージを選択"
msgid "Unicode font coverage for most languages"
msgstr "ほとんどの言語をカバーする Unicode フォント"
msgid "color emoji for browsers and apps"
msgstr "ブラウザやアプリ用のカラー絵文字"
msgid "Chinese, Japanese, Korean characters"
msgstr "日本語、中国語、韓国語の文字"
msgid "Select audio configuration"
msgstr "オーディオ設定を選択"
@ -1888,7 +2051,7 @@ msgid "Enter credentials file decryption password"
msgstr "認証情報ファイルの復号パスワードを入力"
msgid "Enter root password"
msgstr "ルートパスワードを入力"
msgstr "root パスワードを入力"
msgid "Select bootloader to install"
msgstr "インストールするブートローダーを選択"
@ -1970,6 +2133,12 @@ msgstr "インターフェースを選択"
msgid "Choose network configuration"
msgstr "ネットワーク設定を選択"
msgid "Recommended: Network Manager for desktop, Manual for server"
msgstr "推奨: デスクトップのネットワークマネージャー、サーバーのマニュアル"
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
msgstr "警告: ネットワーク設定が選択されていません。インストール後のシステムで、ネットワークを手動で設定する必要があります。"
msgid "No packages found"
msgstr "パッケージが見つかりません"
@ -1990,3 +2159,74 @@ msgstr "パスワードを入力"
msgid "The password did not match, please try again"
msgstr "パスワードが一致しません。もう一度試してください"
msgid "Password strength: Weak"
msgstr "パスワードの強度: 弱い"
msgid "Password strength: Moderate"
msgstr "パスワードの強度: 中程度"
msgid "Password strength: Strong"
msgstr "パスワードの強度: 強い"
msgid "The selected desktop profile requires a regular user to log in via the greeter"
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 "厳選した KDE Plasma パッケージ"
msgid "Dependencies"
msgstr "依存関係"
msgid "Package group"
msgstr "パッケージグループ"
msgid "Extensive KDE Plasma installation"
msgstr "広範囲の KDE Plasma インストール"
msgid "Packages in group"
msgstr "グループ内のパッケージ"
msgid "Minimal KDE Plasma installation"
msgstr "最小限の KDE Plasma インストール"
msgid "Description"
msgstr "説明"
msgid "Select a flavor of KDE Plasma to install"
msgstr "インストールする KDE Plasma のバージョンを選択"
msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
msgstr "Arial/Times/Courier フォントの代替、Steam/ゲームにおけるキリル文字のサポート"
msgid "wide Unicode coverage, good fallback font"
msgstr "Unicode を広くカバーする良い代替フォント"
#, python-brace-format
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
msgstr "U2F ログインの設定: {u2f_config.u2f_login_method.value}"
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr "デフォルト: {DEFAULT_ITER_TIME}ms, 推奨範囲: 1000-60000"
#, python-brace-format
msgid "Setting up U2F login: {}"
msgstr "U2F ログインの設定: {}"
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "デフォルト: {}ms, 推奨範囲: 1000-60000"

View File

@ -42,7 +42,7 @@
{"abbr": "fo", "lang": "Faroese"},
{"abbr": "fa", "lang": "Persian"},
{"abbr": "fj", "lang": "Fijian"},
{"abbr": "fi", "lang": "Finnish"},
{"abbr": "fi", "lang": "Finnish", "translated_lang": "Suomi"},
{"abbr": "fr", "lang": "French", "translated_lang": "Français"},
{"abbr": "fy", "lang": "Western Frisian"},
{"abbr": "ff", "lang": "Fulah"},

View File

@ -256,6 +256,9 @@ msgstr "Мова локалізації"
msgid "Locale encoding"
msgstr "Кодування локалізації"
msgid "Console font"
msgstr "Шрифт консолі"
msgid "Drive(s)"
msgstr "Диск(и)"
@ -1039,7 +1042,7 @@ msgid "Back"
msgstr "Назад"
msgid "Please chose which greeter to install for the chosen profiles: {}"
msgstr "Будь ласка, виберіть, який екран привітання встановити для обраних профілів: {}"
msgstr "Будь ласка, виберіть, який менеджер входу встановити для обраних профілів: {}"
#, python-brace-format
msgid "Environment type: {}"
@ -1101,10 +1104,10 @@ msgid "Graphics driver"
msgstr "Графічні драйвери"
msgid "Greeter"
msgstr "Дисплейний менеджер"
msgstr "Менеджер входу"
msgid "Please chose which greeter to install"
msgstr "Виберіть, який дисплейний менеджер встановлювати"
msgstr "Виберіть, який менеджер входу встановлювати"
msgid "This is a list of pre-programmed default_profiles"
msgstr "Список попередньо налаштованих default_profiles"
@ -1219,6 +1222,21 @@ msgstr "Модель"
msgid "Invalid configuration: {error}"
msgstr "Неправильна конфігурація: {error}"
msgid "Ready to install"
msgstr "Готово до встановлення"
msgid "Disks"
msgstr "Диски"
msgid "Packages"
msgstr "Пакети"
msgid "Network"
msgstr "Мережа"
msgid "Locale"
msgstr "Локаль"
msgid "Type"
msgstr "Тип"
@ -1903,7 +1921,7 @@ msgid "Snapshot type: {}"
msgstr "Тип знімка: {}"
msgid "U2F login setup"
msgstr "Налаштування входу з пристроєм двофакторної авторизації"
msgstr "Налаштування входу з пристроєм двофакторної автентифікації"
msgid "No U2F devices found"
msgstr "Не знайдено жодного пристрою двофакторної автентифікації"
@ -1919,7 +1937,7 @@ msgid "Setting up U2F device for user: {}"
msgstr "Налаштування пристроя двофакторної автентифікації для користувача: {}"
msgid "You may need to enter the PIN and then touch your U2F device to register it"
msgstr "Ви повинні написати PIN та потім торкнутися вашого пристрою двофакторної авторизації, щоб зареєструвати його"
msgstr "Ви повинні написати PIN та потім торкнутися вашого пристрою двофакторної автентифікації, щоб зареєструвати його"
msgid "Starting device modifications in "
msgstr "Початок модифікацій пристрою в "
@ -2115,6 +2133,12 @@ msgstr "Оберіть інтерфейс"
msgid "Choose network configuration"
msgstr "Оберіть конфігурацію мережі"
msgid "Recommended: Network Manager for desktop, Manual for server"
msgstr "Рекомендовано: Network Manager для робочого столу, ручне налаштування для сервера"
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Попередження: конфігурацію мережі не обрано. Мережу доведеться налаштувати вручну на встановленій системі."
msgid "No packages found"
msgstr "Пакети не знайдено"
@ -2148,45 +2172,61 @@ msgstr "Надійність пароля: Сильна"
msgid "The selected desktop profile requires a regular user to log in via the greeter"
msgstr "Обраний профіль робочого столу вимагає входу звичайного користувача через менеджер входу"
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Environment type: {} {}"
msgstr "Тип середовища: {}"
msgstr "Тип середовища: {} {}"
#, fuzzy
msgid "Input cannot be empty"
msgstr "Значення не може бути пустим"
msgid "Recommended"
msgstr ""
msgstr "Рекомендовано"
#, fuzzy
msgid "Package"
msgstr "Група пакетів:"
msgstr "Пакет"
msgid "Curated selection of KDE Plasma packages"
msgstr ""
msgstr "Підібраний набір пакетів KDE Plasma"
msgid "Dependencies"
msgstr ""
msgstr "Залежності"
#, fuzzy
msgid "Package group"
msgstr "Група пакетів:"
msgstr "Група пакетів"
msgid "Extensive KDE Plasma installation"
msgstr ""
msgstr "Розширене встановлення KDE Plasma"
#, fuzzy
msgid "Packages in group"
msgstr "Група пакетів:"
msgstr "Пакети у групі"
msgid "Minimal KDE Plasma installation"
msgstr ""
msgstr "Мінімальне встановлення KDE Plasma"
#, fuzzy
msgid "Description"
msgstr "Шифрування диска"
msgstr "Опис"
#, fuzzy
msgid "Select a flavor of KDE Plasma to install"
msgstr "Оберіть завантажувач для встановлення"
msgstr "Оберіть варіант KDE Plasma для встановлення"
msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
msgstr "заміна Arial/Times/Courier, підтримка кирилиці для Steam та ігор"
msgid "wide Unicode coverage, good fallback font"
msgstr "широке покриття Unicode, хороший резервний шрифт"
#, python-brace-format
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
msgstr "Налаштування входу з пристроєм двофакторної автентифікації: {u2f_config.u2f_login_method.value}"
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr "Стандартно: {DEFAULT_ITER_TIME}мс, Рекомендований діапазон - 1000-60000"
#, python-brace-format
msgid "Setting up U2F login: {}"
msgstr "Налаштування входу з пристроєм двофакторної автентифікації: {}"
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "Стандартно: {}мс, Рекомендований діапазон - 1000-60000"

View File

@ -18,7 +18,7 @@ from archinstall.lib.packages.util import check_version_upgrade
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr, translation_handler
from archinstall.lib.utils.util import running_from_iso
from archinstall.tui.ui.components import tui
from archinstall.tui.components import tui
def _log_sys_info() -> None:

View File

@ -5,6 +5,7 @@ import time
from archinstall.lib.applications.application_handler import ApplicationHandler
from archinstall.lib.args import ArchConfig, ArchConfigHandler
from archinstall.lib.authentication.authentication_handler import AuthenticationHandler
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
@ -21,7 +22,7 @@ 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
from archinstall.tui.ui.components import tui
from archinstall.tui.components import tui
def show_menu(
@ -211,12 +212,21 @@ def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
config.write_debug()
config.save()
# Safety net for silent/config-file flow. The TUI menu blocks Install via
# GlobalMenu._validate_bootloader() before reaching this point.
if failure := validate_bootloader_layout(
arch_config_handler.config.bootloader_config,
arch_config_handler.config.disk_config,
):
error(failure.description)
return
if arch_config_handler.args.dry_run:
return
if not arch_config_handler.args.silent:
aborted = False
res: bool = tui.run(config.confirm_config)
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
if not res:
debug('Installation aborted')

View File

@ -12,7 +12,7 @@ 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.ui.components import tui
from archinstall.tui.components import tui
def perform_installation(arch_config_handler: ArchConfigHandler) -> None:
@ -77,7 +77,7 @@ async def main(arch_config_handler: ArchConfigHandler | None = None) -> None:
if not arch_config_handler.args.silent:
aborted = False
res: bool = tui.run(config.confirm_config)
res: bool = tui.run(lambda: config.confirm_config(show_install_warnings=True))
if not res:
debug('Installation aborted')

View File

@ -10,7 +10,7 @@ from archinstall.lib.installer import Installer
from archinstall.lib.menu.util import delayed_warning
from archinstall.lib.output import debug, error
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.components import tui
from archinstall.tui.components import tui
def show_menu(arch_config_handler: ArchConfigHandler) -> None:

View File

@ -21,8 +21,8 @@ from textual.worker import WorkerCancelled
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import Result, ResultType
ValueT = TypeVar('ValueT')
@ -350,7 +350,7 @@ class OptionListScreen(BaseScreen[ValueT]):
def _set_preview(self, item_id: str) -> None:
if self._preview_location is None:
return None
return
preview_widget = self.query_one('#preview_content', Label)
item = self._group.find_by_id(item_id)
@ -530,7 +530,7 @@ class SelectListScreen(BaseScreen[ValueT]):
selection_list = self.query_one(SelectionList)
if not selection_list.has_focus or event.key != 'enter':
return None
return
if len(self._selected_items) < 1:
index = selection_list.highlighted
@ -743,7 +743,7 @@ class ConfirmationScreen(BaseScreen[ValueT]):
if self._is_btn_focus():
item = self._group.focus_item
if not item:
return None
return
_ = self.dismiss(Result(ResultType.Selection, _item=item))

File diff suppressed because it is too large Load Diff

View File

@ -1,109 +0,0 @@
from dataclasses import dataclass, field
from enum import Enum
from archinstall.lib.translationhandler import tr
class HelpTextGroupId(Enum):
GENERAL = 'General'
NAVIGATION = 'Navigation'
SELECTION = 'Selection'
SEARCH = 'Search'
@dataclass
class HelpText:
description: str
keys: list[str] = field(default_factory=list)
@dataclass
class HelpGroup:
group_id: HelpTextGroupId
group_entries: list[HelpText]
def get_desc_width(self) -> int:
return max([len(e.description) for e in self.group_entries])
def get_key_width(self) -> int:
return max([len(', '.join(e.keys)) for e in self.group_entries])
class Help:
# the groups needs to be classmethods not static methods
# because they rely on the DeferredTranslation setup first;
# if they are static methods, they will be called before the
# translation setup is done
@staticmethod
def general() -> HelpGroup:
return HelpGroup(
group_id=HelpTextGroupId.GENERAL,
group_entries=[
HelpText(tr('Show help'), ['Ctrl+h']),
HelpText(tr('Exit help'), ['Esc']),
],
)
@staticmethod
def navigation() -> HelpGroup:
return HelpGroup(
group_id=HelpTextGroupId.NAVIGATION,
group_entries=[
HelpText(tr('Preview scroll up'), ['PgUp']),
HelpText(tr('Preview scroll down'), ['PgDown']),
HelpText(tr('Move up'), ['k', '']),
HelpText(tr('Move down'), ['j', '']),
HelpText(tr('Move right'), ['l', '']),
HelpText(tr('Move left'), ['h', '']),
HelpText(tr('Jump to entry'), ['1..9']),
],
)
@staticmethod
def selection() -> HelpGroup:
return HelpGroup(
group_id=HelpTextGroupId.SELECTION,
group_entries=[
HelpText(tr('Skip selection (if available)'), ['Esc']),
HelpText(tr('Reset selection (if available)'), ['Ctrl+c']),
HelpText(tr('Select on single select'), ['Enter']),
HelpText(tr('Select on multi select'), ['Space', 'Tab']),
HelpText(tr('Reset'), ['Ctrl-C']),
HelpText(tr('Skip selection menu'), ['Esc']),
],
)
@staticmethod
def search() -> HelpGroup:
return HelpGroup(
group_id=HelpTextGroupId.SEARCH,
group_entries=[
HelpText(tr('Start search mode'), ['/']),
HelpText(tr('Exit search mode'), ['Esc']),
],
)
@staticmethod
def get_help_text() -> str:
help_output = ''
help_texts = [
Help.general(),
Help.navigation(),
Help.selection(),
Help.search(),
]
max_desc_width = max([help.get_desc_width() for help in help_texts]) + 2
max_key_width = max([help.get_key_width() for help in help_texts])
for help_group in help_texts:
help_output += f'{help_group.group_id.value}\n'
divider_len = max_desc_width + max_key_width
help_output += '-' * divider_len + '\n'
for entry in help_group.group_entries:
help_output += entry.description.ljust(max_desc_width, ' ') + ', '.join(entry.keys) + '\n'
help_output += '\n'
return help_output

View File

@ -1,18 +1,17 @@
from collections.abc import Callable
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from enum import Enum
from functools import cached_property
from typing import Any, ClassVar, Self, override
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.encoding import unicode_ljust
@dataclass
class MenuItem:
text: str
value: Any | None = None
action: Callable[[Any], Any] | None = None
action: Callable[[Any], Awaitable[Any]] | None = None
enabled: bool = True
read_only: bool = False
mandatory: bool = False
@ -23,6 +22,7 @@ class MenuItem:
key: str | None = None
_id: str = ''
_yes: ClassVar[Self | None] = None
_no: ClassVar[Self | None] = None
@ -110,6 +110,11 @@ class MenuItemGroup:
if self.focus_item not in self.items:
raise ValueError(f'Selected item not in menu: {self.focus_item}')
@classmethod
def from_objects(cls, items: list[Any]) -> Self:
items = [MenuItem(str(id(item)), value=item) for item in items]
return cls(items)
def add_item(self, item: MenuItem) -> None:
self._menu_items.append(item)
del self.items # resetting the cache
@ -143,7 +148,7 @@ class MenuItemGroup:
cls,
enum_cls: type[Enum],
sort_items: bool = False,
preset: Enum | None = None,
preset: Iterable[Enum] | Enum | None = None,
) -> Self:
items = [MenuItem(elem.value, value=elem) for elem in enum_cls]
group = cls(items, sort_items=sort_items)
@ -198,60 +203,10 @@ class MenuItemGroup:
return None
def index_focus(self) -> int | None:
if self.focus_item and self.items:
try:
return self.items.index(self.focus_item)
except ValueError:
# on large menus (15k+) when filtering very quickly
# the index search is too slow while the items are reduced
# by the filter and it will blow up as it cannot find the
# focus item
pass
return None
@property
def size(self) -> int:
return len(self.items)
def get_max_width(self) -> int:
# use the menu_items not the items here otherwise the preview
# will get resized all the time when a filter is applied
return max([len(self.get_item_text(item)) for item in self._menu_items])
@cached_property
def _max_items_text_width(self) -> int:
return max([len(item.text) for item in self._menu_items])
def get_item_text(self, item: MenuItem) -> str:
if item.is_empty():
return ''
max_width = self._max_items_text_width
display_text = item.get_display_value()
default_text = self._default_suffix(item)
text = unicode_ljust(str(item.text), max_width, ' ')
spacing = ' ' * 4
if display_text:
text = f'{text}{spacing}{display_text}'
elif self._checkmarks:
from archinstall.tui.types import Chars
if item.has_value():
if item.get_value() is not False:
text = f'{text}{spacing}{Chars.Check}'
else:
text = item.text
if default_text:
text = f'{text} {default_text}'
return text.rstrip(' ')
def _default_suffix(self, item: MenuItem) -> str:
if self.default_item == item:
return tr(' (default)')
@ -274,45 +229,11 @@ class MenuItemGroup:
return 0
return 1
@property
def filter_pattern(self) -> str:
return self._filter_pattern
def has_filter(self) -> bool:
return self._filter_pattern != ''
def set_filter_pattern(self, pattern: str) -> None:
self._filter_pattern = pattern
del self.items # resetting the cache
self.focus_first()
def append_filter(self, pattern: str) -> None:
self._filter_pattern += pattern
del self.items # resetting the cache
self.focus_first()
def reduce_filter(self) -> None:
self._filter_pattern = self._filter_pattern[:-1]
del self.items # resetting the cache
self.focus_first()
def _reload_focus_item(self) -> None:
if len(self.items) > 0:
if self.focus_item not in self.items:
self.focus_first()
else:
self.focus_item = None
def is_item_selected(self, item: MenuItem) -> bool:
return item in self.selected_items
def select_current_item(self) -> None:
if self.focus_item:
if self.focus_item in self.selected_items:
self.selected_items.remove(self.focus_item)
else:
self.selected_items.append(self.focus_item)
def focus_index(self, index: int) -> None:
enabled = self.get_enabled_items()
self.focus_item = enabled[index]
@ -379,12 +300,6 @@ class MenuItemGroup:
return None
def is_mandatory_fulfilled(self) -> bool:
for item in self._menu_items:
if item.mandatory and not item.value:
return False
return True
def max_item_width(self) -> int:
spaces = [len(str(it.text)) for it in self.items]
if spaces:
@ -417,99 +332,3 @@ class MenuItemGroup:
return False
return True
class MenuItemsState:
def __init__(
self,
item_group: MenuItemGroup,
total_cols: int,
total_rows: int,
with_frame: bool,
) -> None:
self._item_group = item_group
self._total_cols = total_cols
self._total_rows = total_rows - 2 if with_frame else total_rows
self._prev_row_idx: int = -1
self._prev_visible_rows: list[int] = []
self._view_items: list[list[MenuItem]] = []
def _determine_focus_row(self) -> int | None:
focus_index = self._item_group.index_focus()
if focus_index is None:
return None
row_index = focus_index // self._total_cols
return row_index
def get_view_items(self) -> list[list[MenuItem]]:
enabled_items = self._item_group.get_enabled_items()
focus_row_idx = self._determine_focus_row()
if focus_row_idx is None:
return []
start, end = 0, 0
if len(self._view_items) == 0 or self._prev_row_idx == -1 or focus_row_idx == 0: # initial setup
if focus_row_idx < self._total_rows:
start = 0
end = self._total_rows
elif focus_row_idx > len(enabled_items) - self._total_rows:
start = len(enabled_items) - self._total_rows
end = len(enabled_items)
else:
start = focus_row_idx
end = focus_row_idx + self._total_rows
elif len(enabled_items) <= self._total_rows: # the view can handle all items
start = 0
end = self._total_rows
elif not self._item_group.has_filter() and focus_row_idx in self._prev_visible_rows: # focus is in the same view
self._prev_row_idx = focus_row_idx
return self._view_items
else:
if self._item_group.has_filter():
start = focus_row_idx
end = focus_row_idx + self._total_rows
else:
delta = focus_row_idx - self._prev_row_idx
if delta > 0: # cursor is on the bottom most row
start = focus_row_idx - self._total_rows + 1
end = focus_row_idx + 1
else: # focus is on the top most row
start = focus_row_idx
end = focus_row_idx + self._total_rows
self._view_items = self._get_view_items(enabled_items, start, end)
self._prev_visible_rows = list(range(start, end))
self._prev_row_idx = focus_row_idx
return self._view_items
def _get_view_items(
self,
items: list[MenuItem],
start_row: int,
total_rows: int,
) -> list[list[MenuItem]]:
groups: list[list[MenuItem]] = []
nr_items = self._total_cols * min(total_rows, len(items))
for x in range(start_row, nr_items, self._total_cols):
groups.append(
items[x : x + self._total_cols],
)
return groups
def _max_visible_items(self) -> int:
return self._total_cols * self._total_rows
def _remaining_next_spots(self) -> int:
return self._max_visible_items() - self._prev_row_idx
def _remaining_prev_spots(self) -> int:
return self._max_visible_items() - self._remaining_next_spots()

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass
from enum import Enum, auto
from typing import Self, cast
from archinstall.tui.menu_item import MenuItem
@ -13,25 +14,58 @@ class ResultType(Enum):
@dataclass
class Result[ValueT]:
type_: ResultType
_item: MenuItem | list[MenuItem] | str | None
_data: ValueT | list[ValueT] | None = None
_item: MenuItem | list[MenuItem] | None = None
def has_item(self) -> bool:
@classmethod
def true(cls) -> Self:
return cls(ResultType.Selection, _data=True) # type: ignore[arg-type]
@classmethod
def false(cls) -> Self:
return cls(ResultType.Selection, _data=False) # type: ignore[arg-type]
@classmethod
def reset(cls) -> Self:
return cls(ResultType.Reset)
@classmethod
def selection(cls, value: ValueT | list[ValueT] | None) -> Self:
return cls(ResultType.Selection, _data=value)
@classmethod
def skip(cls) -> Self:
return cls(ResultType.Skip)
def has_data(self) -> bool:
return self._data is not None
def has_value(self) -> bool:
return self._item is not None
def get_value(self) -> ValueT:
return self.item().get_value() # type: ignore[no-any-return]
def get_values(self) -> list[ValueT]:
return [i.get_value() for i in self.items()]
def item(self) -> MenuItem:
assert self._item is not None and isinstance(self._item, MenuItem)
if isinstance(self._item, list) or self._item is None:
raise ValueError('Invalid item type')
return self._item
def items(self) -> list[MenuItem]:
assert self._item is not None and isinstance(self._item, list)
return self._item
if isinstance(self._item, list):
return self._item
def text(self) -> str:
assert self._item is not None and isinstance(self._item, str)
return self._item
raise ValueError('Invalid item type')
def get_value(self) -> ValueT:
if self._item is not None:
return self.item().get_value() # type: ignore[no-any-return]
if type(self._data) is not list and self._data is not None:
return cast(ValueT, self._data)
raise ValueError('No value found')
def get_values(self) -> list[ValueT]:
if self._item is not None:
return [i.get_value() for i in self.items()]
assert type(self._data) is list
return cast(list[ValueT], self._data)

View File

@ -1,144 +0,0 @@
import curses
from dataclasses import dataclass
from enum import Enum, auto
from typing import Self
SCROLL_INTERVAL = 10
class STYLE(Enum):
NORMAL = 1
CURSOR_STYLE = 2
MENU_STYLE = 3
HELP = 4
ERROR = 5
class MenuKeys(Enum):
# latin keys
STD_KEYS = frozenset(range(32, 127))
# numbers
NUM_KEYS = frozenset(range(49, 58))
# Menu up: up, k
MENU_UP = frozenset({259, 107})
# Menu down: down, j
MENU_DOWN = frozenset({258, 106})
# Menu left: left, h
MENU_LEFT = frozenset({260, 104})
# Menu right: right, l
MENU_RIGHT = frozenset({261, 108})
# Menu start: home CTRL-a
MENU_START = frozenset({262, 1})
# Menu end: end CTRL-e
MENU_END = frozenset({360, 5})
# Enter
ACCEPT = frozenset({10})
# Selection: space, tab
MULTI_SELECT = frozenset({32, 9})
# Search: /
ENABLE_SEARCH = frozenset({47})
# ESC
ESC = frozenset({27})
# BACKSPACE (search)
BACKSPACE = frozenset({127, 263})
# Help view: ctrl+h
HELP = frozenset({8})
# Scroll up: PGUP
SCROLL_UP = frozenset({339})
# Scroll down: PGDOWN
SCROLL_DOWN = frozenset({338})
@classmethod
def from_ord(cls, key: int) -> list[Self]:
matches: list[Self] = []
for group in cls:
if key in group.value:
matches.append(group)
return matches
@classmethod
def decode(cls, key: int) -> str:
byte_str = curses.keyname(key)
return byte_str.decode('utf-8')
class FrameStyle(Enum):
MAX = auto()
MIN = auto()
@dataclass
class FrameProperties:
header: str
w_frame_style: FrameStyle = FrameStyle.MAX
h_frame_style: FrameStyle = FrameStyle.MAX
@classmethod
def max(cls, header: str) -> Self:
return cls(
header,
FrameStyle.MAX,
FrameStyle.MAX,
)
@classmethod
def min(cls, header: str) -> Self:
return cls(
header,
FrameStyle.MIN,
FrameStyle.MIN,
)
class Orientation(Enum):
VERTICAL = auto()
HORIZONTAL = auto()
class PreviewStyle(Enum):
NONE = auto()
BOTTOM = auto()
RIGHT = auto()
TOP = auto()
# https://www.compart.com/en/unicode/search?q=box+drawings#characters
# https://en.wikipedia.org/wiki/Box-drawing_characters
class Chars:
Horizontal = ''
Vertical = ''
Upper_left = ''
Upper_right = ''
Lower_left = ''
Lower_right = ''
Block = ''
Triangle_up = ''
Triangle_down = ''
Check = '+'
Cross = 'x'
Right_arrow = ''
@dataclass
class ViewportEntry:
text: str
row: int
col: int
style: STYLE
class Alignment(Enum):
LEFT = auto()
CENTER = auto()
@dataclass
class FrameDim:
x_start: int
x_end: int
height: int
def x_delta(self) -> int:
return self.x_end - self.x_start

View File

@ -1,334 +0,0 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from enum import Enum
from functools import cached_property
from typing import Any, ClassVar, Self, override
from archinstall.lib.translationhandler import tr
@dataclass
class MenuItem:
text: str
value: Any | None = None
action: Callable[[Any], Awaitable[Any]] | None = None
enabled: bool = True
read_only: bool = False
mandatory: bool = False
dependencies: list[str | Callable[[], bool]] = field(default_factory=list)
dependencies_not: list[str] = field(default_factory=list)
display_action: Callable[[Any], str] | None = None
preview_action: Callable[[Self], str | None] | None = None
key: str | None = None
_id: str = ''
_yes: ClassVar[Self | None] = None
_no: ClassVar[Self | None] = None
def __post_init__(self) -> None:
if self.key is not None:
self._id = self.key
else:
self._id = str(id(self))
@override
def __hash__(self) -> int:
return hash(self._id)
def get_id(self) -> str:
return self._id
def get_value(self) -> Any:
assert self.value is not None
return self.value
@classmethod
def yes(cls, action: Callable[[Any], Any] | None = None) -> Self:
if cls._yes is None:
cls._yes = cls(tr('Yes'), value=True, key='yes', action=action)
return cls._yes
@classmethod
def no(cls, action: Callable[[Any], Any] | None = None) -> Self:
if cls._no is None:
cls._no = cls(tr('No'), value=False, key='no', action=action)
return cls._no
def is_empty(self) -> bool:
return self.text == '' or self.text is None
def has_value(self) -> bool:
if self.value is None:
return False
elif isinstance(self.value, list) and len(self.value) == 0:
return False
elif isinstance(self.value, dict) and len(self.value) == 0:
return False
else:
return True
def get_display_value(self) -> str | None:
if self.display_action is not None:
return self.display_action(self.value)
return None
class MenuItemGroup:
def __init__(
self,
menu_items: list[MenuItem],
focus_item: MenuItem | None = None,
default_item: MenuItem | None = None,
sort_items: bool = False,
sort_case_sensitive: bool = True,
checkmarks: bool = False,
) -> None:
if len(menu_items) < 1:
raise ValueError('Menu must have at least one item')
if sort_items:
if sort_case_sensitive:
menu_items = sorted(menu_items, key=lambda x: x.text)
else:
menu_items = sorted(menu_items, key=lambda x: x.text.lower())
self._filter_pattern: str = ''
self._checkmarks: bool = checkmarks
self._menu_items: list[MenuItem] = menu_items
self.focus_item: MenuItem | None = focus_item
self.selected_items: list[MenuItem] = []
self.default_item: MenuItem | None = default_item
if not focus_item:
self.focus_first()
if self.focus_item not in self.items:
raise ValueError(f'Selected item not in menu: {self.focus_item}')
@classmethod
def from_objects(cls, items: list[Any]) -> Self:
items = [MenuItem(str(id(item)), value=item) for item in items]
return cls(items)
def add_item(self, item: MenuItem) -> None:
self._menu_items.append(item)
del self.items # resetting the cache
def find_by_id(self, item_id: str) -> MenuItem:
for item in self._menu_items:
if item.get_id() == item_id:
return item
raise ValueError(f'No item found for id: {item_id}')
def find_by_key(self, key: str) -> MenuItem:
for item in self._menu_items:
if item.key == key:
return item
raise ValueError(f'No item found for key: {key}')
def get_enabled_items(self) -> list[MenuItem]:
return [it for it in self.items if self.is_enabled(it)]
@classmethod
def yes_no(cls) -> Self:
return cls(
[MenuItem.yes(), MenuItem.no()],
sort_items=True,
)
@classmethod
def from_enum(
cls,
enum_cls: type[Enum],
sort_items: bool = False,
preset: Enum | None = None,
) -> Self:
items = [MenuItem(elem.value, value=elem) for elem in enum_cls]
group = cls(items, sort_items=sort_items)
if preset is not None:
group.set_selected_by_value(preset)
return group
def set_preview_for_all(self, action: Callable[[Any], str | None]) -> None:
for item in self.items:
item.preview_action = action
def set_focus_by_value(self, value: Any) -> None:
for item in self._menu_items:
if item.value == value:
self.focus_item = item
break
def set_default_by_value(self, value: Any) -> None:
for item in self._menu_items:
if item.value == value:
self.default_item = item
break
def set_selected_by_value(self, values: Any | list[Any] | None) -> None:
if values is None:
return
if not isinstance(values, list):
values = [values]
for item in self._menu_items:
if item.value in values:
self.selected_items.append(item)
if values:
self.set_focus_by_value(values[0])
def get_focused_index(self) -> int | None:
items = self.get_enabled_items()
if self.focus_item and items:
try:
return items.index(self.focus_item)
except ValueError:
# on large menus (15k+) when filtering very quickly
# the index search is too slow while the items are reduced
# by the filter and it will blow up as it cannot find the
# focus item
pass
return None
@cached_property
def _max_items_text_width(self) -> int:
return max([len(item.text) for item in self._menu_items])
def _default_suffix(self, item: MenuItem) -> str:
if self.default_item == item:
return tr(' (default)')
return ''
def set_action_for_all(self, action: Callable[[Any], Any]) -> None:
for item in self.items:
item.action = action
@cached_property
def items(self) -> list[MenuItem]:
pattern = self._filter_pattern.lower()
items = filter(lambda item: item.is_empty() or pattern in item.text.lower(), self._menu_items)
l_items = sorted(items, key=self._items_score)
return l_items
def _items_score(self, item: MenuItem) -> int:
pattern = self._filter_pattern.lower()
if item.text.lower().startswith(pattern):
return 0
return 1
def set_filter_pattern(self, pattern: str) -> None:
self._filter_pattern = pattern
del self.items # resetting the cache
self.focus_first()
def focus_index(self, index: int) -> None:
enabled = self.get_enabled_items()
self.focus_item = enabled[index]
def focus_first(self) -> None:
if len(self.items) == 0:
return
first_item: MenuItem | None = self.items[0]
if first_item and not self._is_selectable(first_item):
first_item = self._find_next_selectable_item(self.items, first_item, 1)
if first_item is not None:
self.focus_item = first_item
def focus_last(self) -> None:
if len(self.items) == 0:
return
last_item: MenuItem | None = self.items[-1]
if last_item and not self._is_selectable(last_item):
last_item = self._find_next_selectable_item(self.items, last_item, -1)
if last_item is not None:
self.focus_item = last_item
def focus_prev(self, skip_empty: bool = True) -> None:
# e.g. when filter shows no items
if self.focus_item is None:
return
item = self._find_next_selectable_item(self.items, self.focus_item, -1)
if item is not None:
self.focus_item = item
def focus_next(self, skip_not_enabled: bool = True) -> None:
# e.g. when filter shows no items
if self.focus_item is None:
return
item = self._find_next_selectable_item(self.items, self.focus_item, 1)
if item is not None:
self.focus_item = item
def _find_next_selectable_item(
self,
items: list[MenuItem],
start_item: MenuItem,
direction: int,
) -> MenuItem | None:
start_index = self.items.index(start_item)
n = len(items)
current_index = start_index
for _ in range(n):
current_index = (current_index + direction) % n
if self._is_selectable(items[current_index]):
return items[current_index]
return None
def max_item_width(self) -> int:
spaces = [len(str(it.text)) for it in self.items]
if spaces:
return max(spaces)
return 0
def _is_selectable(self, item: MenuItem) -> bool:
if item.is_empty():
return False
elif item.read_only:
return False
return self.is_enabled(item)
def is_enabled(self, item: MenuItem) -> bool:
if not item.enabled:
return False
for dep in item.dependencies:
if isinstance(dep, str):
item = self.find_by_key(dep)
if not item.value or not self.is_enabled(item):
return False
else:
return dep()
for dep_not in item.dependencies_not:
item = self.find_by_key(dep_not)
if item.value is not None:
return False
return True

View File

@ -1,71 +0,0 @@
from dataclasses import dataclass
from enum import Enum, auto
from typing import Self, cast
from archinstall.tui.ui.menu_item import MenuItem
class ResultType(Enum):
Selection = auto()
Skip = auto()
Reset = auto()
@dataclass
class Result[ValueT]:
type_: ResultType
_data: ValueT | list[ValueT] | None = None
_item: MenuItem | list[MenuItem] | None = None
@classmethod
def true(cls) -> Self:
return cls(ResultType.Selection, _data=True) # type: ignore[arg-type]
@classmethod
def false(cls) -> Self:
return cls(ResultType.Selection, _data=False) # type: ignore[arg-type]
@classmethod
def reset(cls) -> Self:
return cls(ResultType.Reset)
@classmethod
def selection(cls, value: ValueT | list[ValueT] | None) -> Self:
return cls(ResultType.Selection, _data=value)
@classmethod
def skip(cls) -> Self:
return cls(ResultType.Skip)
def has_data(self) -> bool:
return self._data is not None
def has_value(self) -> bool:
return self._item is not None
def item(self) -> MenuItem:
if isinstance(self._item, list) or self._item is None:
raise ValueError('Invalid item type')
return self._item
def items(self) -> list[MenuItem]:
if isinstance(self._item, list):
return self._item
raise ValueError('Invalid item type')
def get_value(self) -> ValueT:
if self._item is not None:
return self.item().get_value() # type: ignore[no-any-return]
if type(self._data) is not list and self._data is not None:
return cast(ValueT, self._data)
raise ValueError('No value found')
def get_values(self) -> list[ValueT]:
if self._item is not None:
return [i.get_value() for i in self.items()]
assert type(self._data) is list
return cast(list[ValueT], self._data)

View File

@ -22,7 +22,7 @@ Restarting ``systemd-timesyncd.service`` might work but most often you need to c
Waiting for Arch Linux keyring sync (archlinux-keyring-wkd-sync) to complete. `#2679`_
------------------------------
The ``archlinux-keyring-wkd-sync.service`` or ``archlinux-keyring-wkd-sync.timer`` can hang "indefinitely" some times.
The ``archlinux-keyring-wkd-sync.service`` or ``archlinux-keyring-wkd-sync.timer`` can hang "indefinitely" sometimes.
This is usually due to an inability to reach the key servers, or a slow connection towards key servers.
The script ``/usr/bin/archlinux-keyring-wkd-sync`` can be run manually, to verify if it's executing slowly or not.
@ -65,7 +65,7 @@ ARM, 32bit and other CPU types error out `#1686`_, `#2185`_
This is a bit of a catch-all known issue.
Officially `x86_64`_ is only supported by Arch Linux.
Hence little effort have been put into supporting other platforms.
Hence little effort has been put into supporting other platforms.
In theory, other architectures should work but small quirks might arise.
@ -79,10 +79,10 @@ Missing key-issues tend to be that the `archlinux-keyring`_ package is out of da
There is an attempt from upstream to fix this issue, and it's the `archlinux-keyring-wkd-sync.service`_
The service starts almost immediately during boot, and if network is not configured in time — the service will fail.
Subsequently the ``archinstall`` run might operate on a old keyring despite there being an update service for this.
Subsequently the ``archinstall`` run might operate on an old keyring despite there being an update service for this.
There is really no way to reliably over time work around this issue in ``archinstall``.
Instead, efforts to the upstream service should be considered the way forward. And/or keys not expiring between a sane amount of ISO's.
Instead, efforts to the upstream service should be considered the way forward. And/or keys not expiring between a sane amount of ISOs.
.. note::

View File

@ -20,8 +20,8 @@ classifiers = [
dependencies = [
"pyparted==3.13.0",
"pydantic==2.12.5",
"cryptography==46.0.7",
"textual==8.2.4",
"cryptography==47.0.0",
"textual==8.2.5",
"markdown-it-py==4.0.0",
"linkify-it-py==2.1.0",
]
@ -34,10 +34,10 @@ Source = "https://github.com/archlinux/archinstall"
[project.optional-dependencies]
log = ["systemd_python==235"]
dev = [
"mypy==1.20.1",
"mypy==1.20.2",
"flake8==7.3.0",
"pre-commit==4.5.1",
"ruff==0.15.11",
"pre-commit==4.6.0",
"ruff==0.15.12",
"pylint==4.0.5",
"pytest==9.0.3",
]
@ -172,6 +172,23 @@ score = false
[tool.pylint.variables]
init-import = true
[tool.pyrefly]
python-version = "3.14"
replace-imports-with-any = [
"parted", # pyparted doesn't have type hints
]
[tool.pyrefly.errors]
# Enable some additional rules that are disabled by default
implicit-abstract-class = true
missing-override-decorator = true
missing-source = true
not-required-key-access = true
open-unpacking = true
unannotated-parameter = true
unused-ignore = true
[tool.ruff]
target-version = "py314"
line-length = 160