From 652ec9616f1d775c74f94af97ca9f6fb1462c4ca Mon Sep 17 00:00:00 2001 From: Softer Date: Fri, 1 May 2026 13:15:11 +0300 Subject: [PATCH 1/4] Fix truncated package metadata in additional packages preview (#3580) --- archinstall/lib/models/packages.py | 33 +++++++++++++++++++++++----- archinstall/lib/packages/packages.py | 10 +++++++-- archinstall/lib/pacman/pacman.py | 8 +++++-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/archinstall/lib/models/packages.py b/archinstall/lib/models/packages.py index 6ba6e1348a..1adc527ecb 100644 --- a/archinstall/lib/models/packages.py +++ b/archinstall/lib/models/packages.py @@ -1,3 +1,5 @@ +import os +import textwrap from dataclasses import dataclass, field from enum import Enum from functools import cached_property @@ -134,13 +136,32 @@ def longest_key(self) -> int: # return all package info line by line def info(self) -> str: - output = '' + # Preview pane occupies roughly half the terminal width when shown + # alongside the option list. Wrap each value to fit so long fields + # (Description, Optional Deps, etc.) do not produce horizontal scroll. + try: + cols = os.get_terminal_size().columns + except OSError: + cols = 80 + preview_width = max(40, cols // 2 - 5) + indent = ' ' * (self.longest_key + 3) + + lines: list[str] = [] for key, value in self.model_dump().items(): - key = key.replace('_', ' ').capitalize() - key = key.ljust(self.longest_key) - output += f'{key} : {value}\n' - - return output + key_label = key.replace('_', ' ').capitalize().ljust(self.longest_key) + prefix = f'{key_label} : ' + lines.append( + textwrap.fill( + str(value), + width=preview_width, + initial_indent=prefix, + subsequent_indent=indent, + break_long_words=False, + break_on_hyphens=False, + ), + ) + + return '\n'.join(lines) + '\n' @cached_property def get_depends_on(self) -> list[str]: diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index 3572c6c445..6c7de39091 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -9,6 +9,12 @@ from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType +# Force pacman to emit untruncated single-line field values. With the default +# pty width of 80 columns, pacman wraps long fields like Description across +# multiple lines, and our line-based parser ends up dropping the continuation +# lines after they get pre-stripped of their leading whitespace. See #3580. +_WIDE_PACMAN_ENV = {'COLUMNS': '500'} + def installed_package(package: str) -> LocalPackage | None: try: @@ -52,7 +58,7 @@ def package_group_info(package: str) -> PackageGroup | None: def available_package(package: str) -> AvailablePackage | None: try: package_info: list[str] = [] - for line in Pacman.run(f'-S --info {package}'): + for line in Pacman.run(f'-S --info {package}', environment_vars=_WIDE_PACMAN_ENV): package_info.append(line.decode().strip()) return _parse_package_output(package_info, AvailablePackage) @@ -78,7 +84,7 @@ def list_available_packages( except Exception as e: debug(f'Failed to sync Arch Linux package database: {e}') - for line in Pacman.run('-S --info'): + for line in Pacman.run('-S --info', environment_vars=_WIDE_PACMAN_ENV): dec_line = line.decode().strip() current_package.append(dec_line) diff --git a/archinstall/lib/pacman/pacman.py b/archinstall/lib/pacman/pacman.py index 83f8b40def..4f0a1e82ad 100644 --- a/archinstall/lib/pacman/pacman.py +++ b/archinstall/lib/pacman/pacman.py @@ -18,7 +18,11 @@ def __init__(self, target: Path, silent: bool = False): self.target = target @staticmethod - def run(args: str, default_cmd: str = 'pacman') -> SysCommand: + def run( + args: str, + default_cmd: str = 'pacman', + environment_vars: dict[str, str] | None = None, + ) -> SysCommand: """ A centralized function to call `pacman` from. It also protects us from colliding with other running pacman sessions (if used locally). @@ -37,7 +41,7 @@ def run(args: str, default_cmd: str = 'pacman') -> SysCommand: error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.')) sys.exit(1) - return SysCommand(f'{default_cmd} {args}') + return SysCommand(f'{default_cmd} {args}', environment_vars=environment_vars) def ask(self, error_message: str, bail_message: str, func: Callable, *args, **kwargs) -> None: # type: ignore[no-untyped-def, type-arg] while True: From 5136b3190d9d8c6d137fc060dbc774e0263a0839 Mon Sep 17 00:00:00 2001 From: Softer Date: Sun, 3 May 2026 04:12:20 +0300 Subject: [PATCH 2/4] Simplify package info fix: use rstrip and CSS wrap instead of env var plumbing --- archinstall/lib/models/packages.py | 34 +++++----------------------- archinstall/lib/packages/packages.py | 16 ++++--------- archinstall/lib/pacman/pacman.py | 8 ++----- archinstall/tui/ui/components.py | 5 ++++ 4 files changed, 18 insertions(+), 45 deletions(-) diff --git a/archinstall/lib/models/packages.py b/archinstall/lib/models/packages.py index 1adc527ecb..205d653e21 100644 --- a/archinstall/lib/models/packages.py +++ b/archinstall/lib/models/packages.py @@ -1,5 +1,3 @@ -import os -import textwrap from dataclasses import dataclass, field from enum import Enum from functools import cached_property @@ -134,34 +132,14 @@ class AvailablePackage(BaseModel): def longest_key(self) -> int: return max(len(key) for key in self.model_dump().keys()) - # return all package info line by line def info(self) -> str: - # Preview pane occupies roughly half the terminal width when shown - # alongside the option list. Wrap each value to fit so long fields - # (Description, Optional Deps, etc.) do not produce horizontal scroll. - try: - cols = os.get_terminal_size().columns - except OSError: - cols = 80 - preview_width = max(40, cols // 2 - 5) - indent = ' ' * (self.longest_key + 3) - - lines: list[str] = [] + output = '' for key, value in self.model_dump().items(): - key_label = key.replace('_', ' ').capitalize().ljust(self.longest_key) - prefix = f'{key_label} : ' - lines.append( - textwrap.fill( - str(value), - width=preview_width, - initial_indent=prefix, - subsequent_indent=indent, - break_long_words=False, - break_on_hyphens=False, - ), - ) - - return '\n'.join(lines) + '\n' + key = key.replace('_', ' ').capitalize() + key = key.ljust(self.longest_key) + output += f'{key} : {value}\n' + + return output @cached_property def get_depends_on(self) -> list[str]: diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index 6c7de39091..ae509bfe66 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -9,18 +9,12 @@ from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.ui.result import ResultType -# Force pacman to emit untruncated single-line field values. With the default -# pty width of 80 columns, pacman wraps long fields like Description across -# multiple lines, and our line-based parser ends up dropping the continuation -# lines after they get pre-stripped of their leading whitespace. See #3580. -_WIDE_PACMAN_ENV = {'COLUMNS': '500'} - def installed_package(package: str) -> LocalPackage | None: try: package_info = [] for line in Pacman.run(f'-Q --info {package}'): - package_info.append(line.decode().strip()) + package_info.append(line.decode().rstrip()) return _parse_package_output(package_info, LocalPackage) except SysCallError: @@ -58,8 +52,8 @@ def package_group_info(package: str) -> PackageGroup | None: def available_package(package: str) -> AvailablePackage | None: try: package_info: list[str] = [] - for line in Pacman.run(f'-S --info {package}', environment_vars=_WIDE_PACMAN_ENV): - package_info.append(line.decode().strip()) + for line in Pacman.run(f'-S --info {package}'): + package_info.append(line.decode().rstrip()) return _parse_package_output(package_info, AvailablePackage) except SysCallError: @@ -84,8 +78,8 @@ def list_available_packages( except Exception as e: debug(f'Failed to sync Arch Linux package database: {e}') - for line in Pacman.run('-S --info', environment_vars=_WIDE_PACMAN_ENV): - dec_line = line.decode().strip() + for line in Pacman.run('-S --info'): + dec_line = line.decode().rstrip() current_package.append(dec_line) if dec_line.startswith('Validated'): diff --git a/archinstall/lib/pacman/pacman.py b/archinstall/lib/pacman/pacman.py index 4f0a1e82ad..83f8b40def 100644 --- a/archinstall/lib/pacman/pacman.py +++ b/archinstall/lib/pacman/pacman.py @@ -18,11 +18,7 @@ def __init__(self, target: Path, silent: bool = False): self.target = target @staticmethod - def run( - args: str, - default_cmd: str = 'pacman', - environment_vars: dict[str, str] | None = None, - ) -> SysCommand: + def run(args: str, default_cmd: str = 'pacman') -> SysCommand: """ A centralized function to call `pacman` from. It also protects us from colliding with other running pacman sessions (if used locally). @@ -41,7 +37,7 @@ def run( error(tr('Pre-existing pacman lock never exited. Please clean up any existing pacman sessions before using archinstall.')) sys.exit(1) - return SysCommand(f'{default_cmd} {args}', environment_vars=environment_vars) + return SysCommand(f'{default_cmd} {args}') def ask(self, error_message: str, bail_message: str, func: Callable, *args, **kwargs) -> None: # type: ignore[no-untyped-def, type-arg] while True: diff --git a/archinstall/tui/ui/components.py b/archinstall/tui/ui/components.py index c51d35f94c..5b9faf9422 100644 --- a/archinstall/tui/ui/components.py +++ b/archinstall/tui/ui/components.py @@ -433,6 +433,11 @@ class SelectListScreen(BaseScreen[ValueT]): color: white; text-style: bold; } + + #preview_content { + width: 100%; + height: auto; + } """ def __init__( From 89329380a53889b146eec1b4c2bb0dc77b7c151c Mon Sep 17 00:00:00 2001 From: Softer Date: Mon, 4 May 2026 13:00:51 +0300 Subject: [PATCH 3/4] Apply preview text wrap conditionally via wrap_preview parameter --- archinstall/lib/menu/helpers.py | 3 +++ archinstall/lib/packages/packages.py | 1 + archinstall/tui/components.py | 16 +++++++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py index b512c95625..3d1615086d 100644 --- a/archinstall/lib/menu/helpers.py +++ b/archinstall/lib/menu/helpers.py @@ -20,6 +20,7 @@ def __init__( preview_location: Literal['right', 'bottom'] | None = None, multi: bool = False, enable_filter: bool = False, + wrap_preview: bool = False, ): self._header = header self._title = title @@ -29,6 +30,7 @@ def __init__( self._preview_location = preview_location self._multi = multi self._enable_filter = enable_filter + self._wrap_preview = wrap_preview async def show(self) -> Result[ValueT]: if self._multi: @@ -39,6 +41,7 @@ async def show(self) -> Result[ValueT]: allow_reset=self._allow_reset, preview_location=self._preview_location, enable_filter=self._enable_filter, + wrap_preview=self._wrap_preview, ).run() else: result = await OptionListScreen[ValueT]( diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index f21535c7a2..a742298671 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -187,6 +187,7 @@ async def select_additional_packages( multi=True, preview_location='right', enable_filter=True, + wrap_preview=True, ).show() match pck_result.type_: diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py index f893d6bc31..9268b57b63 100644 --- a/archinstall/tui/components.py +++ b/archinstall/tui/components.py @@ -211,6 +211,7 @@ def __init__( allow_reset: bool = False, preview_location: Literal['right', 'bottom'] | None = None, enable_filter: bool = False, + wrap_preview: bool = False, ): super().__init__(allow_skip, allow_reset) self._group = group @@ -218,6 +219,7 @@ def __init__( self._title = title self._preview_location = preview_location self._filter = enable_filter + self._wrap_preview = wrap_preview self._show_frame = False self._options = self._get_options() @@ -280,7 +282,10 @@ def compose(self) -> ComposeResult: with Container(): yield option_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + preview_label = Label('', id='preview_content', markup=False) + if self._wrap_preview: + preview_label.add_class('wrap-preview') + yield ScrollableContainer(preview_label) if self._filter: yield Input(placeholder='/filter', id='filter-input') @@ -434,7 +439,7 @@ class SelectListScreen(BaseScreen[ValueT]): text-style: bold; } - #preview_content { + .wrap-preview { width: 100%; height: auto; } @@ -448,6 +453,7 @@ def __init__( allow_reset: bool = False, preview_location: Literal['right', 'bottom'] | None = None, enable_filter: bool = False, + wrap_preview: bool = False, ): super().__init__(allow_skip, allow_reset) self._group = group @@ -455,6 +461,7 @@ def __init__( self._preview_location = preview_location self._show_frame = False self._filter = enable_filter + self._wrap_preview = wrap_preview self._selected_items: list[MenuItem] = self._group.selected_items self._options: list[Selection[MenuItem]] = self._get_selections() @@ -515,7 +522,10 @@ def compose(self) -> ComposeResult: with Container(): yield selection_list yield Rule(orientation=rule_orientation) - yield ScrollableContainer(Label('', id='preview_content', markup=False)) + preview_label = Label('', id='preview_content', markup=False) + if self._wrap_preview: + preview_label.add_class('wrap-preview') + yield ScrollableContainer(preview_label) if self._filter: yield Input(placeholder='/filter', id='filter-input') From 8708dce09b58c1fc21268ddb8be838907958e6ce Mon Sep 17 00:00:00 2001 From: Softer Date: Wed, 6 May 2026 15:57:02 +0300 Subject: [PATCH 4/4] Add missing wrap-preview CSS to OptionListScreen and pass parameter through SelectMenu --- archinstall/lib/menu/helpers.py | 1 + archinstall/tui/components.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/archinstall/lib/menu/helpers.py b/archinstall/lib/menu/helpers.py index 3d1615086d..947f3c55fb 100644 --- a/archinstall/lib/menu/helpers.py +++ b/archinstall/lib/menu/helpers.py @@ -52,6 +52,7 @@ async def show(self) -> Result[ValueT]: allow_reset=self._allow_reset, preview_location=self._preview_location, enable_filter=self._enable_filter, + wrap_preview=self._wrap_preview, ).run() if result.type_ == ResultType.Reset: diff --git a/archinstall/tui/components.py b/archinstall/tui/components.py index 9268b57b63..f732c21ff7 100644 --- a/archinstall/tui/components.py +++ b/archinstall/tui/components.py @@ -200,6 +200,11 @@ class OptionListScreen(BaseScreen[ValueT]): color: white; text-style: bold; } + + .wrap-preview { + width: 100%; + height: auto; + } """ def __init__(