Move locales and cleanup menu (#1814)

* Cleanup imports and unused code

* Cleanup imports and unused code

* Update build check

* Keep deprecation exception

* Simplify logging

* Move locale into new sub-menu

---------

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
This commit is contained in:
Daniel Girtler 2023-06-05 18:02:49 +10:00 committed by GitHub
parent 5276d95339
commit 06eadb31d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 374 additions and 187 deletions

View File

@ -213,8 +213,7 @@ def load_config():
""" """
from .lib.models import NetworkConfiguration from .lib.models import NetworkConfiguration
arguments.setdefault('sys-language', 'en_US') arguments['locale_config'] = locale.LocaleConfiguration.parse_arg(arguments)
arguments.setdefault('sys-encoding', 'utf-8')
if (archinstall_lang := arguments.get('archinstall-language', None)) is not None: if (archinstall_lang := arguments.get('archinstall-language', None)) is not None:
arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang) arguments['archinstall-language'] = TranslationHandler().get_language_by_name(archinstall_lang)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from enum import Enum, auto from enum import Enum, auto
from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar from typing import List, Optional, Any, Dict, TYPE_CHECKING, TypeVar
from archinstall.lib.output import FormattedOutput from archinstall.lib.utils.util import format_cols
if TYPE_CHECKING: if TYPE_CHECKING:
from archinstall.lib.installer import Installer from archinstall.lib.installer import Installer
@ -185,17 +185,6 @@ class Profile:
return None return None
def packages_text(self) -> str: def packages_text(self) -> str:
text = str(_('Installed packages')) + ':\n' header = str(_('Installed packages'))
output = format_cols(self.packages, header)
nr_packages = len(self.packages) return output
if nr_packages <= 5:
col = 1
elif nr_packages <= 10:
col = 2
elif nr_packages <= 15:
col = 3
else:
col = 4
text += FormattedOutput.as_columns(self.packages, col)
return text

View File

