From 34713bb61a9d7276a54672f31511126b993987e1 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sun, 24 Aug 2025 20:56:34 +0600 Subject: [PATCH] added a prefix creation tab --- winehelper | 1 + winehelper_gui.py | 506 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 495 insertions(+), 12 deletions(-) diff --git a/winehelper b/winehelper index 2bf6b38..22b3bbd 100755 --- a/winehelper +++ b/winehelper @@ -2176,6 +2176,7 @@ case "$arg1" in remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; generate-db) generate_wine_metadata "$@" ;; + init-prefix) prepair_wine ; wait_wineserver ;; *) if [[ -f "$arg1" ]] ; then WIN_FILE_EXEC="$(readlink -f "$arg1")" diff --git a/winehelper_gui.py b/winehelper_gui.py index 2895c8a..1169e88 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -11,10 +11,10 @@ import json import hashlib from functools import partial from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, - QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, + QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser) from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve -from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter +from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices from PyQt5.QtNetwork import QLocalServer, QLocalSocket @@ -1011,6 +1011,205 @@ class ScriptParser: except Exception as e: return f"Ошибка чтения файла: {str(e)}" +class WineVersionSelectionDialog(QDialog): + """Диалог для выбора версии Wine/Proton с группировкой.""" + + def __init__(self, architecture, winehelper_path, user_work_path, parent=None): + super().__init__(parent) + self.architecture = architecture + self.winehelper_path = winehelper_path + self.user_work_path = user_work_path + self.selected_version = None + self.wine_versions_data = {} + + self.setWindowTitle(f"Выбор версии Wine/Proton для {architecture} префикса") + self.setMinimumSize(900, 500) + self.setModal(True) + + main_layout = QVBoxLayout(self) + + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Поиск версии...") + self.search_edit.textChanged.connect(self.filter_versions) + main_layout.addWidget(self.search_edit) + + self.version_tabs = QTabWidget() + main_layout.addWidget(self.version_tabs) + button_layout = QHBoxLayout() + self.refresh_button = QPushButton("Обновить список") + self.refresh_button.setIcon(QIcon.fromTheme("view-refresh")) + self.refresh_button.clicked.connect(self.load_versions) + button_layout.addStretch() + button_layout.addWidget(self.refresh_button) + main_layout.addLayout(button_layout) + + self.load_versions() + + def load_versions(self): + """Запускает процесс получения списка версий Wine.""" + if not shutil.which('jq'): + QMessageBox.critical(self, "Ошибка", "Утилита 'jq' не найдена. Невозможно получить список версий Wine.\n\nУстановите пакет 'jq'.") + return + + self.version_tabs.clear() + loading_widget = QWidget() + loading_layout = QVBoxLayout(loading_widget) + status_label = QLabel("Загрузка, пожалуйста, подождите...") + status_label.setAlignment(Qt.AlignCenter) + loading_layout.addWidget(status_label) + self.version_tabs.addTab(loading_widget, "Загрузка...") + self.version_tabs.setEnabled(False) + + self.refresh_button.setEnabled(False) + self.db_process = QProcess(self) + self.db_process.setProcessChannelMode(QProcess.MergedChannels) + self.db_process.finished.connect(self._on_db_generation_finished) + self.db_process.start(self.winehelper_path, ["generate-db"]) + + def _on_db_generation_finished(self, exit_code, exit_status): + """Обрабатывает завершение генерации метаданных Wine.""" + self.refresh_button.setEnabled(True) + self.version_tabs.setEnabled(True) + + error_message = None + if exit_code != 0: + error_output = self.db_process.readAll().data().decode('utf-8', 'ignore') + QMessageBox.warning(self, "Ошибка", f"Не удалось получить список версий Wine.\n\n{error_output}") + error_message = "Ошибка загрузки списка версий." + else: + metadata_file = os.path.join(self.user_work_path, "tmp", "wine_metadata.json") + if not os.path.exists(metadata_file): + QMessageBox.warning(self, "Ошибка", f"Файл метаданных не найден:\n{metadata_file}") + error_message = "Ошибка: файл метаданных не найден." + else: + try: + with open(metadata_file, 'r', encoding='utf-8') as f: + self.wine_versions_data = json.load(f) + except (json.JSONDecodeError, IOError) as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать или обработать файл метаданных:\n{e}") + error_message = "Ошибка парсинга JSON." + + if error_message: + self.version_tabs.clear() + error_widget = QWidget() + error_layout = QVBoxLayout(error_widget) + error_label = QLabel(error_message) + error_label.setAlignment(Qt.AlignCenter) + error_layout.addWidget(error_label) + self.version_tabs.addTab(error_widget, "Ошибка") + return + + self.populate_ui() + + def populate_ui(self): + """Заполняет UI отфильтрованными версиями.""" + self.version_tabs.clear() + + is_win64 = self.architecture == "win64" + re_32bit = re.compile(r'i[3-6]86|x86(?!_64)') + re_64bit = re.compile(r'amd64|x86_64|wow64') + + # --- System Tab --- + system_wine_display_name = "system" + if shutil.which('wine'): + try: + # Пытаемся получить версию системного wine + result = subprocess.run(['wine', '--version'], capture_output=True, text=True, check=True, encoding='utf-8') + version_line = result.stdout.strip() + # Вывод обычно "wine-X.Y.Z" + system_wine_display_name = f"system ({version_line})" + except (FileNotFoundError, subprocess.CalledProcessError) as e: + print(f"Не удалось получить версию системного wine: {e}") + # Если wine возвращает ошибку, просто используем "system" + + self._create_version_tab("Системный", [(system_wine_display_name, "system")]) + + # --- Other versions from JSON --- + group_keys = sorted(self.wine_versions_data.keys()) + + for key in group_keys: + versions = self.wine_versions_data.get(key, []) + all_version_names = {v.get("name", "") for v in versions if v.get("name")} + + filtered_versions = [] + for name in sorted(list(all_version_names), reverse=True): + if is_win64: + if re_64bit.search(name) or not re_32bit.search(name): + filtered_versions.append(name) + else: # win32 + if re_32bit.search(name): + filtered_versions.append(name) + + if not filtered_versions: + continue + + pretty_key = key.replace('_', ' ').title() + self._create_version_tab(pretty_key, filtered_versions) + + self.filter_versions() + + def _create_version_tab(self, title, versions_list): + """Создает вкладку с сеткой кнопок для переданного списка версий.""" + tab_page = QWidget() + tab_layout = QVBoxLayout(tab_page) + tab_layout.setContentsMargins(5, 5, 5, 5) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + tab_layout.addWidget(scroll_area) + + scroll_content = QWidget() + scroll_area.setWidget(scroll_content) + + grid_layout = QGridLayout(scroll_content) + grid_layout.setAlignment(Qt.AlignTop) + + num_columns = 3 + row, col = 0, 0 + for version_data in versions_list: + if isinstance(version_data, tuple): + display_name, value_name = version_data + else: + display_name = value_name = version_data + + btn = QPushButton(display_name) + btn.clicked.connect(partial(self.on_version_selected, value_name)) + grid_layout.addWidget(btn, row, col) + col += 1 + if col >= num_columns: + col = 0 + row += 1 + + self.version_tabs.addTab(tab_page, title) + + def filter_versions(self): + """Фильтрует видимость кнопок версий на основе текста поиска.""" + search_text = self.search_edit.text().lower() + + for i in range(self.version_tabs.count()): + tab_widget = self.version_tabs.widget(i) + # The grid layout is inside a scroll area content widget + grid_layout = tab_widget.findChild(QGridLayout) + if not grid_layout: + continue + + any_visible_in_tab = False + for j in range(grid_layout.count()): + btn_widget = grid_layout.itemAt(j).widget() + if isinstance(btn_widget, QPushButton): + is_match = search_text in btn_widget.text().lower() + btn_widget.setVisible(is_match) + if is_match: + any_visible_in_tab = True + + # Enable/disable tab based on content + self.version_tabs.setTabEnabled(i, any_visible_in_tab) + + def on_version_selected(self, version_name): + """Обрабатывает выбор версии.""" + self.selected_version = version_name + self.accept() + class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() @@ -1063,6 +1262,9 @@ class WineHelperGUI(QMainWindow): self.icon_animators = {} self.previous_tab_index = 0 + # State for command dialog log processing (specifically for prefix creation) + self.command_output_buffer = "" + self.command_last_line_was_progress = False # Создаем главный виджет и layout self.main_widget = QWidget() self.setCentralWidget(self.main_widget) @@ -1085,6 +1287,7 @@ class WineHelperGUI(QMainWindow): self.create_auto_install_tab() self.create_manual_install_tab() self.create_installed_tab() + self.create_prefix_tab() self.create_help_tab() # Инициализируем состояние, которое будет использоваться для логов @@ -1167,13 +1370,13 @@ class WineHelperGUI(QMainWindow): self.info_panel_layout.setStretch(1, 1) self.info_panel_layout.setStretch(4, 0) - if current_tab_text in ["Автоматическая установка", "Ручная установка", "Установленные"]: + if current_tab_text in ["Справка", "Создать префикс"]: + self.info_panel.setVisible(False) + else: self.info_panel.setVisible(True) self._reset_info_panel_to_default(current_tab_text) if current_tab_text == "Установленные": self.filter_installed_buttons() - else: - self.info_panel.setVisible(False) def create_info_panel(self): """Создает правую панель с информацией о скрипте""" @@ -1565,6 +1768,81 @@ class WineHelperGUI(QMainWindow): ) self.tabs.addTab(installed_tab, "Установленные") + def open_wine_version_dialog(self): + """Открывает диалог выбора версии Wine.""" + architecture = "win32" if self.arch_win32_radio.isChecked() else "win64" + dialog = WineVersionSelectionDialog(architecture, self.winehelper_path, Var.USER_WORK_PATH, self) + if dialog.exec_() == QDialog.Accepted and dialog.selected_version: + self.wine_version_edit.setText(dialog.selected_version) + + def clear_wine_version_selection(self): + """ + Сбрасывает выбор версии Wine при смене архитектуры, + чтобы заставить пользователя выбрать заново. + """ + self.wine_version_edit.clear() + + def create_prefix_tab(self): + """Создает вкладку для создания нового префикса""" + self.prefix_tab = QWidget() + layout = QVBoxLayout(self.prefix_tab) + layout.setContentsMargins(10, 10, 10, 10) + + form_layout = QFormLayout() + form_layout.setSpacing(10) + + self.prefix_name_edit = QLineEdit() + self.prefix_name_edit.setPlaceholderText("Например: 'm_prefix'") + form_layout.addRow("Имя нового префикса:", self.prefix_name_edit) + + self.arch_groupbox = QGroupBox("Архитектура") + arch_layout = QHBoxLayout() + self.arch_win32_radio = QRadioButton("32-bit") + self.arch_win64_radio = QRadioButton("64-bit") + self.arch_win64_radio.setChecked(True) + arch_layout.addWidget(self.arch_win32_radio) + arch_layout.addWidget(self.arch_win64_radio) + self.arch_groupbox.setLayout(arch_layout) + form_layout.addRow("Разрядность:", self.arch_groupbox) + + self.type_groupbox = QGroupBox("Тип префикса") + type_layout = QHBoxLayout() + self.type_clean_radio = QRadioButton("Чистый") + self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками") + self.type_recommended_radio.setChecked(True) + type_layout.addWidget(self.type_clean_radio) + type_layout.addWidget(self.type_recommended_radio) + self.type_groupbox.setLayout(type_layout) + form_layout.addRow("Наполнение:", self.type_groupbox) + + self.wine_version_edit = QLineEdit() + self.wine_version_edit.setReadOnly(True) + self.wine_version_edit.setPlaceholderText("Версия не выбрана") + + select_version_button = QPushButton("Выбрать версию...") + select_version_button.clicked.connect(self.open_wine_version_dialog) + + version_layout = QHBoxLayout() + version_layout.addWidget(self.wine_version_edit) + version_layout.addWidget(select_version_button) + form_layout.addRow("Версия Wine/Proton:", version_layout) + + layout.addLayout(form_layout) + layout.addStretch() + + self.create_prefix_button = QPushButton("Создать префикс") + self.create_prefix_button.setFont(QFont('Arial', 12, QFont.Bold)) + self.create_prefix_button.setStyleSheet("background-color: #0078d7; color: white;") + self.create_prefix_button.setEnabled(False) + self.create_prefix_button.clicked.connect(self.start_prefix_creation) + layout.addWidget(self.create_prefix_button) + + self.tabs.addTab(self.prefix_tab, "Создать префикс") + + self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection) + self.prefix_name_edit.textChanged.connect(self.update_create_prefix_button_state) + self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) + def create_help_tab(self): """Создает вкладку 'Справка' с подвкладками""" help_tab = QWidget() @@ -1685,6 +1963,101 @@ class WineHelperGUI(QMainWindow): self.tabs.addTab(help_tab, "Справка") + def update_create_prefix_button_state(self): + """Включает или выключает кнопку 'Создать префикс' в зависимости от заполнения полей.""" + name_ok = bool(self.prefix_name_edit.text().strip()) + version_ok = bool(self.wine_version_edit.text().strip()) + self.create_prefix_button.setEnabled(name_ok and version_ok) + + def start_prefix_creation(self): + """Запускает создание префикса после валидации.""" + if not self._show_license_agreement_dialog(): + return + + prefix_name = self.prefix_name_edit.text().strip() + + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.") + return + + if not re.match(r'^[a-zA-Z0-9_.-]+$', prefix_name): + QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if os.path.exists(prefix_path): + QMessageBox.warning(self, "Ошибка", f"Префикс с именем '{prefix_name}' уже существует.") + return + + wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64" + base_pfx = "none" if self.type_clean_radio.isChecked() else "" + wine_use = self.wine_version_edit.text() + + self.command_dialog = QDialog(self) + self.command_dialog.setWindowTitle(f"Создание префикса: {prefix_name}") + self.command_dialog.setMinimumSize(750, 400) + self.command_dialog.setModal(True) + self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint) + + layout = QVBoxLayout() + self.command_log_output = QTextEdit() + self.command_log_output.setReadOnly(True) + self.command_log_output.setFont(QFont('DejaVu Sans Mono', 10)) + layout.addWidget(self.command_log_output) + + self.command_close_button = QPushButton("Закрыть") + self.command_close_button.setEnabled(False) + self.command_close_button.clicked.connect(self.command_dialog.close) + layout.addWidget(self.command_close_button) + self.command_dialog.setLayout(layout) + + # Сбрасываем состояние для обработки лога с прогрессом + self.command_output_buffer = "" + self.command_last_line_was_progress = False + + self.command_process = QProcess(self.command_dialog) + self.command_process.setProcessChannelMode(QProcess.MergedChannels) + + # Для создания префикса используем специальный обработчик вывода с поддержкой прогресс-бара + self.command_process.readyReadStandardOutput.connect(self._handle_prefix_creation_output) + self.command_process.finished.connect(self._handle_prefix_creation_finished) + + env = QProcessEnvironment.systemEnvironment() + env.insert("WINEPREFIX", prefix_path) + env.insert("WINEARCH", wine_arch) + env.insert("WH_WINE_USE", wine_use) + if base_pfx: + env.insert("BASE_PFX", base_pfx) + self.command_process.setProcessEnvironment(env) + + args = ["init-prefix"] + self.command_log_output.append(f"=== Параметры создания префикса ===\nИмя: {prefix_name}\nПуть: {prefix_path}\nАрхитектура: {wine_arch}\nВерсия Wine: {wine_use}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n") + self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}") + self.command_process.start(self.winehelper_path, args) + self.command_dialog.exec_() + + def _handle_prefix_creation_finished(self, exit_code, exit_status): + """Обрабатывает завершение создания префикса.""" + # Обрабатываем остаток в буфере, если он есть + if self.command_output_buffer: + self._process_command_log_line(self.command_output_buffer) + self.command_output_buffer = "" + + # Если последней строкой был прогресс, "завершаем" его переносом строки. + if self.command_last_line_was_progress: + cursor = self.command_log_output.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText("\n") + self.command_last_line_was_progress = False + + prefix_name = self.command_process.processEnvironment().value('WINEPREFIX').split('/')[-1] + + self._handle_command_finished(exit_code, exit_status) + if exit_code == 0: + self.prefix_name_edit.clear() + self.wine_version_edit.clear() + QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' успешно создан.") + def update_installed_apps(self): """Обновляет список установленных приложений в виде кнопок""" # Если активная кнопка находится в списке удаляемых, сбрасываем ее @@ -1891,7 +2264,6 @@ class WineHelperGUI(QMainWindow): self.command_close_button.clicked.connect(self.command_dialog.close) layout.addWidget(self.command_close_button) self.command_dialog.setLayout(layout) - # Устанавливаем родителя, чтобы избежать утечек памяти self.command_process = QProcess(self.command_dialog) self.command_process.setProcessChannelMode(QProcess.MergedChannels) @@ -1935,7 +2307,6 @@ class WineHelperGUI(QMainWindow): self.command_close_button.clicked.connect(self.command_dialog.close) layout.addWidget(self.command_close_button) self.command_dialog.setLayout(layout) - # Устанавливаем родителя, чтобы избежать утечек памяти self.command_process = QProcess(self.command_dialog) self.command_process.setProcessChannelMode(QProcess.MergedChannels) @@ -2332,6 +2703,55 @@ class WineHelperGUI(QMainWindow): self.installed_global_action_widget.setVisible(False) self.install_button.setText(f"Установить «{display_name}»") + def _show_license_agreement_dialog(self): + """Показывает модальный диалог с лицензионным соглашением.""" + dialog = QDialog(self) + dialog.setWindowTitle("Лицензионное соглашение") + dialog.setMinimumSize(750, 400) + dialog.setModal(True) + + layout = QVBoxLayout(dialog) + + license_text = QTextBrowser() + try: + license_file_path = Var.LICENSE_AGREEMENT_FILE + if not license_file_path or not os.path.exists(license_file_path): + raise FileNotFoundError + + with open(license_file_path, 'r', encoding='utf-8') as f: + license_content = f.read() + + escaped_license_content = html.escape(license_content) + license_text.setHtml(f""" +
{escaped_license_content}
+ """) + except (FileNotFoundError, TypeError): + license_text.setHtml(f'

Лицензионные соглашения

Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}

') + except Exception as e: + license_text.setHtml(f'

Лицензионные соглашения

Произошла ошибка при чтении файла лицензии:
{str(e)}

') + + layout.addWidget(license_text) + + checkbox = QCheckBox("Я принимаю условия лицензионного соглашения") + layout.addWidget(checkbox) + + button_layout = QHBoxLayout() + accept_button = QPushButton("Принять") + accept_button.setEnabled(False) + accept_button.clicked.connect(dialog.accept) + + cancel_button = QPushButton("Отклонить") + cancel_button.clicked.connect(dialog.reject) + + button_layout.addStretch() + button_layout.addWidget(accept_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + checkbox.stateChanged.connect(lambda state: accept_button.setEnabled(state == Qt.Checked)) + + return dialog.exec_() == QDialog.Accepted + def install_current_script(self): """Устанавливает текущий выбранный скрипт""" if not self.current_script: @@ -2499,10 +2919,11 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}") self.cleanup_process() - def append_log(self, text, is_error=False, add_newline=True): - """Добавляет сообщение в лог""" - if not hasattr(self, 'log_output'): return - cursor = self.log_output.textCursor() + def _append_to_log(self, log_widget, text, is_error=False, add_newline=True): + """Helper to append text to a QTextEdit log widget.""" + if not log_widget: + return + cursor = log_widget.textCursor() cursor.movePosition(QTextCursor.End) if is_error: @@ -2513,9 +2934,13 @@ class WineHelperGUI(QMainWindow): formatted_text = f"{text}\n" if add_newline else text cursor.insertText(formatted_text) - self.log_output.ensureCursorVisible() + log_widget.ensureCursorVisible() QApplication.processEvents() + def append_log(self, text, is_error=False, add_newline=True): + """Добавляет сообщение в лог установки.""" + self._append_to_log(self.log_output, text, is_error, add_newline) + def _process_log_line(self, line_with_delimiter): """Обрабатывает одну строку лога, управляя заменой строк прогресса.""" is_progress_line = '\r' in line_with_delimiter @@ -2552,6 +2977,42 @@ class WineHelperGUI(QMainWindow): self.last_line_was_progress = is_progress_line + def _process_command_log_line(self, line_with_delimiter): + """Обрабатывает одну строку лога для диалога создания префикса, управляя заменой строк прогресса.""" + is_progress_line = '\r' in line_with_delimiter + + # Фильтруем "мусорные" строки прогресса (например, '-=O=-' от wget), + # обрабатывая только те, что содержат знак процента. + if is_progress_line: + if not re.search(r'\d\s*%', line_with_delimiter): + return # Игнорируем строку прогресса без процентов + + clean_line = line_with_delimiter.replace('\r', '').replace('\n', '').strip() + + if not clean_line: + return + + cursor = self.command_log_output.textCursor() + + # Если новая строка - это прогресс, и предыдущая тоже была прогрессом, + # то мы удаляем старую, чтобы заменить ее новой. + if is_progress_line and self.command_last_line_was_progress: + cursor.movePosition(QTextCursor.End) + cursor.select(QTextCursor.LineUnderCursor) + cursor.removeSelectedText() + elif not is_progress_line and self.command_last_line_was_progress: + # Это переход от строки прогресса к финальной строке. + # Вместо добавления переноса, мы заменяем предыдущую строку новой. + cursor.movePosition(QTextCursor.End) + cursor.select(QTextCursor.LineUnderCursor) + cursor.removeSelectedText() + + # Добавляем новую очищенную строку. + # Для прогресса - без переноса строки, для обычных строк - с переносом. + self._append_to_log(self.command_log_output, clean_line, add_newline=not is_progress_line) + + self.command_last_line_was_progress = is_progress_line + def handle_process_output(self): """Обрабатывает вывод процесса, корректно отображая однострочный прогресс.""" new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore') @@ -2604,6 +3065,27 @@ class WineHelperGUI(QMainWindow): # Кнопка прервать self.btn_abort.setEnabled(False) + def _handle_prefix_creation_output(self): + """Обрабатывает вывод процесса создания префикса, корректно отображая прогресс.""" + if not hasattr(self, 'command_process') or not self.command_process: + return + new_data = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore') + self.command_output_buffer += new_data + + while True: + # Ищем ближайший разделитель (\n или \r) + idx_n = self.command_output_buffer.find('\n') + idx_r = self.command_output_buffer.find('\r') + + if idx_n == -1 and idx_r == -1: + break # Нет полных строк для обработки + + split_idx = min(idx for idx in [idx_n, idx_r] if idx != -1) + + line = self.command_output_buffer[:split_idx + 1] + self.command_output_buffer = self.command_output_buffer[split_idx + 1:] + self._process_command_log_line(line) + # Процесс завершен, можно запланировать его удаление и очистить ссылку, # чтобы избежать утечек и висячих ссылок. if self.install_process: