diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py index c61bc5a15f..6b72acfcca 100644 --- a/archinstall/lib/args.py +++ b/archinstall/lib/args.py @@ -142,6 +142,7 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self: if archinstall_lang := args_config.get('archinstall-language', None): arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang) + translation_handler.activate(arch_config.archinstall_language, set_font=False) if disk_config := args_config.get('disk_config', {}): enc_password = args_config.get('encryption_password', '') diff --git a/archinstall/lib/general/general_menu.py b/archinstall/lib/general/general_menu.py index b6cf9a75ee..a493037afe 100644 --- a/archinstall/lib/general/general_menu.py +++ b/archinstall/lib/general/general_menu.py @@ -106,9 +106,9 @@ async def select_archinstall_language(languages: list[Language], preset: Languag group = MenuItemGroup(items, sort_items=True) group.set_focus_by_value(preset) - title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n' - title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n' - title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n' + title = 'NOTE: Console font will be set automatically for supported languages.\n' + title += 'For other languages, fonts can be found in "/usr/share/kbd/consolefonts"\n' + title += 'and set manually with: setfont \n' result = await Selection[Language]( header=title, diff --git a/archinstall/lib/translationhandler.py b/archinstall/lib/translationhandler.py index c7ce959bec..23afecb311 100644 --- a/archinstall/lib/translationhandler.py +++ b/archinstall/lib/translationhandler.py @@ -2,10 +2,15 @@ import gettext import json import os +import tempfile from dataclasses import dataclass from pathlib import Path from typing import override +from archinstall.lib.command import SysCommand +from archinstall.lib.exceptions import SysCallError +from archinstall.lib.output import debug + @dataclass class Language: @@ -14,6 +19,7 @@ class Language: translation: gettext.NullTranslations translation_percent: int translated_lang: str | None + console_font: str | None = None @property def display_name(self) -> str: @@ -31,10 +37,68 @@ def json(self) -> str: return self.name_en +_DEFAULT_FONT = 'default8x16' +_ENV_FONT = os.environ.get('FONT') + + +def _set_console_font(font_name: str | None) -> bool: + """ + Set the console font via setfont. + If font_name is None, sets default8x16. + On failure, keeps the current font unchanged. + Returns True on success, False on failure. + """ + target = font_name or _DEFAULT_FONT + + try: + SysCommand(f'setfont {target}') + return True + except SysCallError as err: + debug(f'Failed to set console font {target}: {err}') + return False + + +def save_console_font() -> None: + """Save the current console font (with unicode map) and console map to temp files.""" + try: + font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_') + cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_') + os.close(font_fd) + os.close(cmap_fd) + translation_handler._font_backup = Path(font_path) + translation_handler._cmap_backup = Path(cmap_path) + SysCommand(f'setfont -O {translation_handler._font_backup} -om {translation_handler._cmap_backup}') + except SysCallError as err: + debug(f'Failed to save console font: {err}') + translation_handler._font_backup = None + translation_handler._cmap_backup = None + + +def restore_console_font() -> None: + """Restore console font (with unicode map) and console map from backup.""" + if translation_handler._font_backup is None or not translation_handler._font_backup.exists(): + return + + args = str(translation_handler._font_backup) + if translation_handler._cmap_backup is not None and translation_handler._cmap_backup.exists(): + args += f' -m {translation_handler._cmap_backup}' + _set_console_font(args) + + translation_handler._font_backup.unlink(missing_ok=True) + translation_handler._font_backup = None + if translation_handler._cmap_backup is not None: + translation_handler._cmap_backup.unlink(missing_ok=True) + translation_handler._cmap_backup = None + + class TranslationHandler: def __init__(self) -> None: self._base_pot = 'base.pot' self._languages = 'languages.json' + self._active_language: Language | None = None + self._font_backup: Path | None = None + self._cmap_backup: Path | None = None + self._using_env_font: bool = False self._total_messages = self._get_total_active_messages() self._translated_languages = self._get_translations() @@ -43,6 +107,12 @@ def __init__(self) -> None: def translated_languages(self) -> list[Language]: return self._translated_languages + @property + def active_font(self) -> str | None: + if self._active_language is not None: + return self._active_language.console_font + return None + def _get_translations(self) -> list[Language]: """ Load all translated languages and return a list of such @@ -57,6 +127,7 @@ def _get_translations(self) -> list[Language]: abbr = mapping_entry['abbr'] lang = mapping_entry['lang'] translated_lang = mapping_entry.get('translated_lang', None) + console_font = mapping_entry.get('console_font', None) try: # get a translation for a specific language @@ -71,7 +142,7 @@ def _get_translations(self) -> list[Language]: # prevent cases where the .pot file is out of date and the percentage is above 100 percent = min(100, percent) - language = Language(abbr, lang, translation, percent, translated_lang) + language = Language(abbr, lang, translation, percent, translated_lang, console_font) languages.append(language) except FileNotFoundError as err: raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}") @@ -127,12 +198,36 @@ def get_language_by_abbr(self, abbr: str) -> Language: except Exception: raise ValueError(f'No language with abbreviation "{abbr}" found') - def activate(self, language: Language) -> None: + def activate(self, language: Language, set_font: bool = True) -> None: """ Set the provided language as the current translation """ # The install() call has the side effect of assigning GNUTranslations.gettext to builtins._ language.translation.install() + self._active_language = language + + if set_font and not self._using_env_font: + _set_console_font(language.console_font) + + def apply_console_font(self) -> None: + """Apply console font from FONT env var or active language mapping. + + If FONT env var is set and valid, use it and skip language mapping. + If FONT is set but invalid, fall back to language font. + If FONT is not set, use active language font. + """ + if _ENV_FONT: + if _set_console_font(_ENV_FONT): + self._using_env_font = True + debug(f'Console font set from FONT env var: {_ENV_FONT}') + else: + debug(f'FONT={_ENV_FONT} could not be set, falling back to language font mapping') + if self.active_font: + _set_console_font(self.active_font) + debug(f'Console font set from language mapping: {self.active_font}') + elif self.active_font: + _set_console_font(self.active_font) + debug(f'Console font set from language mapping: {self.active_font}') def _get_locales_dir(self) -> Path: """ diff --git a/archinstall/locales/languages.json b/archinstall/locales/languages.json index 1cc5cda434..2dbcf3b4d4 100644 --- a/archinstall/locales/languages.json +++ b/archinstall/locales/languages.json @@ -169,7 +169,7 @@ {"abbr": "tr", "lang": "Turkish", "translated_lang" : "Türkçe"}, {"abbr": "tw", "lang": "Twi"}, {"abbr": "ug", "lang": "Uighur"}, - {"abbr": "uk", "lang": "Ukrainian"}, + {"abbr": "uk", "lang": "Ukrainian", "console_font": "UniCyr_8x16"}, {"abbr": "ur", "lang": "Urdu", "translated_lang": "اردو"}, {"abbr": "uz", "lang": "Uzbek", "translated_lang": "O'zbek"}, {"abbr": "ve", "lang": "Venda"}, diff --git a/archinstall/main.py b/archinstall/main.py index ac072b0e84..f25b63e6ad 100644 --- a/archinstall/main.py +++ b/archinstall/main.py @@ -16,7 +16,7 @@ from archinstall.lib.output import debug, error, info, warn from archinstall.lib.packages.util import check_version_upgrade from archinstall.lib.pacman.pacman import Pacman -from archinstall.lib.translationhandler import tr +from archinstall.lib.translationhandler import restore_console_font, save_console_font, tr from archinstall.lib.utils.util import running_from_iso from archinstall.tui.ui.components import tui @@ -95,6 +95,8 @@ def run() -> int: print(tr('Archinstall requires root privileges to run. See --help for more.')) return 1 + save_console_font() + _log_sys_info() if not arch_config_handler.args.offline: @@ -159,6 +161,8 @@ def main() -> int: _error_message(exc) rc = 1 + restore_console_font() + return rc diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index e06dd21679..308e72b83f 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -1266,6 +1266,13 @@ def __init__(self, main: InstanceRunnable[ValueT] | Callable[[], Awaitable[Value super().__init__(ansi_color=True) self._main = main + @override + async def _on_exit_app(self) -> None: + from archinstall.lib.translationhandler import restore_console_font + + restore_console_font() + await super()._on_exit_app() + def action_trigger_help(self) -> None: from textual.widgets import HelpPanel @@ -1275,6 +1282,9 @@ def action_trigger_help(self) -> None: _ = self.screen.mount(HelpPanel()) def on_mount(self) -> None: + from archinstall.lib.translationhandler import translation_handler + + translation_handler.apply_console_font() _translate_bindings(self._merged_bindings, self._bindings) self._run_worker()