@ -4,6 +4,7 @@ from typing import Any, List, Optional, Union, Dict, TYPE_CHECKING
from . import disk from . import disk
from .general import secret from .general import secret
from .locale.locale_menu import LocaleConfiguration, LocaleMenu
from .menu import Selector, AbstractMenu from .menu import Selector, AbstractMenu
from .mirrors import MirrorConfiguration, MirrorMenu from .mirrors import MirrorConfiguration, MirrorMenu
from .models import NetworkConfiguration from .models import NetworkConfiguration
@ -24,9 +25,7 @@ from .interactions import ask_to_configure_network
from .interactions import get_password, ask_for_a_timezone from .interactions import get_password, ask_for_a_timezone
from .interactions import select_additional_repositories from .interactions import select_additional_repositories
from .interactions import select_kernel from .interactions import select_kernel
from .interactions import select_language from .utils.util import format_cols
from .interactions import select_locale_enc
from .interactions import select_locale_lang
from .interactions import ask_ntp from .interactions import ask_ntp
from .interactions.disk_conf import select_disk_config from .interactions.disk_conf import select_disk_config
@ -36,6 +35,7 @@ if TYPE_CHECKING:
class GlobalMenu(AbstractMenu): class GlobalMenu(AbstractMenu):
def __init__(self, data_store: Dict[str, Any]): def __init__(self, data_store: Dict[str, Any]):
self._defined_text = str(_('Defined'))
super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3) super().__init__(data_store=data_store, auto_cursor=True, preview_size=0.3)
def setup_selection_menu_options(self): def setup_selection_menu_options(self):
@ -46,28 +46,19 @@ class GlobalMenu(AbstractMenu):
lambda x: self._select_archinstall_language(x), lambda x: self._select_archinstall_language(x),
display_func=lambda x: x.display_name, display_func=lambda x: x.display_name,
default=self.translation_handler.get_language_by_abbr('en')) default=self.translation_handler.get_language_by_abbr('en'))
self._menu_options['keyboard-layout'] = \ self._menu_options['locale_config'] = \
Selector( Selector(
_('Keyboard layout'), _('Locales'),
lambda preset: select_language(preset), lambda preset: self._locale_selection(preset),
default='us') preview_func=self._prev_locale,
display_func=lambda x: self._defined_text if x else '')
self._menu_options['mirror_config'] = \ self._menu_options['mirror_config'] = \
Selector( Selector(
_('Mirrors'), _('Mirrors'),
lambda preset: self._mirror_configuration(preset), lambda preset: self._mirror_configuration(preset),
display_func=lambda x: str(_('Defined')) if x else '', display_func=lambda x: self._defined_text if x else '',
preview_func=self._prev_mirror_config preview_func=self._prev_mirror_config
) )
self._menu_options['sys-language'] = \
Selector(
_('Locale language'),
lambda preset: select_locale_lang(preset),
default='en_US')
self._menu_options['sys-encoding'] = \
Selector(
_('Locale encoding'),
lambda preset: select_locale_enc(preset),
default='UTF-8')
self._menu_options['disk_config'] = \ self._menu_options['disk_config'] = \
Selector( Selector(
_('Disk configuration'), _('Disk configuration'),
@ -103,32 +94,32 @@ class GlobalMenu(AbstractMenu):
Selector( Selector(
_('Root password'), _('Root password'),
lambda preset:self._set_root_password(), lambda preset:self._set_root_password(),
display_func=lambda x: secret(x) if x else 'None') display_func=lambda x: secret(x) if x else '')
self._menu_options['!users'] = \ self._menu_options['!users'] = \
Selector( Selector(
_('User account'), _('User account'),
lambda x: self._create_user_account(x), lambda x: self._create_user_account(x),
default=[], default=[],
display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None, display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else '',
preview_func=self._prev_users) preview_func=self._prev_users)
self._menu_options['profile_config'] = \ self._menu_options['profile_config'] = \
Selector( Selector(
_('Profile'), _('Profile'),
lambda preset: self._select_profile(preset), lambda preset: self._select_profile(preset),
display_func=lambda x: x.profile.name if x else 'None', display_func=lambda x: x.profile.name if x else '',
preview_func=self._prev_profile preview_func=self._prev_profile
) )
self._menu_options['audio'] = \ self._menu_options['audio'] = \
Selector( Selector(
_('Audio'), _('Audio'),
lambda preset: self._select_audio(preset), lambda preset: self._select_audio(preset),
display_func=lambda x: x if x else 'None', display_func=lambda x: x if x else '',
default=None default=None
) )
self._menu_options['parallel downloads'] = \ self._menu_options['parallel downloads'] = \
Selector( Selector(
_('Parallel Downloads'), _('Parallel Downloads'),
add_number_of_parrallel_downloads, lambda preset: add_number_of_parrallel_downloads(preset),
display_func=lambda x: x if x else '0', display_func=lambda x: x if x else '0',
default=0 default=0
) )
@ -141,19 +132,20 @@ class GlobalMenu(AbstractMenu):
self._menu_options['packages'] = \ self._menu_options['packages'] = \
Selector( Selector(
_('Additional packages'), _('Additional packages'),
# lambda x: ask_additional_packages_to_install(storage['arguments'].get('packages', None)), lambda preset: ask_additional_packages_to_install(preset),
ask_additional_packages_to_install, display_func=lambda x: self._defined_text if x else '',
preview_func=self._prev_additional_pkgs,
default=[]) default=[])
self._menu_options['additional-repositories'] = \ self._menu_options['additional-repositories'] = \
Selector( Selector(
_('Optional repositories'), _('Optional repositories'),
select_additional_repositories, lambda preset: select_additional_repositories(preset),
display_func=lambda x: ', '.join(x) if x else None, display_func=lambda x: ', '.join(x) if x else None,
default=[]) default=[])
self._menu_options['nic'] = \ self._menu_options['nic'] = \
Selector( Selector(
_('Network configuration'), _('Network configuration'),
ask_to_configure_network, lambda preset: ask_to_configure_network(preset),
display_func=lambda x: self._display_network_conf(x), display_func=lambda x: self._display_network_conf(x),
preview_func=self._prev_network_config, preview_func=self._prev_network_config,
default={}) default={})
@ -177,12 +169,37 @@ class GlobalMenu(AbstractMenu):
self._menu_options['install'] = \ self._menu_options['install'] = \
Selector( Selector(
self._install_text(), self._install_text(),
exec_func=lambda n,v: True if len(self._missing_configs()) == 0 else False, exec_func=lambda n, v: True if len(self._missing_configs()) == 0 else False,
preview_func=self._prev_install_missing_config, preview_func=self._prev_install_missing_config,
no_store=True) no_store=True)
self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1)) self._menu_options['abort'] = Selector(_('Abort'), exec_func=lambda n,v:exit(1))
def _missing_configs(self) -> List[str]:
def check(s):
return self._menu_options.get(s).has_selection()
def has_superuser() -> bool:
sel = self._menu_options['!users']
if sel.current_selection:
return any([u.sudo for u in sel.current_selection])
return False
mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
missing = set()
for key, selector in mandatory_fields.items():
if key in ['!root-password', '!users']:
if not check('!root-password') and not has_superuser():
missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
)
elif key == 'disk_config':
if not check('disk_config'):
missing.add(self._menu_options['disk_config'].description)
return list(missing)
def _update_install_text(self, name: str, value: str): def _update_install_text(self, name: str, value: str):
text = self._install_text() text = self._install_text()
self._menu_options['install'].update_description(text) self._menu_options['install'].update_description(text)
@ -216,6 +233,21 @@ class GlobalMenu(AbstractMenu):
disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run() disk_encryption = disk.DiskEncryptionMenu(mods, data_store, preset=preset).run()
return disk_encryption return disk_encryption
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
data_store: Dict[str, Any] = {}
locale_config = LocaleMenu(data_store, preset).run()
return locale_config
def _prev_locale(self) -> Optional[str]:
selector = self._menu_options['locale_config']
if selector.has_selection():
config: LocaleConfiguration = selector.current_selection # type: ignore
output = '{}: {}\n'.format(str(_('Keyboard layout')), config.kb_layout)
output += '{}: {}\n'.format(str(_('Locale language')), config.sys_lang)
output += '{}: {}'.format(str(_('Locale encoding')), config.sys_enc)
return output
return None
def _prev_network_config(self) -> Optional[str]: def _prev_network_config(self) -> Optional[str]:
selector = self._menu_options['nic'] selector = self._menu_options['nic']
if selector.has_selection(): if selector.has_selection():
@ -224,6 +256,13 @@ class GlobalMenu(AbstractMenu):
return FormattedOutput.as_table(ifaces) return FormattedOutput.as_table(ifaces)
return None return None
def _prev_additional_pkgs(self):
selector = self._menu_options['packages']
if selector.has_selection():
packages: List[str] = selector.current_selection
return format_cols(packages, None)
return None
def _prev_disk_layouts(self) -> Optional[str]: def _prev_disk_layouts(self) -> Optional[str]:
selector = self._menu_options['disk_config'] selector = self._menu_options['disk_config']
disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection disk_layout_conf: Optional[disk.DiskLayoutConfiguration] = selector.current_selection

