Set console font automatically when selecting language (#4356)

* Set console font automatically when selecting language

Add console_font field to languages.json and Language dataclass.
When a language is activated, setfont is called automatically,
falling back to default8x16 on error or for languages without
a custom font. Also activate translation when loading language
from config file.

* Support FONT environment variable for console font override

When FONT env var is set, use it as the console font instead of the language-specific font mapping from languages.json. The font is applied at startup and preserved across language switches.

On exit (success or failure), restore the console font to default8x16.

* CI checks fix

* Try to restore original console font with setfont -O

* Fix for pylint

* Restore console font before Textual exits application mode

Move font set/restore into Textual lifecycle to prevent color
artifacts from 256/512 glyph transitions. Apply FONT env var
and language font in on_mount, restore in _on_exit_app.

Skip font change when loading language from config (set_font=False)
to defer it until TUI starts.

* Fall back to language font mapping when FONT env var is invalid

Add _using_env_font flag to skip mapping only when FONT was
successfully applied. Show info message after TUI exits if FONT
could not be set.

* Use tempfile for console font backup files

* Fix linter errors: use mkstemp, close fds, add @override

* Fix ruff formatting

* Move font state from module singleton into TranslationHandler

* Refactor font handling per review feedback

* Make font methods members of TranslationHandler, skip on non-ISO

* ci: trigger tests

* Move running_from_iso import to module level

No circular dep, simpler than per-method local imports.

* Add explicit ISO guard to restore_console_font

Matches the existing guards in _set_font and save_console_font.
Behaviour was already safe implicitly via _font_backup=None, but
the explicit check makes intent obvious at the call site.

* Skip apply_console_font off-ISO

* Use list form for setfont SysCommand calls
This commit is contained in:
Softer 2026-04-21 11:36:29 +03:00 committed by GitHub
parent 7d10f9e08b
commit 03b245a77a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 124 additions and 7 deletions

View File

@ -143,6 +143,7 @@ class ArchConfig:
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', '')

View File

@ -105,9 +105,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 <fontname>\n'
result = await Selection[Language](
header=title,

View File

@ -2,10 +2,16 @@ import builtins
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
from archinstall.lib.utils.util import running_from_iso
@dataclass
class Language:
@ -14,6 +20,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 +38,18 @@ class Language:
return self.name_en
_DEFAULT_FONT = 'default8x16'
_ENV_FONT = os.environ.get('FONT')
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 +58,65 @@ class TranslationHandler:
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 _set_font(self, font_name: str | None) -> bool:
"""Set the console font via setfont. Only runs on ISO. Returns True on success."""
if not running_from_iso():
return False
target = font_name or _DEFAULT_FONT
try:
SysCommand(['setfont', target])
return True
except SysCallError as err:
debug(f'Failed to set console font {target}: {err}')
return False
def save_console_font(self) -> None:
"""Save the current console font (with unicode map) and console map to temp files."""
if not running_from_iso():
return
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)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
except SysCallError as err:
debug(f'Failed to save console font: {err}')
self._font_backup = None
self._cmap_backup = None
def restore_console_font(self) -> None:
"""Restore console font (with unicode map) and console map from backup."""
if not running_from_iso():
return
if self._font_backup is None or not self._font_backup.exists():
return
cmd = ['setfont', str(self._font_backup)]
if self._cmap_backup is not None and self._cmap_backup.exists():
cmd += ['-m', str(self._cmap_backup)]
try:
SysCommand(cmd)
except SysCallError as err:
debug(f'Failed to restore console font: {err}')
self._font_backup.unlink(missing_ok=True)
self._font_backup = None
if self._cmap_backup is not None:
self._cmap_backup.unlink(missing_ok=True)
self._cmap_backup = None
def _get_translations(self) -> list[Language]:
"""
Load all translated languages and return a list of such
@ -57,6 +131,7 @@ class TranslationHandler:
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 +146,7 @@ class TranslationHandler:
# 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 +202,39 @@ class TranslationHandler:
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:
self._set_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 not running_from_iso():
return
if _ENV_FONT:
if self._set_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:
self._set_font(self.active_font)
debug(f'Console font set from language mapping: {self.active_font}')
elif self.active_font:
self._set_font(self.active_font)
debug(f'Console font set from language mapping: {self.active_font}')
def _get_locales_dir(self) -> Path:
"""

View File

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

View File

@ -16,7 +16,7 @@ from archinstall.lib.networking import ping
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 tr, translation_handler
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
translation_handler.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
translation_handler.restore_console_font()
return rc

View File

@ -1272,6 +1272,13 @@ class _AppInstance(App[ValueT]):
super().__init__(ansi_color=True)
self._main = main
@override
async def _on_exit_app(self) -> None:
from archinstall.lib.translationhandler import translation_handler
translation_handler.restore_console_font()
await super()._on_exit_app()
def action_trigger_help(self) -> None:
from textual.widgets import HelpPanel
@ -1281,6 +1288,9 @@ class _AppInstance(App[ValueT]):
_ = 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()