View File

@ -3,9 +3,9 @@ from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, List from typing import Optional, Dict, List
from .exceptions import SysCallError
from .general import SysCommand from .general import SysCommand
from .networking import list_interfaces, enrich_iface_types from .networking import list_interfaces, enrich_iface_types
from .exceptions import SysCallError
from .output import debug from .output import debug
AVAILABLE_GFX_DRIVERS = { AVAILABLE_GFX_DRIVERS = {

View File

@ -12,6 +12,7 @@ from . import disk
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
from .general import SysCommand from .general import SysCommand
from .hardware import SysInfo from .hardware import SysInfo
from .locale import LocaleConfiguration
from .locale import verify_keyboard_layout, verify_x11_keyboard_layout from .locale import verify_keyboard_layout, verify_x11_keyboard_layout
from .luks import Luks2 from .luks import Luks2
from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors from .mirrors import use_mirrors, MirrorConfiguration, add_custom_mirrors
@ -457,37 +458,36 @@ class Installer:
with open(f'{self.target}/etc/hostname', 'w') as fh: with open(f'{self.target}/etc/hostname', 'w') as fh:
fh.write(hostname + '\n') fh.write(hostname + '\n')
def set_locale(self, locale :str, encoding :str = 'UTF-8', *args :str, **kwargs :str) -> bool: def set_locale(self, locale_config: LocaleConfiguration):
if not len(locale):
return True
modifier = '' modifier = ''
lang = locale_config.sys_lang
encoding = locale_config.sys_enc
# This is a temporary patch to fix #1200 # This is a temporary patch to fix #1200
if '.' in locale: if '.' in locale_config.sys_lang:
locale, potential_encoding = locale.split('.', 1) lang, potential_encoding = locale_config.sys_lang.split('.', 1)
# Override encoding if encoding is set to the default parameter # Override encoding if encoding is set to the default parameter
# and the "found" encoding differs. # and the "found" encoding differs.
if encoding == 'UTF-8' and encoding != potential_encoding: if locale_config.sys_enc == 'UTF-8' and locale_config.sys_enc != potential_encoding:
encoding = potential_encoding encoding = potential_encoding
# Make sure we extract the modifier, that way we can put it in if needed. # Make sure we extract the modifier, that way we can put it in if needed.
if '@' in locale: if '@' in locale_config.sys_lang:
locale, modifier = locale.split('@', 1) lang, modifier = locale_config.sys_lang.split('@', 1)
modifier = f"@{modifier}" modifier = f"@{modifier}"
# - End patch # - End patch
with open(f'{self.target}/etc/locale.gen', 'a') as fh: with open(f'{self.target}/etc/locale.gen', 'a') as fh:
fh.write(f'{locale}.{encoding}{modifier} {encoding}\n') fh.write(f'{lang}.{encoding}{modifier} {encoding}\n')
with open(f'{self.target}/etc/locale.conf', 'w') as fh: with open(f'{self.target}/etc/locale.conf', 'w') as fh:
fh.write(f'LANG={locale}.{encoding}{modifier}\n') fh.write(f'LANG={lang}.{encoding}{modifier}\n')
try: try:
SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen') SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen')
return True except SysCallError as e:
except SysCallError: error(f'Failed to run locale-gen on target: {e}')
return False
def set_timezone(self, zone :str, *args :str, **kwargs :str) -> bool: def set_timezone(self, zone :str, *args :str, **kwargs :str) -> bool:
if not zone: if not zone:
@ -620,7 +620,7 @@ class Installer:
return True return True
def mkinitcpio(self, *flags :str) -> bool: def mkinitcpio(self, flags: List[str], locale_config: LocaleConfiguration) -> bool:
for plugin in plugins.values(): for plugin in plugins.values():
if hasattr(plugin, 'on_mkinitcpio'): if hasattr(plugin, 'on_mkinitcpio'):
# Allow plugins to override the usage of mkinitcpio altogether. # Allow plugins to override the usage of mkinitcpio altogether.
@ -630,7 +630,7 @@ class Installer:
# mkinitcpio will error out if there's no vconsole. # mkinitcpio will error out if there's no vconsole.
if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False: if (vconsole := Path(f"{self.target}/etc/vconsole.conf")).exists() is False:
with vconsole.open('w') as fh: with vconsole.open('w') as fh:
fh.write(f"KEYMAP={storage['arguments']['keyboard-layout']}\n") fh.write(f"KEYMAP={locale_config.kb_layout}\n")
with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit:
mkinit.write(f"MODULES=({' '.join(self.modules)})\n") mkinit.write(f"MODULES=({' '.join(self.modules)})\n")
@ -658,7 +658,7 @@ class Installer:
testing: bool = False, testing: bool = False,
multilib: bool = False, multilib: bool = False,
hostname: str = 'archinstall', hostname: str = 'archinstall',
locales: List[str] = ['en_US.UTF-8 UTF-8'] locale_config: LocaleConfiguration = LocaleConfiguration.default()
): ):
for mod in self._disk_config.device_modifications: for mod in self._disk_config.device_modifications:
for part in mod.partitions: for part in mod.partitions:
@ -734,12 +734,12 @@ class Installer:
# sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime') # sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime')
# sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime') # sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime')
self.set_hostname(hostname) self.set_hostname(hostname)
self.set_locale(*locales[0].split()) self.set_locale(locale_config)
# TODO: Use python functions for this # TODO: Use python functions for this
SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') SysCommand(f'/usr/bin/arch-chroot {self.target} chmod 700 /root')
self.mkinitcpio('-P') self.mkinitcpio(['-P'], locale_config)
self.helper_flags['base'] = True self.helper_flags['base'] = True

View File

@ -1,4 +1,3 @@
from .locale_conf import select_locale_lang, select_locale_enc
from .manage_users_conf import UserList, ask_for_additional_users from .manage_users_conf import UserList, ask_for_additional_users
from .network_conf import ManualNetworkConfig, ask_to_configure_network from .network_conf import ManualNetworkConfig, ask_to_configure_network
from .utils import get_password from .utils import get_password
@ -10,7 +9,7 @@ from .disk_conf import (
) )
from .general_conf import ( from .general_conf import (
ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection, select_language, ask_ntp, ask_hostname, ask_for_a_timezone, ask_for_audio_selection,
select_archinstall_language, ask_additional_packages_to_install, select_archinstall_language, ask_additional_packages_to_install,
add_number_of_parrallel_downloads, select_additional_repositories add_number_of_parrallel_downloads, select_additional_repositories
) )

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import pathlib import pathlib
from typing import List, Any, Optional, TYPE_CHECKING from typing import List, Any, Optional, TYPE_CHECKING
from ..locale import list_keyboard_languages, list_timezones from ..locale import list_timezones, list_keyboard_languages
from ..menu import MenuSelectionType, Menu, TextInput from ..menu import MenuSelectionType, Menu, TextInput
from ..output import warn from ..output import warn
from ..packages.packages import validate_package_list from ..packages.packages import validate_package_list
@ -119,18 +119,18 @@ def select_archinstall_language(languages: List[Language], preset: Language) ->
raise ValueError('Language selection not handled') raise ValueError('Language selection not handled')
def ask_additional_packages_to_install(pre_set_packages: List[str] = []) -> List[str]: def ask_additional_packages_to_install(preset: List[str] = []) -> List[str]:
# Additional packages (with some light weight error handling for invalid package names) # Additional packages (with some light weight error handling for invalid package names)
print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) print(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.'))
print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) print(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.'))
def read_packages(already_defined: list = []) -> list: def read_packages(p: List = []) -> list:
display = ' '.join(already_defined) display = ' '.join(p)
input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip() input_packages = TextInput(_('Write additional packages to install (space separated, leave blank to skip): '), display).run().strip()
return input_packages.split() if input_packages else [] return input_packages.split() if input_packages else []
pre_set_packages = pre_set_packages if pre_set_packages else [] preset = preset if preset else []
packages = read_packages(pre_set_packages) packages = read_packages(preset)
if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']: if not storage['arguments']['offline'] and not storage['arguments']['no_pkg_lookups']:
while True: while True:

View File

@ -1,43 +0,0 @@
from typing import Any, TYPE_CHECKING, Optional
from ..locale import list_locales
from ..menu import Menu, MenuSelectionType
if TYPE_CHECKING:
_: Any
def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales])
choice = Menu(
_('Choose which locale language to use'),
list(locale_lang),
sort=True,
preset_values=preset
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales])
choice = Menu(
_('Choose which locale encoding to use'),
list(locale_enc),
sort=True,
preset_values=preset
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None

View File

@ -29,14 +29,14 @@ def select_kernel(preset: List[str] = []) -> List[str]:
sort=True, sort=True,
multi=True, multi=True,
preset_values=preset, preset_values=preset,
allow_reset=True,
allow_reset_warning_msg=warning allow_reset_warning_msg=warning
).run() ).run()
match choice.type_: match choice.type_:
case MenuSelectionType.Skip: return preset case MenuSelectionType.Skip: return preset
case MenuSelectionType.Reset: return [] case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Selection: return choice.value # type: ignore
return []
def ask_for_bootloader(preset: Bootloader) -> Bootloader: def ask_for_bootloader(preset: Bootloader) -> Bootloader:

View File

@ -0,0 +1,6 @@
from .locale_menu import LocaleConfiguration
from .locale import (
list_keyboard_languages, list_locales, list_x11_keyboard_languages,
verify_keyboard_layout, verify_x11_keyboard_layout, set_kb_layout,
list_timezones
)

View File

@ -0,0 +1,68 @@
from typing import Iterator, List
from ..exceptions import ServiceException, SysCallError
from ..general import SysCommand
from ..output import error
def list_keyboard_languages() -> Iterator[str]:
for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip()
def list_locales() -> List[str]:
with open('/etc/locale.gen', 'r') as fp:
locales = []
# before the list of locales begins there's an empty line with a '#' in front
# so we'll collect the localels from bottom up and halt when we're donw
entries = fp.readlines()
entries.reverse()
for entry in entries:
text = entry.replace('#', '').strip()
if text == '':
break
locales.append(text)
locales.reverse()
return locales
def list_x11_keyboard_languages() -> Iterator[str]:
for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip()
def verify_keyboard_layout(layout :str) -> bool:
for language in list_keyboard_languages():
if layout.lower() == language.lower():
return True
return False
def verify_x11_keyboard_layout(layout :str) -> bool:
for language in list_x11_keyboard_languages():
if layout.lower() == language.lower():
return True
return False
def set_kb_layout(locale :str) -> bool:
if len(locale.strip()):
if not verify_keyboard_layout(locale):
error(f"Invalid keyboard locale specified: {locale}")
return False
try:
SysCommand(f'localectl set-keymap {locale}')
except SysCallError as err:
raise ServiceException(f"Unable to set locale '{locale}' for console: {err}")
return True
return False
def list_timezones() -> Iterator[str]:
for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip()

View File

@ -0,0 +1,155 @@
from dataclasses import dataclass
from typing import Dict, Any, TYPE_CHECKING, Optional
from .locale import set_kb_layout, list_keyboard_languages, list_locales
from ..menu import Selector, AbstractSubMenu, MenuSelectionType, Menu
if TYPE_CHECKING:
_: Any
@dataclass
class LocaleConfiguration:
kb_layout: str
sys_lang: str
sys_enc: str
@staticmethod
def default() -> 'LocaleConfiguration':
return LocaleConfiguration('us', 'en_US', 'UTF-8')
def json(self) -> Dict[str, str]:
return {
'kb_layout': self.kb_layout,
'sys_lang': self.sys_lang,
'sys_enc': self.sys_enc
}
@classmethod
def _load_config(cls, config: 'LocaleConfiguration', args: Dict[str, Any]) -> 'LocaleConfiguration':
if 'sys_lang' in args:
config.sys_lang = args['sys_lang']
if 'sys_enc' in args:
config.sys_enc = args['sys_enc']
if 'kb_layout' in args:
config.kb_layout = args['kb_layout']
return config
@classmethod
def parse_arg(cls, args: Dict[str, Any]) -> 'LocaleConfiguration':
default = cls.default()
if 'locale_config' in args:
default = cls._load_config(default, args['locale_config'])
else:
default = cls._load_config(default, args)
return default
class LocaleMenu(AbstractSubMenu):
def __init__(
self,
data_store: Dict[str, Any],
locele_conf: LocaleConfiguration
):
self._preset = locele_conf
super().__init__(data_store=data_store)
def setup_selection_menu_options(self):
self._menu_options['keyboard-layout'] = \
Selector(
_('Keyboard layout'),
lambda preset: self._select_kb_layout(preset),
default='us',
enabled=True)
self._menu_options['sys-language'] = \
Selector(
_('Locale language'),
lambda preset: select_locale_lang(preset),
default='en_US',
enabled=True)
self._menu_options['sys-encoding'] = \
Selector(
_('Locale encoding'),
lambda preset: select_locale_enc(preset),
default='UTF-8',
enabled=True)
def run(self, allow_reset: bool = True) -> LocaleConfiguration:
super().run(allow_reset=allow_reset)
return LocaleConfiguration(
self._data_store['keyboard-layout'],
self._data_store['sys-language'],
self._data_store['sys-encoding']
)
def _select_kb_layout(self, preset: Optional[str]) -> Optional[str]:
kb_lang = select_kb_layout(preset)
if kb_lang:
set_kb_layout(kb_lang)
return kb_lang
def select_locale_lang(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales])
choice = Menu(
_('Choose which locale language to use'),
list(locale_lang),
sort=True,
preset_values=preset
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
def select_locale_enc(preset: Optional[str] = None) -> Optional[str]:
locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales])
choice = Menu(
_('Choose which locale encoding to use'),
list(locale_enc),
sort=True,
preset_values=preset
).run()
match choice.type_:
case MenuSelectionType.Selection: return choice.single_value
case MenuSelectionType.Skip: return preset
return None
def select_kb_layout(preset: Optional[str] = None) -> Optional[str]:
"""
Asks the user to select a language
Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
:return: The language/dictionary key of the selected language
:rtype: str
"""
kb_lang = list_keyboard_languages()
# sort alphabetically and then by length
sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len)
choice = Menu(
_('Select keyboard layout'),
sorted_kb_lang,
preset_values=preset,
sort=False
).run()
match choice.type_:
case MenuSelectionType.Skip: return preset
case MenuSelectionType.Selection: return choice.single_value
return None

View File

@ -3,7 +3,6 @@ from __future__ import annotations
from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING from typing import Callable, Any, List, Iterator, Tuple, Optional, Dict, TYPE_CHECKING
from .menu import Menu, MenuSelectionType from .menu import Menu, MenuSelectionType
from ..locale import set_keyboard_language
from ..output import error from ..output import error
from ..translationhandler import TranslationHandler, Language from ..translationhandler import TranslationHandler, Language
@ -130,7 +129,7 @@ class Selector:
if current: if current:
padding += 5 padding += 5
description = str(self._description).ljust(padding, ' ') description = str(self._description).ljust(padding, ' ')
current = str(_('set: {}').format(current)) current = current
else: else:
description = self._description description = self._description
current = '' current = ''
@ -243,31 +242,6 @@ class AbstractMenu:
elif selector is not None and selector.has_selection(): elif selector is not None and selector.has_selection():
self._data_store[selector_name] = selector.current_selection self._data_store[selector_name] = selector.current_selection
def _missing_configs(self) -> List[str]:
def check(s):
return self._menu_options.get(s).has_selection()
def has_superuser() -> bool:
sel = self._menu_options['!users']
if sel.current_selection:
return any([u.sudo for u in sel.current_selection])
return False
mandatory_fields = dict(filter(lambda x: x[1].is_mandatory(), self._menu_options.items()))
missing = set()
for key, selector in mandatory_fields.items():
if key in ['!root-password', '!users']:
if not check('!root-password') and not has_superuser():
missing.add(
str(_('Either root-password or at least 1 user with sudo privileges must be specified'))
)
elif key == 'disk_config':
if not check('disk_config'):
missing.add(self._menu_options['disk_config'].description)
return list(missing)
def setup_selection_menu_options(self): def setup_selection_menu_options(self):
""" Define the menu options. """ Define the menu options.
Menu options can be defined here in a subclass or done per program calling self.set_option() Menu options can be defined here in a subclass or done per program calling self.set_option()
@ -328,9 +302,6 @@ class AbstractMenu:
cursor_pos = None cursor_pos = None
while True: while True:
# Before continuing, set the preferred keyboard layout/language in the current terminal.
# This will just help the user with the next following questions.
self._set_kb_language()
enabled_menus = self._menus_to_enable() enabled_menus = self._menus_to_enable()
padding = self._get_menu_text_padding(list(enabled_menus.values())) padding = self._get_menu_text_padding(list(enabled_menus.values()))
@ -425,13 +396,6 @@ class AbstractMenu:
return True return True
def _set_kb_language(self):
""" general for ArchInstall"""
# Before continuing, set the preferred keyboard layout/language in the current terminal.
# This will just help the user with the next following questions.
if self._data_store.get('keyboard-layout', None) and len(self._data_store['keyboard-layout']):
set_keyboard_language(self._data_store['keyboard-layout'])
def _verify_selection_enabled(self, selection_name: str) -> bool: def _verify_selection_enabled(self, selection_name: str) -> bool:
""" general """ """ general """
if selection := self._menu_options.get(selection_name, None): if selection := self._menu_options.get(selection_name, None):

View File

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from typing import Any, TYPE_CHECKING, Optional from typing import Any, TYPE_CHECKING, Optional, List
from ..output import FormattedOutput
from ..output import info from ..output import info
if TYPE_CHECKING: if TYPE_CHECKING:
@ -28,3 +29,23 @@ def is_subpath(first: Path, second: Path):
return True return True
except ValueError: except ValueError:
return False return False
def format_cols(items: List[str], header: Optional[str]) -> str:
if header:
text = f'{header}:\n'
else:
text = ''
nr_items = len(items)
if nr_items <= 5:
col = 1
elif nr_items <= 10:
col = 2
elif nr_items <= 15:
col = 3
else:
col = 4
text += FormattedOutput.as_columns(items, col)
return text

View File

@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING
import archinstall import archinstall
from archinstall import info, debug from archinstall import info, debug
from archinstall import SysInfo from archinstall import SysInfo
from archinstall.lib import locale
from archinstall.lib import disk from archinstall.lib import disk
from archinstall.lib.global_menu import GlobalMenu from archinstall.lib.global_menu import GlobalMenu
from archinstall.default_profiles.applications.pipewire import PipewireProfile from archinstall.default_profiles.applications.pipewire import PipewireProfile
@ -42,14 +43,10 @@ def ask_user_questions():
global_menu.enable('archinstall-language') global_menu.enable('archinstall-language')
global_menu.enable('keyboard-layout')
# Set which region to download packages from during the installation # Set which region to download packages from during the installation
global_menu.enable('mirror_config') global_menu.enable('mirror_config')
global_menu.enable('sys-language') global_menu.enable('locale_config')
global_menu.enable('sys-encoding')
global_menu.enable('disk_config', mandatory=True) global_menu.enable('disk_config', mandatory=True)
@ -76,7 +73,7 @@ def ask_user_questions():
global_menu.enable('audio') global_menu.enable('audio')
# Ask for preferred kernel: # Ask for preferred kernel:
global_menu.enable('kernels') global_menu.enable('kernels', mandatory=True)
global_menu.enable('packages') global_menu.enable('packages')
@ -114,9 +111,7 @@ def perform_installation(mountpoint: Path):
# Retrieve list of additional repositories and set boolean values appropriately # Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}"
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
with Installer( with Installer(
@ -147,7 +142,7 @@ def perform_installation(mountpoint: Path):
testing=enable_testing, testing=enable_testing,
multilib=enable_multilib, multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'), hostname=archinstall.arguments.get('hostname', 'archlinux'),
locales=[locale] locale_config=locale_config
) )
if mirror_config := archinstall.arguments.get('mirror_config', None): if mirror_config := archinstall.arguments.get('mirror_config', None):
@ -210,7 +205,7 @@ def perform_installation(mountpoint: Path):
# This step must be after profile installs to allow profiles_bck to install language pre-requisits. # This step must be after profile installs to allow profiles_bck to install language pre-requisits.
# After which, this step will set the language both for console and x11 if x11 was installed for instance. # After which, this step will set the language both for console and x11 if x11 was installed for instance.
installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) installation.set_keyboard_language(locale_config.kb_layout)
if profile_config := archinstall.arguments.get('profile_config', None): if profile_config := archinstall.arguments.get('profile_config', None):
profile_config.profile.post_install(installation) profile_config.profile.post_install(installation)

View File

@ -8,6 +8,7 @@ from archinstall import SysInfo, info, debug
from archinstall.lib import mirrors from archinstall.lib import mirrors
from archinstall.lib import models from archinstall.lib import models
from archinstall.lib import disk from archinstall.lib import disk
from archinstall.lib import locale
from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.networking import check_mirror_reachable
from archinstall.lib.profile.profiles_handler import profile_handler from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib import menu from archinstall.lib import menu
@ -92,14 +93,14 @@ class SwissMainMenu(GlobalMenu):
match self._execution_mode: match self._execution_mode:
case ExecutionMode.Full | ExecutionMode.Lineal: case ExecutionMode.Full | ExecutionMode.Lineal:
options_list = [ options_list = [
'keyboard-layout', 'mirror_config', 'disk_config', 'mirror_config', 'disk_config',
'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password', 'disk_encryption', 'swap', 'bootloader', 'hostname', '!root-password',
'!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic', '!users', 'profile_config', 'audio', 'kernels', 'packages', 'additional-repositories', 'nic',
'timezone', 'ntp' 'timezone', 'ntp'
] ]
if archinstall.arguments.get('advanced', False): if archinstall.arguments.get('advanced', False):
options_list.extend(['sys-language', 'sys-encoding']) options_list.extend(['locale_config'])
mandatory_list = ['disk_config', 'bootloader', 'hostname'] mandatory_list = ['disk_config', 'bootloader', 'hostname']
case ExecutionMode.Only_HD: case ExecutionMode.Only_HD:
@ -107,7 +108,7 @@ class SwissMainMenu(GlobalMenu):
mandatory_list = ['disk_config'] mandatory_list = ['disk_config']
case ExecutionMode.Only_OS: case ExecutionMode.Only_OS:
options_list = [ options_list = [
'keyboard-layout', 'mirror_config','bootloader', 'hostname', 'mirror_config','bootloader', 'hostname',
'!root-password', '!users', 'profile_config', 'audio', 'kernels', '!root-password', '!users', 'profile_config', 'audio', 'kernels',
'packages', 'additional-repositories', 'nic', 'timezone', 'ntp' 'packages', 'additional-repositories', 'nic', 'timezone', 'ntp'
] ]
@ -115,7 +116,7 @@ class SwissMainMenu(GlobalMenu):
mandatory_list = ['hostname'] mandatory_list = ['hostname']
if archinstall.arguments.get('advanced', False): if archinstall.arguments.get('advanced', False):
options_list += ['sys-language','sys-encoding'] options_list += ['locale_config']
case ExecutionMode.Minimal: case ExecutionMode.Minimal:
pass pass
case _: case _:
@ -176,8 +177,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode):
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}"
with Installer( with Installer(
mountpoint, mountpoint,
@ -206,7 +206,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode):
testing=enable_testing, testing=enable_testing,
multilib=enable_multilib, multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'), hostname=archinstall.arguments.get('hostname', 'archlinux'),
locales=[locale] locale_config=locale_config
) )
if mirror_config := archinstall.arguments.get('mirror_config', None): if mirror_config := archinstall.arguments.get('mirror_config', None):
@ -263,7 +263,7 @@ def perform_installation(mountpoint: Path, exec_mode: ExecutionMode):
# This step must be after profile installs to allow profiles_bck to install language pre-requisits. # This step must be after profile installs to allow profiles_bck to install language pre-requisits.
# After which, this step will set the language both for console and x11 if x11 was installed for instance. # After which, this step will set the language both for console and x11 if x11 was installed for instance.
installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) installation.set_keyboard_language(locale_config.kb_layout)
if profile_config := archinstall.arguments.get('profile_config', None): if profile_config := archinstall.arguments.get('profile_config', None):
profile_config.profile.post_install(installation) profile_config.profile.post_install(installation)

View File

@ -10,6 +10,7 @@ from archinstall.default_profiles.applications.pipewire import PipewireProfile
from archinstall import disk from archinstall import disk
from archinstall import menu from archinstall import menu
from archinstall import models from archinstall import models
from archinstall import locale
from archinstall import info, debug from archinstall import info, debug
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,14 +22,10 @@ def ask_user_questions():
global_menu.enable('archinstall-language') global_menu.enable('archinstall-language')
global_menu.enable('keyboard-layout')
# Set which region to download packages from during the installation # Set which region to download packages from during the installation
global_menu.enable('mirror_config') global_menu.enable('mirror_config')
global_menu.enable('sys-language') global_menu.enable('locale_config')
global_menu.enable('sys-encoding')
global_menu.enable('disk_config', mandatory=True) global_menu.enable('disk_config', mandatory=True)
@ -55,7 +52,7 @@ def ask_user_questions():
global_menu.enable('audio') global_menu.enable('audio')
# Ask for preferred kernel: # Ask for preferred kernel:
global_menu.enable('kernels') global_menu.enable('kernels', mandatory=True)
global_menu.enable('packages') global_menu.enable('packages')
@ -93,9 +90,7 @@ def perform_installation(mountpoint: Path):
# Retrieve list of additional repositories and set boolean values appropriately # Retrieve list of additional repositories and set boolean values appropriately
enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', []) enable_testing = 'testing' in archinstall.arguments.get('additional-repositories', [])
enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', []) enable_multilib = 'multilib' in archinstall.arguments.get('additional-repositories', [])
locale_config: locale.LocaleConfiguration = archinstall.arguments['locale_config']
locale = f"{archinstall.arguments.get('sys-language', 'en_US')} {archinstall.arguments.get('sys-encoding', 'UTF-8').upper()}"
disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None) disk_encryption: disk.DiskEncryption = archinstall.arguments.get('disk_encryption', None)
with Installer( with Installer(
@ -126,7 +121,7 @@ def perform_installation(mountpoint: Path):
testing=enable_testing, testing=enable_testing,
multilib=enable_multilib, multilib=enable_multilib,
hostname=archinstall.arguments.get('hostname', 'archlinux'), hostname=archinstall.arguments.get('hostname', 'archlinux'),
locales=[locale] locale_config=locale_config
) )
if mirror_config := archinstall.arguments.get('mirror_config', None): if mirror_config := archinstall.arguments.get('mirror_config', None):
@ -189,7 +184,7 @@ def perform_installation(mountpoint: Path):
# This step must be after profile installs to allow profiles_bck to install language pre-requisits. # This step must be after profile installs to allow profiles_bck to install language pre-requisits.
# After which, this step will set the language both for console and x11 if x11 was installed for instance. # After which, this step will set the language both for console and x11 if x11 was installed for instance.
installation.set_keyboard_language(archinstall.arguments['keyboard-layout']) installation.set_keyboard_language(locale_config.kb_layout)
if profile_config := archinstall.arguments.get('profile_config', None): if profile_config := archinstall.arguments.get('profile_config', None):
profile_config.profile.post_install(installation) profile_config.profile.post_install(installation)