#!/usr/bin/env python3
import os
import subprocess
import sys
import re
import shlex
import shutil
import html
import time
import json
import hashlib
from functools import partial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar,
                             QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox,
                             QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox, QSystemTrayIcon, QMenu)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve, pyqtSignal, QRect
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor, QTextCharFormat, QColor, QPalette
from PyQt5.QtNetwork import QLocalServer, QLocalSocket


class Var:
    # Переменные определяемые в скрипте winehelper
    SCRIPT_NAME = os.environ.get("SCRIPT_NAME")
    USER_WORK_PATH = os.environ.get("USER_WORK_PATH")
    RUN_SCRIPT = os.environ.get("RUN_SCRIPT")
    DATA_PATH = os.environ.get("DATA_PATH")
    CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE")
    WH_ICON_PATH = os.environ.get("WH_ICON_PATH")
    WH_ICON_TRAY = os.environ.get("WH_ICON_TRAY")
    LICENSE_FILE = os.environ.get("LICENSE_FILE")
    LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
    THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE")
    GENERAL = os.environ.get("GENERAL")
    WH_WINETRICKS = os.environ.get("WH_WINETRICKS")

class DependencyManager:
    """Класс для управления проверкой и установкой системных зависимостей."""

    def __init__(self):
        """Инициализирует менеджер, определяя необходимые пути."""
        self.dependencies_script_path = self._get_dependencies_path()
        self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper")
        self.hash_flag_file = os.path.join(self.config_dir, "dependencies_hash.txt")
        os.makedirs(self.config_dir, exist_ok=True)
        self.app_icon = QIcon(Var.WH_ICON_PATH) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH) else QIcon()

    def _get_dependencies_path(self):
        """Определяет и возвращает путь к скрипту dependencies.sh."""
        if not Var.DATA_PATH:
            return None

        return os.path.join(Var.DATA_PATH, 'dependencies.sh')

    def _calculate_file_hash(self):
        """Вычисляет хэш SHA256 файла зависимостей."""
        if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
            return None
        hasher = hashlib.sha256()
        try:
            with open(self.dependencies_script_path, 'rb') as f:
                while chunk := f.read(4096):
                    hasher.update(chunk)
            return hasher.hexdigest()
        except IOError:
            return None

    def _parse_dependencies_from_script(self):
        """
        Парсит скрипт dependencies.sh для извлечения списка базовых пакетов.
        Возвращает список пакетов или None в случае ошибки.
        """
        if not os.path.exists(self.dependencies_script_path):
            return None

        base_packages = []
        try:
            with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
                content = f.read()

            content = content.replace('\\\n', '')
            pattern = r'apt-get install\s+\{i586-,\}(\{.*?\}|[\w.-]+)'
            matches = re.findall(pattern, content)

            for match in matches:
                match = match.strip()
                if match.startswith('{') and match.endswith('}'):
                    group_content = match[1:-1]
                    packages = [pkg.strip() for pkg in group_content.split(',')]
                    base_packages.extend(packages)
                else:
                    base_packages.append(match)

            return sorted(list(set(pkg for pkg in base_packages if pkg))) or None
        except Exception:
            return None

    def _parse_repo_error_from_script(self):
        """
        Парсит скрипт dependencies.sh для извлечения сообщения об ошибке репозитория.
        Возвращает сообщение или None в случае ошибки.
        """
        if not os.path.exists(self.dependencies_script_path):
            return None

        try:
            with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
                content = f.read()

            content = content.replace('\\\n', ' ')
            match = re.search(r'apt-repo.*\|\|.*fatal\s+"([^"]+)"', content)
            if match:
                error_message = match.group(1).strip()
                return re.sub(r'\s+', ' ', error_message)
            return None
        except Exception:
            return None

    def _show_startup_messages(self):
        """
        Проверяет, является ли это первым запуском или обновлением,
        и показывает соответствующие сообщения.
        """
        current_hash = self._calculate_file_hash()
        stored_hash = None
        hash_file_exists = os.path.exists(self.hash_flag_file)

        if hash_file_exists:
            try:
                with open(self.hash_flag_file, 'r', encoding='utf-8') as f:
                    stored_hash = f.read().strip()
            except IOError:
                pass

        if not hash_file_exists:
            msg_box = QMessageBox(QMessageBox.Information, "Первый запуск WineHelper",
                                  "Поскольку это первый запуск, программа проверит наличие необходимых системных зависимостей.")
            msg_box.setWindowIcon(self.app_icon)
            msg_box.exec_()
        elif current_hash != stored_hash:
            msg_box = QMessageBox(QMessageBox.Information, "Обновление зависимостей",
                                  "Обнаружены изменения в системных требованиях.\n\n"
                                  "Программа выполнит проверку, чтобы убедиться, что все компоненты на месте.")
            msg_box.setWindowIcon(self.app_icon)
            msg_box.exec_()

    def _save_dependency_hash(self):
        """Сохраняет хэш зависимостей в конфигурационный файл."""
        current_hash = self._calculate_file_hash()
        if not current_hash:
            return
        try:
            with open(self.hash_flag_file, 'w', encoding='utf-8') as f:
                f.write(current_hash)
        except IOError:
            print("Предупреждение: не удалось записать файл с хэшем зависимостей.")

    def _perform_check_and_install(self):
        """
        Выполняет основную логику проверки и установки зависимостей.
        Возвращает True в случае успеха, иначе False.
        """
        # Проверка наличия ключевых утилит
        if not shutil.which('apt-repo') or not shutil.which('rpm'):
            return True

        if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
            msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
                f"Файл зависимостей не найден по пути:\n'{self.dependencies_script_path}'.\n\n"
                "Программа не может продолжить работу."
            )
            msg_box.setWindowIcon(self.app_icon)
            msg_box.exec_()
            return False

        # 1. Проверка наличия репозитория x86_64-i586
        try:
            result = subprocess.run(['apt-repo'], capture_output=True, text=True, check=False, encoding='utf-8')
            if result.returncode != 0 or 'x86_64-i586' not in result.stdout:
                error_message = self._parse_repo_error_from_script()
                if not error_message:
                    msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
                        f"Репозиторий x86_64-i586 не подключен, но не удалось извлечь текст ошибки из файла:\n'{self.dependencies_script_path}'.\n\n"
                        "Проверьте целостность скрипта. Работа программы будет прекращена."
                    )
                    msg_box.setWindowIcon(self.app_icon)
                    msg_box.exec_()
                    return False

                msg_box = QMessageBox(QMessageBox.Critical, "Ошибка репозитория",
                                      f"{error_message}\n\nРабота программы будет прекращена.")
                msg_box.setWindowIcon(self.app_icon)
                msg_box.exec_()
                return False
        except FileNotFoundError:
            return True

        # 2. Определение списка пакетов из dependencies.sh
        base_packages = self._parse_dependencies_from_script()

        # Если парсинг не удался или скрипт не найден, прерываем работу.
        if not base_packages:
            msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
                f"Не удалось найти или проанализировать файл зависимостей:\n'{self.dependencies_script_path}'.\n\n"
                "Программа не может продолжить работу без списка необходимых пакетов."
            )
            msg_box.setWindowIcon(self.app_icon)
            msg_box.exec_()
            return False

        required_packages = [f"i586-{pkg}" for pkg in base_packages] + base_packages

        # 3. Проверка, какие пакеты отсутствуют
        try:
            # Запрашиваем список всех установленных пакетов один раз для эффективности
            result = subprocess.run(
                ['rpm', '-qa', '--queryformat', '%{NAME}\n'],
                capture_output=True, text=True, check=True, encoding='utf-8'
            )
            installed_packages_set = set(result.stdout.splitlines())
            required_packages_set = set(required_packages)

            # Находим разницу между требуемыми и установленными пакетами
            missing_packages = sorted(list(required_packages_set - installed_packages_set))

        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            # В случае ошибки (например, rpm не найден), показываем критическое сообщение
            msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
                f"Не удалось получить список установленных пакетов с помощью rpm.\n\nОшибка: {e}\n\n"
                "Программа не может проверить зависимости и будет закрыта."
            )
            msg_box.setWindowIcon(self.app_icon)
            msg_box.exec_()
            return False

        if not missing_packages:
            return True

        # 4. Запрос у пользователя на установку
        msg_box = QMessageBox()
        msg_box.setWindowIcon(self.app_icon)
        msg_box.setIcon(QMessageBox.Warning)
        msg_box.setWindowTitle("Отсутствуют зависимости")
        # Устанавливаем формат текста в RichText, чтобы QMessageBox корректно обрабатывал HTML-теги.
        msg_box.setTextFormat(Qt.RichText)

        # Формируем весь текст как единый HTML-блок для корректного отображения.
        full_html_text = (
            "Для корректной работы WineHelper требуются дополнительные системные компоненты.<br><br>"
            "Отсутствуют следующие пакеты:<br>"
            # Ограничиваем высоту блока с пакетами и добавляем прокрутку, если список длинный.
            f"""<div style="font-family: monospace; max-height: 150px; overflow-y: auto; margin-top: 5px; margin-bottom: 10px;">
            {'<br>'.join(sorted(missing_packages))}
            </div>"""
            "Нажмите <b>'Установить'</b>, чтобы запустить установку с правами администратора. "
            "Вам потребуется ввести пароль. Этот процесс может занять некоторое время."
        )
        msg_box.setText(full_html_text)
        install_button = msg_box.addButton("Установить", QMessageBox.AcceptRole)
        cancel_button = msg_box.addButton("Отмена", QMessageBox.RejectRole)
        msg_box.setDefaultButton(install_button)

        msg_box.exec_()

        if msg_box.clickedButton() != install_button:
            cancel_box = QMessageBox(QMessageBox.Warning, "Отмена",
                                     "Установка отменена.\n\n"
                                     "WineHelper не может продолжить работу\nбез необходимых зависимостей.")
            cancel_box.setWindowIcon(self.app_icon)
            cancel_box.exec_()
            return False

        # 5. Запуск установки с отображением лога в диалоговом окне
        if not shutil.which('pkexec'):
            error_box = QMessageBox(QMessageBox.Critical, "Ошибка",
                "Не найден компонент 'pkexec' для повышения прав.\n"
                "Пожалуйста, установите пакет 'polkit-pkexec', "
                f"а затем установите зависимости вручную:\n\n sudo apt-get install {' '.join(missing_packages)}"
            )
            error_box.setWindowIcon(self.app_icon)
            error_box.exec_()
            return False

        install_cmd_str = f"apt-get update && apt-get install -y {' '.join(missing_packages)}"

        # Этот флаг будет установлен в True, если установка и проверка пройдут успешно.
        installation_successful = False

        # Создаем диалог для вывода лога установки
        dialog = QDialog()
        dialog.setWindowIcon(self.app_icon)
        dialog.setWindowTitle("Установка зависимостей")
        dialog.setMinimumSize(680, 400)
        dialog.setModal(True)

        # Центрирование окна по центру экрана
        screen_geometry = QApplication.primaryScreen().availableGeometry()
        dialog.move(
            (screen_geometry.width() - dialog.width()) // 2,
            (screen_geometry.height() - dialog.height()) // 2
        )

        layout = QVBoxLayout(dialog)
        log_output = QTextEdit()
        log_output.setReadOnly(True)
        log_output.setFont(QFont("DejaVu Sans Mono", 10))
        layout.addWidget(log_output)

        close_button = QPushButton("Закрыть")
        close_button.setEnabled(False)
        close_button.clicked.connect(dialog.accept)
        layout.addWidget(close_button)

        process = QProcess(dialog)
        process.setProcessChannelMode(QProcess.MergedChannels)

        def handle_output():
            # Используем insertPlainText для корректного отображения потокового вывода (например, прогресс-баров)
            log_output.insertPlainText(process.readAll().data().decode('utf-8', 'ignore'))
            log_output.moveCursor(QTextCursor.End)

        def handle_finish(exit_code, exit_status):
            nonlocal installation_successful
            log_output.moveCursor(QTextCursor.End)
            if exit_code == 0 and exit_status == QProcess.NormalExit:
                log_output.append("\n<b><font color='green'>=== Установка успешно завершена ===</font></b>")
                log_output.ensureCursorVisible()

                # Повторная проверка зависимостей сразу после установки
                try:
                    result = subprocess.run(
                        ['rpm', '-qa', '--queryformat', '%{NAME}\n'],
                        capture_output=True, text=True, check=True, encoding='utf-8'
                    )
                    installed_packages_set = set(result.stdout.splitlines())
                    missing_packages_set = set(missing_packages)
                    still_missing = sorted(list(missing_packages_set - installed_packages_set))
                except (subprocess.CalledProcessError, FileNotFoundError):
                    warn_box = QMessageBox(dialog)
                    warn_box.setWindowIcon(self.app_icon)
                    warn_box.setIcon(QMessageBox.Warning)
                    warn_box.setWindowTitle("Проверка не удалась")
                    warn_box.setText("Не удалось повторно проверить зависимости после установки.")
                    warn_box.exec_()
                    still_missing = missing_packages

                if not still_missing:
                    installation_successful = True
                    close_button.setText("Запустить WineHelper")
                else:
                    warn_box = QMessageBox(dialog)
                    warn_box.setWindowIcon(self.app_icon)
                    warn_box.setIcon(QMessageBox.Warning)
                    warn_box.setWindowTitle("Установка не завершена")
                    warn_box.setText(
                        "Не все зависимости были установлены. WineHelper может работать некорректно.\n\n"
                        f"Отсутствуют: {', '.join(sorted(still_missing))}\n\n"
                        "Попробуйте запустить установку снова или установите пакеты вручную."
                    )
                    warn_box.exec_()
            else:
                if exit_code == 127:  # pkexec: пользователь отменил аутентификацию
                    log_output.append("\n<b><font color='orange'>=== УСТАНОВКА ОТМЕНЕНА ПОЛЬЗОВАТЕЛЕМ ===</font></b>")
                    log_output.append("Вы отменили ввод пароля. Установка зависимостей не была выполнена.")
                elif exit_code == 126:  # pkexec: у пользователя нет прав
                    log_output.append("\n<b><font color='red'>=== ОШИБКА: НЕДОСТАТОЧНО ПРАВ ===</font></b>")
                    log_output.append("У вашего пользователя нет прав для выполнения этой операции.")
                else:
                    log_tag = "ПРЕРВАНО" if exit_status == QProcess.CrashExit else "ОШИБКА"
                    log_output.append(f"\n<b><font color='red'>=== {log_tag} (код: {exit_code}) ===</font></b>")
                    log_output.append("Произошла непредвиденная ошибка во время установки.")
                log_output.ensureCursorVisible()

            close_button.setEnabled(True)

        def dialog_close_handler(event):
            """Обрабатывает закрытие окна во время установки зависимостей."""
            if process.state() == QProcess.Running:
                # QMessageBox без кнопок может некорректно обрабатывать закрытие.
                # Используем простой QDialog для надежности.
                info_dialog = QDialog(dialog)
                info_dialog.setWindowTitle("Идет установка")
                info_dialog.setModal(True)
                info_dialog.setFixedSize(450, 150)

                layout = QVBoxLayout(info_dialog)
                label = QLabel(
                    "<h3>Установка зависимостей еще не завершена.</h3>"
                    "<p>Пожалуйста, дождитесь окончания процесса.</p>"
                    "<p>Закрыть основное окно можно будет после завершения установки.</p>"
                )
                label.setTextFormat(Qt.RichText)
                label.setAlignment(Qt.AlignCenter)
                layout.addWidget(label)

                info_dialog.exec_()
                event.ignore()
            else:
                event.accept()

        process.readyRead.connect(handle_output)
        process.finished.connect(handle_finish)

        log_output.append(f"Выполнение команды:\npkexec sh -c \"{install_cmd_str}\"\n")
        log_output.append("Пожалуйста, введите пароль в появившемся окне аутентификации...")
        log_output.append("-" * 40 + "\n")

        dialog.closeEvent = dialog_close_handler
        process.start('pkexec', ['sh', '-c', install_cmd_str])
        dialog.exec_()

        return installation_successful

    def run(self):
        """
        Основной публичный метод для запуска полной проверки зависимостей.
        Возвращает True, если все зависимости удовлетворены, иначе False.
        """
        self._show_startup_messages()
        if self._perform_check_and_install():
            self._save_dependency_hash()
            return True
        return False

class WinetricksManagerDialog(QDialog):
    """Диалог для управления компонентами Winetricks."""

    INFO_TEXT = (
        "Компоненты можно только установить либо переустановить.\n"
        "Удаление компонентов не реализовано в Winetricks.\n"
        "Для установки нового компонента: Поставьте галочку и нажмите «Применить».\n"
        "Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»."
    )

    installation_complete = pyqtSignal()

    def __init__(self, prefix_path, winetricks_path, parent=None, wine_executable=None):
        super().__init__(parent)
        self.prefix_path = prefix_path
        self.winetricks_path = winetricks_path
        self.wine_executable = wine_executable or 'wine'
        self.initial_states = {}
        self.apply_process = None
        self.installation_finished = False
        self.user_cancelled = False
        self.processes = {}
        self.category_statuses = {}
        self.previous_tab_widget = None
        self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks")
        os.makedirs(self.cache_dir, exist_ok=True)
        self.is_reloading_after_cache_clear = False

        self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
        self.setMinimumSize(800, 500)

        # Основной layout
        main_layout = QVBoxLayout(self)

        # Табы для категорий
        self.tabs = QTabWidget()
        main_layout.addWidget(self.tabs)

        # Создаем табы
        self.categories = {
            "Библиотеки": "dlls",
            "Шрифты": "fonts",
            "Настройки": "settings"
        }
        self.list_widgets = {}
        self.search_edits = {}
        for display_name, internal_name in self.categories.items():
            list_widget, search_edit = self._create_category_tab(display_name)
            self.list_widgets[internal_name] = list_widget
            self.search_edits[internal_name] = search_edit

        # Лог для вывода команд
        self.log_output = QTextEdit()
        self.log_output.setReadOnly(True)
        self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
        self.log_output.setMaximumHeight(150)
        self.log_output.setText(self.INFO_TEXT)
        main_layout.addWidget(self.log_output)

        # Кнопки управления
        button_layout = QHBoxLayout()

        self.clear_cache_button = QPushButton("Очистить кеш")
        self.clear_cache_button.setStyleSheet("background-color: red; color: white;")
        self.clear_cache_button.clicked.connect(self.clear_winetricks_cache)
        button_layout.addWidget(self.clear_cache_button)

        button_layout.addStretch(1)

        self.apply_button = QPushButton("Применить")
        self.apply_button.setEnabled(False)
        self.apply_button.clicked.connect(self.apply_changes)
        button_layout.addWidget(self.apply_button)

        self.reinstall_button = QPushButton("Переустановить")
        self.reinstall_button.setEnabled(False)
        self.reinstall_button.clicked.connect(self.reinstall_selected)
        button_layout.addWidget(self.reinstall_button)

        self.close_button = QPushButton("Закрыть")
        self.close_button.clicked.connect(self.close)
        button_layout.addWidget(self.close_button)
        main_layout.addLayout(button_layout)

        # Подключаем сигнал после создания всех виджетов, чтобы избежать ошибки атрибута
        self.tabs.currentChanged.connect(self.on_tab_switched)

        # Загружаем данные
        self.load_all_categories()
        # Устанавливаем начальное состояние для отслеживания покинутой вкладки
        self.previous_tab_widget = self.tabs.currentWidget()

    def on_tab_switched(self, index):
        """
        Обрабатывает переключение вкладок.
        Если установка только что завершилась, сбрасывает лог к информационному тексту.
        """
        # Очищаем поле поиска на той вкладке, которую покинули.
        if self.previous_tab_widget:
            search_edit = self.previous_tab_widget.findChild(QLineEdit)
            if search_edit:
                search_edit.clear()

        if self.installation_finished:
            self.log_output.setText(self.INFO_TEXT)
            self.installation_finished = False
        self._update_ui_state()
        # Сохраняем текущую вкладку для следующего переключения
        self.previous_tab_widget = self.tabs.widget(index)

    def _create_category_tab(self, title):
        """Создает вкладку с поиском и списком."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        search_edit = QLineEdit()
        search_edit.setPlaceholderText("Поиск...")
        layout.addWidget(search_edit)

        list_widget = QListWidget()
        list_widget.itemChanged.connect(self._on_item_changed)
        list_widget.currentItemChanged.connect(self._update_ui_state)
        layout.addWidget(list_widget)

        search_edit.textChanged.connect(lambda text, lw=list_widget: self.filter_list(text, lw))

        self.tabs.addTab(tab, title)
        return list_widget, search_edit

    def filter_list(self, text, list_widget):
        """Фильтрует элементы в списке."""
        for i in range(list_widget.count()):
            item = list_widget.item(i)
            item.setHidden(text.lower() not in item.text().lower())

    def load_all_categories(self):
        """Запускает загрузку всех категорий."""
        self.loading_count = len(self.categories)
        for internal_name in self.categories.values():
            self._start_load_process(internal_name)

    def _get_cache_path(self, category):
        """Возвращает путь к файлу кэша для указанной категории."""
        return os.path.join(self.cache_dir, f"{category}.json")

    def _get_winetricks_hash(self):
        """Вычисляет хэш файла winetricks для проверки его обновления."""
        try:
            hasher = hashlib.sha256()
            with open(self.winetricks_path, 'rb') as f:
                while chunk := f.read(4096):
                    hasher.update(chunk)
            return hasher.hexdigest()
        except (IOError, OSError):
            return None

    def _start_load_process(self, category):
        """Запускает QProcess для получения списка компонентов, используя кэш."""
        cache_path = self._get_cache_path(category)
        cache_ttl_seconds = 86400  # 24 часа

        # Попытка прочитать из кэша
        if os.path.exists(cache_path):
            try:
                with open(cache_path, 'r', encoding='utf-8') as f:
                    cache_data = json.load(f)
                cache_age = time.time() - cache_data.get("timestamp", 0)
                winetricks_hash = self._get_winetricks_hash()
                if cache_age < cache_ttl_seconds and cache_data.get("hash") == winetricks_hash:
                    QTimer.singleShot(0, lambda: self._on_load_finished(
                        category, 0, QProcess.NormalExit, from_cache=cache_data.get("output")
                    ))
                    return
            except (json.JSONDecodeError, IOError, KeyError):
                self._log(f"--- Кэш для '{category}' поврежден, будет выполнена перезагрузка. ---")
        process = QProcess(self)
        self.processes[category] = process
        process.setProcessChannelMode(QProcess.MergedChannels)

        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", self.prefix_path)
        env.insert("WINE", self.wine_executable)
        # Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'.
        # Это известная проблема при запуске winetricks из ГУИ.
        process.setProcessEnvironment(env)

        # Используем functools.partial для надежной привязки категории к слоту.
        # Это стандартный и самый надежный способ избежать проблем с замыканием в цикле.
        process.finished.connect(partial(self._on_load_finished, category))
        process.start(self.winetricks_path, [category, "list"])

    def _parse_winetricks_log(self):
        """Читает winetricks.log и возвращает множество установленных компонентов."""
        installed_verbs = set()
        log_path = os.path.join(self.prefix_path, "winetricks.log")
        if not os.path.exists(log_path):
            return installed_verbs

        try:
            with open(log_path, 'r', encoding='utf-8') as f:
                for line in f:
                    verb = line.split('#', 1)[0].strip()
                    if verb:
                        installed_verbs.add(verb)
        except Exception as e:
            self._log(f"--- Предупреждение: не удалось прочитать {log_path}: {e} ---")
        return installed_verbs

    def _parse_winetricks_list_output(self, output, installed_verbs, list_widget, category):
        """Парсит вывод 'winetricks list' и заполняет QListWidget."""
        # Regex, который обрабатывает строки как с префиксом статуса '[ ]', так и без него.
        # 1. `(?:\[(.)]\s+)?` - опциональная группа для статуса (напр. '[x]').
        # 2. `([^\s]+)` - имя компонента (без пробелов).
        # 3. `(.*)` - оставшаяся часть строки (описание).

        # Определяем шаблоны для фильтрации на основе категории
        dlls_blacklist_pattern = None
        fonts_blacklist_pattern = None
        settings_blacklist_pattern = None

        if category == 'dlls':
            # Исключаем dont_use, dxvk*, vkd3d*, galliumnine, faudio*, Foundation
            dlls_blacklist_pattern = re.compile(
                r'^(dont_use|dxvk|vkd3d|galliumnine|faudio|foundation)', re.IGNORECASE
            )
        elif category == 'fonts':
            fonts_blacklist_pattern = re.compile(
                r'^(dont_use)', re.IGNORECASE
            )
        elif category == 'settings':
            # Исключаем vista*, alldlls, autostart_*, bad*, good*, win*, videomemory*, vd=*, isolate_home
            settings_blacklist_pattern = re.compile(
                r'^(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)', re.IGNORECASE
            )

        line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
        found_items = False

        for line in output.splitlines():
            match = line_re.match(line)
            if not match:
                continue

            found_items = True
            _status, name, description = match.groups()

            # Удаляем из описания информацию о доступности для скачивания, так как она избыточна
            description = re.sub(r'\[\s*доступно для скачивания[^]]*]', '', description)
            description = re.sub(r'\[\s*в кэше\s*]', '', description)

            # Фильтруем служебные строки, которые могут быть ошибочно распознаны.
            # Имена компонентов winetricks не содержат слэшей и не являются командами.
            if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
                continue

            # Применяем фильтры для черных списков
            if dlls_blacklist_pattern and dlls_blacklist_pattern.search(name):
                continue
            if fonts_blacklist_pattern and fonts_blacklist_pattern.search(name):
                continue
            if settings_blacklist_pattern and settings_blacklist_pattern.search(name):
                continue

            is_checked = name in installed_verbs
            item_text = f"{name.ljust(27)}{description.strip()}"
            item = QListWidgetItem(item_text)
            item.setData(Qt.UserRole, name)
            item.setFont(QFont("DejaVu Sans Mono", 10))
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked if is_checked else Qt.Unchecked)
            list_widget.addItem(item)
            self.initial_states[name] = is_checked

        return found_items

    def _on_load_finished(self, category, exit_code, exit_status, from_cache=None):
        """Обрабатывает завершение загрузки списка компонентов."""
        if from_cache is not None:
            output = from_cache
            process = None
        else:
            process = self.processes[category]
            output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')

        list_widget = self.list_widgets[category]
        category_display_name = next(k for k, v in self.categories.items() if v == category)

        # Очищаем список перед заполнением.
        list_widget.clear()

        if exit_code != 0 or exit_status != QProcess.NormalExit:
            error_string = process.errorString() if process else "N/A"
            self._log(f"--- Ошибка загрузки категории '{category_display_name}' (код: {exit_code}) ---", "red")
            if exit_status == QProcess.CrashExit:
                self._log("--- Процесс winetricks завершился аварийно. ---", "red")
            if error_string != "Неизвестная ошибка":
                self._log(f"--- Системная ошибка: {error_string} ---", "red")
            self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.")
            self._log("--------------------------------------------------", "red")
        else:
            installed_verbs = self._parse_winetricks_log()
            found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget, category)

            if from_cache is None:  # Только если мы не читали из кэша
                # Сохраняем успешный результат в кэш
                cache_path = self._get_cache_path(category)
                winetricks_hash = self._get_winetricks_hash()
                if winetricks_hash:
                    cache_data = {
                        "timestamp": time.time(),
                        "hash": winetricks_hash,
                        "output": output
                    }
                    try:
                        with open(cache_path, 'w', encoding='utf-8') as f:
                            json.dump(cache_data, f, ensure_ascii=False, indent=2)
                    except (IOError, OSError) as e:
                        self._log(f"--- Не удалось сохранить кэш для '{category}': {e} ---")
            if not found_items and output.strip():
                self._log(f"--- Не удалось распознать вывод для категории '{category}' ---")
                self._log(output)
                self._log("--------------------------------------------------")

        # Проверяем, были ли ошибки во время загрузки какой-либо из категорий
        has_errors = any(status == 'error' for status in self.category_statuses.values())

        self.loading_count -= 1
        if self.loading_count == 0:
            self._update_ui_state()
            if self.is_reloading_after_cache_clear:
                if has_errors:
                    self._log("\n=== Ошибка при обновлении списков. Проверьте лог выше. ===", "red")
                else:
                    self._log("\n=== Списки успешно обновлены ===")
                self.is_reloading_after_cache_clear = False # Сбрасываем флаг

    def _on_item_changed(self, item):
        """Обрабатывает изменение состояния чекбокса, предотвращая снятие галочки с установленных."""
        name = item.data(Qt.UserRole)
        # Если компонент был изначально установлен и пользователь пытается его снять
        if name in self.initial_states and self.initial_states.get(name) is True:
            if item.checkState() == Qt.Unchecked:
                # Блокируем сигналы, чтобы избежать рекурсии, и возвращаем галочку на место.
                list_widget = item.listWidget()
                if list_widget:
                    list_widget.blockSignals(True)
                item.setCheckState(Qt.Checked)
                if list_widget:
                    list_widget.blockSignals(False)
        self._update_ui_state()

    def _update_ui_state(self, *args):
        """Централизованно обновляет состояние кнопок 'Применить' и 'Переустановить'."""
        # 1. Проверяем, есть ли изменения в чекбоксах (установка новых или снятие галочек с новых)
        has_changes = False
        for list_widget in self.list_widgets.values():
            for i in range(list_widget.count()):
                item = list_widget.item(i)
                name = item.data(Qt.UserRole)
                if name in self.initial_states:
                    initial_state = self.initial_states[name]
                    current_state = item.checkState() == Qt.Checked
                    if current_state != initial_state:
                        has_changes = True
                        break
            if has_changes:
                break

        self.apply_button.setEnabled(has_changes)

        # 2. Проверяем, можно ли переустановить выбранный компонент
        is_reinstallable = False
        # Переустановка возможна только если нет других изменений
        if not has_changes:
            current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
            if current_list_widget:
                current_item = current_list_widget.currentItem()
                if current_item:
                    name = current_item.data(Qt.UserRole)
                    # Компонент можно переустановить, если он был изначально установлен
                    if self.initial_states.get(name, False):
                        is_reinstallable = True

        self.reinstall_button.setEnabled(is_reinstallable)

    def reinstall_selected(self):
        """Переустанавливает выбранный компонент."""
        current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
        if not current_list_widget:
            return

        current_item = current_list_widget.currentItem()
        if not current_item:
            return

        name = current_item.data(Qt.UserRole)
        if not name:
            return

        self.log_output.setText(self.INFO_TEXT)
        self.apply_button.setEnabled(False)
        self.reinstall_button.setEnabled(False)
        self.close_button.setEnabled(False)

        # Установка будет форсированной
        verbs_to_reinstall = [name]
        self._start_install_process(verbs_to_reinstall)

    def apply_changes(self):
        """Применяет выбранные изменения."""
        # Собираем все компоненты, которые были отмечены для установки.
        verbs_to_install = []

        for list_widget in self.list_widgets.values():
            for i in range(list_widget.count()):
                item = list_widget.item(i)
                name = item.data(Qt.UserRole)
                if name not in self.initial_states:
                    continue

                initial_state = self.initial_states[name]
                current_state = item.checkState() == Qt.Checked

                if current_state != initial_state:
                    verbs_to_install.append(name)

        if not verbs_to_install:
            QMessageBox.information(self, "Нет изменений", "Не выбрано ни одного компонента для установки.")
            return

        self.log_output.setText(self.INFO_TEXT)
        self.apply_button.setEnabled(False)
        self.reinstall_button.setEnabled(False)
        self.close_button.setEnabled(False)

        self._start_install_process(verbs_to_install)

    def _start_install_process(self, verbs_to_install):
        """Запускает процесс установки/переустановки winetricks."""
        # Добавляем флаг --force, чтобы разрешить переустановку
        self._log(f"Выполнение установки: winetricks --unattended --force {' '.join(verbs_to_install)}")
        self.apply_process = QProcess(self)
        self.apply_process.setProcessChannelMode(QProcess.MergedChannels)
        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", self.prefix_path)
        env.insert("WINE", self.wine_executable)
        self.apply_process.setProcessEnvironment(env)
        self.apply_process.readyReadStandardOutput.connect(lambda: self.log_output.append(self.apply_process.readAllStandardOutput().data().decode('utf-8', 'ignore').strip()))
        self.apply_process.finished.connect(self.on_apply_finished)
        self.apply_process.start(self.winetricks_path, ["--unattended", "--force"] + verbs_to_install)

    def on_apply_finished(self, exit_code, exit_status):
        """Обрабатывает завершение применения изменений."""
        # 1. Проверяем, была ли отмена пользователем
        if self.user_cancelled:
            self._log("\n=== Установка прервана пользователем. ===")
            # Процесс завершен, теперь можно безопасно закрыть диалог.
            self.close()
            return

        # 2. Обрабатываем реальную ошибку
        if exit_code != 0 or exit_status != QProcess.NormalExit:
            self._log(f"\n=== Ошибка во время выполнения операций (код: {exit_code}). ===", "red")
            self._show_message_box("Ошибка",
                                   "Произошла ошибка во время выполнения операций.\n"
                                   "Подробности смотрите в логе.",
                                   QMessageBox.Warning,
                                   {"buttons": {"OK": QMessageBox.AcceptRole}})
            # Сбрасываем формат символов к значению по умолчанию.
            # Это гарантирует, что следующий вызов append() не унаследует красный цвет.
            self.log_output.setCurrentCharFormat(QTextCharFormat())
            self.apply_button.setEnabled(True)
            self.close_button.setEnabled(True)
            return

        # 3. Обрабатываем успех
        self._log("\n=== Все операции успешно завершены ===")
        self.apply_button.setEnabled(True)
        self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние
        self.close_button.setEnabled(True)

        # Очищаем все поля поиска.
        for search_edit in self.search_edits.values():
            search_edit.clear()

        # Перезагружаем данные, чтобы обновить состояние
        self.initial_states.clear()
        self.load_all_categories()
        self.installation_complete.emit()
        self.installation_finished = True

    def clear_winetricks_cache(self):
        """Запускает очистку кэша Winetricks."""
        reply = self._show_message_box(
            "Очистка кэша Winetricks",
            "Вы собираетесь удалить все скачанные установщики и списки компонентов Winetricks.\n\n"
            "Это действие может потребоваться, если у вас возникают проблемы со скачиванием или установкой компонентов.\n\n"
            "Продолжить?",
            QMessageBox.Question,
            {"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}
        )

        if reply != "Да":
            return

        # Блокируем UI на время выполнения
        self.tabs.setEnabled(False)
        self.clear_cache_button.setEnabled(False)
        self.apply_button.setEnabled(False)
        self.reinstall_button.setEnabled(False)
        self.close_button.setEnabled(False)

        self.log_output.clear()

        self.cache_clear_process = QProcess(self)
        self.cache_clear_process.setProcessChannelMode(QProcess.MergedChannels)

        def handle_output():
            output = self.cache_clear_process.readAll().data().decode('utf-8', 'ignore').strip()
            if output:
                self._log(output)

        def handle_finish(exit_code, exit_status):
            if exit_code == 0:
                self.is_reloading_after_cache_clear = True # Устанавливаем флаг перед перезагрузкой
                self.category_statuses.clear() # Очищаем статусы перед новой загрузкой
                # Воссоздаем директорию кэша, так как скрипт ее полностью удалил
                os.makedirs(self.cache_dir, exist_ok=True)
                self._log("Обновление списков...")
                self.initial_states.clear()
                self.load_all_categories()
            else:
                self._log(f"\n=== Ошибка (код: {exit_code}) ===", "red")

            # Восстанавливаем UI
            self.tabs.setEnabled(True)
            self.clear_cache_button.setEnabled(True)
            self.close_button.setEnabled(True)
            self._update_ui_state() # Обновляем состояние кнопок Применить/Переустановить

        self.cache_clear_process.readyRead.connect(handle_output)
        self.cache_clear_process.finished.connect(handle_finish)

        winehelper_path = self.parent().winehelper_path if hasattr(self.parent(), 'winehelper_path') else Var.RUN_SCRIPT
        args = ["clear-winetricks-cache", "--force"]

        self._log(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(args)}\n")
        self.cache_clear_process.start(winehelper_path, args)

    def closeEvent(self, event):
        """Обрабатывает закрытие окна, чтобы предотвратить выход во время установки."""
        # Проверяем, запущен ли процесс установки/переустановки
        if self.apply_process and self.apply_process.state() == QProcess.Running:
            reply = self._show_message_box('Подтверждение',
                                           "Процесс установки еще не завершен. Вы уверены, что хотите прервать его?",
                                           QMessageBox.Question,
                                           {"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"})
            if reply == "Да":
                self.user_cancelled = True
                self.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===")
                self.apply_process.terminate()  # Попытка мягкого завершения
                # Запрещаем закрытие. on_apply_finished обработает его после завершения процесса.
                event.ignore()
            else:
                event.ignore()  # Запрещаем закрытие
        else:
            event.accept()  # Процесс не запущен, можно закрывать

    def _show_message_box(self, title, text, icon, config):
        """Централизованный метод для создания и показа QMessageBox."""
        msg_box = QMessageBox(self)
        msg_box.setWindowTitle(title)
        msg_box.setText(text)
        msg_box.setIcon(icon)

        buttons = {}
        for btn_text, role in config.get("buttons", {}).items():
            buttons[btn_text] = msg_box.addButton(btn_text, role)

        default_btn_text = config.get("default")
        if default_btn_text and default_btn_text in buttons:
            msg_box.setDefaultButton(buttons[default_btn_text])

        msg_box.exec_()

        clicked_button = msg_box.clickedButton()
        return clicked_button.text() if clicked_button else None

    def _log(self, message, color=None):
        """Добавляет сообщение в лог с возможностью указания цвета."""
        if color:
            self.log_output.append(f'<span style="color:{color};">{message}</span>')
            # Сбрасываем формат, чтобы следующий текст не наследовал цвет
            self.log_output.setCurrentCharFormat(QTextCharFormat())
        else:
            self.log_output.append(message)
        self.log_output.moveCursor(QTextCursor.End)

class ScriptParser:
    """Утилитарный класс для парсинга информации из скриптов установки."""

    @staticmethod
    def extract_icons_from_script(script_path):
        """
        Извлекает иконку для скрипта.
        Сначала ищет переменную 'export PROG_ICON=', если не находит,
        то ищет все вызовы 'create_desktop' и берет иконки из третьего аргумента.
        Возвращает список имен иконок.
        """
        try:
            with open(script_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            # 1. Приоритет у переменной ICONS_BUTTON_GUI для GUI
            for line in lines:
                if line.strip().startswith('export ICONS_BUTTON_GUI='):
                    icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
                    if icon_names_str:
                        return icon_names_str.split()

            # 2. Если ICONS_BUTTON_GUI не найдена, ищем PROG_ICON (для обратной совместимости)
            for line in lines:
                if line.strip().startswith('export PROG_ICON='):
                    icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
                    if icon_names_str:
                        return icon_names_str.split()

            # 3. Если ничего не найдено, ищем все вызовы create_desktop
            icon_names = []
            for line in lines:
                line = line.strip()
                # Пропускаем закомментированные строки и пустые строки
                if not line or line.startswith('#'):
                    continue

                if 'create_desktop' in line:
                    try:
                        parts = shlex.split(line)
                        # Ищем все вхождения, а не только первое
                        for i, part in enumerate(parts):
                            if part == 'create_desktop':
                                if len(parts) > i + 3:
                                    icon_name = parts[i + 3]
                                    # Игнорируем служебные слова, которые не являются иконками
                                    if icon_name and icon_name.lower() not in ('auto', 'nocopy'):
                                        icon_names.append(icon_name)
                    except (ValueError, IndexError):
                        continue
            return icon_names
        except Exception as e:
            print(f"Ошибка чтения файла для извлечения иконки: {str(e)}")
            return []

    @staticmethod
    def extract_prog_name_from_script(script_path):
        """Извлекает имя программы из строки PROG_NAME= в скрипте"""
        try:
            with open(script_path, 'r', encoding='utf-8') as f:
                for line in f:
                    if line.strip().startswith(('export PROG_NAME=', 'PROG_NAME=')):
                        name = line.split('=', 1)[1].strip().strip('"\'')
                        if name:
                            return name
            return None
        except Exception as e:
            print(f"Ошибка чтения файла для извлечения PROG_NAME: {str(e)}")
            return None

    @staticmethod
    def extract_prog_url_from_script(script_path):
        """Извлекает URL из строки export PROG_URL= в скрипте"""
        try:
            with open(script_path, 'r', encoding='utf-8') as f:
                for line in f:
                    if line.startswith('export PROG_URL='):
                        return line.replace('export PROG_URL=', '').strip().strip('"\'')
            return None
        except Exception as e:
            print(f"Ошибка чтения файла для извлечения PROG_URL: {str(e)}")
            return None

    @staticmethod
    def extract_info_ru(script_path):
        """Извлекает информацию из строки # info_ru: в скрипте"""
        try:
            with open(script_path, 'r', encoding='utf-8') as f:
                for line in f:
                    if line.startswith('# info_ru:'):
                        return line.replace('# info_ru:', '').strip()
            return "Описание отсутствует"
        except Exception as e:
            return f"Ошибка чтения файла: {str(e)}"

class WineVersionSelectionDialog(QDialog):
    """Диалог для выбора версии Wine/Proton с группировкой."""

    def __init__(self, architecture, parent=None):
        super().__init__(parent)
        self.architecture = architecture
        self.selected_version = None
        self.wine_versions_data = {}
        self.system_wine_display_name = "Системная версия"
        self.selected_display_text = None

        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.main_tabs = QTabWidget()
        main_layout.addWidget(self.main_tabs)

        self.load_versions()

    def load_versions(self):
        """Запускает процесс получения списка версий Wine."""
        self.main_tabs.clear()
        loading_widget = QWidget()
        loading_layout = QVBoxLayout(loading_widget)
        status_label = QLabel("Загрузка, пожалуйста, подождите...")
        status_label.setAlignment(Qt.AlignCenter)
        loading_layout.addWidget(status_label)
        self.main_tabs.addTab(loading_widget, "Загрузка...")
        self.main_tabs.setEnabled(False)

        QApplication.processEvents()

        self._parse_sha256_list()
        self.populate_ui()

        self.main_tabs.setEnabled(True)

    def _parse_sha256_list(self):
        """Парсит sha256sum.list для получения списка версий."""
        sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list")
        if not os.path.exists(sha256_path):
            QMessageBox.warning(self, "Ошибка", f"Файл с версиями не найден:\n{sha256_path}")
            self.wine_versions_data = {}
            return

        self.wine_versions_data = {}
        current_group = None

        try:
            with open(sha256_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue

                    match = re.match(r'^#+\s*(.*?)\s*#*$', line)
                    if match:
                        group_name = match.group(1).strip()
                        allowed_groups = {"WINE WOW64", "WINE AMD64", "WINE I586"}
                        # Отображаем только группы, которые являются сборками WINE или PROTON
                        if group_name in allowed_groups:
                            current_group = group_name
                            if current_group not in self.wine_versions_data:
                                self.wine_versions_data[current_group] = []
                        else:
                            current_group = None
                        continue

                    if current_group and re.match(r'^[a-f0-9]{64}', line):
                        parts = line.split(maxsplit=1)
                        if len(parts) == 2:
                            filename = parts[1]
                            if filename.endswith('.tar.xz'):
                                version_name = filename[:-7]
                                self.wine_versions_data[current_group].append(version_name)
        except IOError as e:
            QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать файл версий:\n{e}")
            self.wine_versions_data = {}

    def _get_installed_versions(self):
        """Возвращает список локально установленных версий Wine."""
        dist_path = os.path.join(Var.USER_WORK_PATH, "dist")
        if not os.path.isdir(dist_path):
            return []
        try:
            return sorted([
                name for name in os.listdir(dist_path)
                if os.path.isdir(os.path.join(dist_path, name))
            ], reverse=True)
        except OSError:
            return []

    def populate_ui(self):
        """Заполняет UI отфильтрованными версиями."""
        self.main_tabs.clear()

        # --- Вкладка "Установленные" ---
        installed_tab = QWidget()
        self.main_tabs.addTab(installed_tab, "Установленные")
        installed_layout = QVBoxLayout(installed_tab)
        installed_scroll_area = QScrollArea()
        installed_scroll_area.setWidgetResizable(True)
        installed_layout.addWidget(installed_scroll_area)
        installed_content = QWidget()
        installed_scroll_area.setWidget(installed_content)
        self.installed_grid_layout = QGridLayout(installed_content)
        self.installed_grid_layout.setAlignment(Qt.AlignTop)

        installed_versions_for_grid = []

        # Системная версия
        if shutil.which('wine'):
            try:
                result = subprocess.run(['wine', '--version'], capture_output=True, text=True, check=True, encoding='utf-8')
                self.system_wine_display_name = result.stdout.strip()
            except (FileNotFoundError, subprocess.CalledProcessError) as e:
                print(f"Не удалось получить версию системного wine: {e}")
            installed_versions_for_grid.append((self.system_wine_display_name, "system"))

        # Локально установленные версии
        local_versions = self._get_installed_versions()
        installed_versions_for_grid.extend(local_versions)

        self._create_grid_buttons(self.installed_grid_layout, installed_versions_for_grid)

        # --- Вкладка "Скачать" ---
        download_tab = QWidget()
        download_layout = QVBoxLayout(download_tab)
        self.version_tabs = QTabWidget() # Вложенные табы для категорий
        download_layout.addWidget(self.version_tabs)
        download_tab.setLayout(download_layout)
        self.main_tabs.addTab(download_tab, "Скачать")

        if not self.wine_versions_data:
            error_widget = QWidget()
            error_layout = QVBoxLayout(error_widget)
            error_label = QLabel("Список версий пуст или не удалось его загрузить.")
            error_label.setAlignment(Qt.AlignCenter)
            error_layout.addWidget(error_label)
            self.version_tabs.addTab(error_widget, "Ошибка")
            self.filter_versions()
            return

        # --- Фильтрация и сортировка групп в зависимости от архитектуры ---
        is_win64 = self.architecture == "win64"

        if is_win64:
            allowed_groups_for_arch = {"WINE AMD64", "WINE WOW64"}
            tab_order = ["WINE AMD64", "WINE WOW64"]
        else:  # win32
            allowed_groups_for_arch = {"WINE I586","WINE AMD64"}
            tab_order = ["WINE I586","WINE AMD64"]

        # Получаем только те ключи, которые есть в данных и разрешены для данной архитектуры
        available_keys = [key for key in self.wine_versions_data.keys() if key in allowed_groups_for_arch]
        # Сортируем доступные ключи в соответствии с заданным порядком
        group_keys = sorted(available_keys, key=lambda k: tab_order.index(k))

        for key in group_keys:
            versions = self.wine_versions_data.get(key, [])
            if not versions:
                continue
            self._create_version_tab(key, sorted(versions, reverse=True))

        self.filter_versions()

    def _create_version_tab(self, title, versions_list, is_download_tab=True):
        """Создает вкладку с сеткой кнопок для переданного списка версий."""
        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)

        self._create_grid_buttons(grid_layout, versions_list, is_download_tab=is_download_tab)
        self.version_tabs.addTab(tab_page, title)

    def _create_grid_buttons(self, grid_layout, versions_list, is_download_tab=False):
        is_win64_prefix = self.architecture == "win64"
        is_win32_prefix = self.architecture == "win32"
        re_32bit_version = re.compile(r'i[3-6]86|i586')

        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))

            # Выделяем системную версию Wine
            if value_name == "system":
                btn.setText(f"{display_name} (Системный)")
                btn.setToolTip("Системная версия Wine, установленная в вашей ОС.")

            # Отключение 32-битных версий Wine при выбранном 64-битного префикса (вкладка "Установленные")
            if not is_download_tab and is_win64_prefix:
                if re_32bit_version.search(value_name):
                    btn.setEnabled(False)
                    btn.setToolTip("Эта 32-битная версия Wine несовместима с 64-битным префиксом.")

            # Отключение 64-битных версий Wine (системной и wow64) при выборе 32-битного префикса
            if not is_download_tab and is_win32_prefix:
                if value_name == "system" or 'wow64' in value_name:
                    btn.setEnabled(False)
                    btn.setToolTip("Эта 64-битная версия Wine несовместима с 32-битным префиксом.")

            grid_layout.addWidget(btn, row, col)
            col += 1
            if col >= num_columns:
                col = 0
                row += 1

    def filter_versions(self):
        """Фильтрует видимость кнопок версий на основе текста поиска."""
        search_text = self.search_edit.text().lower()

        # Фильтр для вкладки "Установленные"
        if hasattr(self, 'installed_grid_layout'):
            self._filter_grid(self.installed_grid_layout, search_text)

        # Фильтр для вкладок "Скачать"
        if hasattr(self, 'version_tabs'):
            for i in range(self.version_tabs.count()):
                tab_widget = self.version_tabs.widget(i)
                grid_layout = tab_widget.findChild(QGridLayout)
                if grid_layout:
                    any_visible = self._filter_grid(grid_layout, search_text)
                    self.version_tabs.setTabEnabled(i, any_visible)

    def _filter_grid(self, grid_layout, search_text):
        """Helper-функция для фильтрации кнопок в одной сетке."""
        any_visible_in_grid = False
        if not grid_layout:
            return False

        for j in range(grid_layout.count()):
            item = grid_layout.itemAt(j)
            if item:
                btn_widget = item.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_grid = True

        return any_visible_in_grid

    def on_version_selected(self, version_name):
        """Обрабатывает выбор версии."""
        self.selected_version = version_name
        if version_name == 'system':
            self.selected_display_text = self.system_wine_display_name
        else:
            self.selected_display_text = version_name
        self.accept()

class CreatePrefixDialog(QDialog):
    """Диалог для создания нового префикса."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent_gui = parent  # Сохранить ссылку на главное окно
        self.setWindowTitle("Создание нового префикса")
        self.setMinimumSize(680, 250)
        self.setModal(True)

        # Attributes to store results
        self.prefix_name = None
        self.wine_arch = None
        self.base_pfx = None
        self.prepared_prefixes = {}
        self.selected_wine_version_value = None
        self.selected_wine_version_display = None

        layout = QVBoxLayout(self)
        form_layout = QFormLayout()
        form_layout.setSpacing(10)

        # Создаем виджет для поля ввода и предупреждения
        name_input_widget = QWidget()
        name_input_layout = QVBoxLayout(name_input_widget)
        name_input_layout.setContentsMargins(0, 0, 0, 0)
        name_input_layout.setSpacing(2)

        self.prefix_name_edit = QLineEdit()
        self.prefix_name_edit.setPlaceholderText("Например: my_prefix")
        name_input_layout.addWidget(self.prefix_name_edit)

        self.name_warning_label = QLabel("Имя может содержать только латинские буквы, цифры, тире и знаки подчеркивания.")
        self.name_warning_label.setStyleSheet("color: red;")
        self.name_warning_label.setVisible(False)
        name_input_layout.addWidget(self.name_warning_label)

        form_layout.addRow("<b>Имя нового префикса:</b>", name_input_widget)

        arch_widget = QWidget()
        arch_layout = QHBoxLayout(arch_widget)
        arch_layout.setContentsMargins(0, 0, 0, 0)
        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)
        form_layout.addRow("<b>Разрядность:</b>", arch_widget)

        # Виджет для выбора типа префикса (шаблона)
        self.prefix_template_selector = QComboBox()
        self.prefix_template_selector.setToolTip("Выберите шаблон для создания префикса.")
        form_layout.addRow("<b>Шаблон префикса:</b>", self.prefix_template_selector)

        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("<b>Версия Wine/Proton:</b>", version_layout)

        layout.addLayout(form_layout)

        # Buttons
        button_layout = QHBoxLayout()
        self.create_button = QPushButton("Создать")
        self.create_button.setFont(QFont('Arial', 11, QFont.Bold))
        self.create_button.setStyleSheet("background-color: #0078d7; color: white;")
        self.create_button.setEnabled(False)
        self.create_button.clicked.connect(self.accept_creation)

        cancel_button = QPushButton("Отмена")
        cancel_button.clicked.connect(self.reject)

        button_layout.addStretch()
        button_layout.addWidget(self.create_button)
        button_layout.addWidget(cancel_button)
        layout.addLayout(button_layout)

        # Connect signals
        self.arch_win32_radio.toggled.connect(self.on_architecture_changed)
        self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
        self.wine_version_edit.textChanged.connect(self.update_create_button_state)

        # Загружаем и настраиваем шаблоны префиксов
        self._load_prepared_prefixes()
        self.on_architecture_changed()

    def _load_prepared_prefixes(self):
        """Загружает и парсит шаблоны префиксов из sha256sum.list."""
        self.prepared_prefixes = {'win32': [], 'win64': []}
        sha256_file = os.path.join(Var.DATA_PATH, "sha256sum.list")
        if not os.path.exists(sha256_file):
            QMessageBox.warning(self, "Ошибка", f"Файл с описаниями префиксов не найден: {sha256_file}")
            return

        in_prefix_section = False
        current_description = ""
        current_prefix_name = None

        try:
            with open(sha256_file, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if "##### PREFIX #####" in line:
                        in_prefix_section = True
                        continue
                    elif line.startswith("#####"):
                        in_prefix_section = False
                        continue

                    if not in_prefix_section:
                        continue

                    if re.match(r'^[a-f0-9]{64}', line):
                        # Если у нас есть имя и описание для предыдущего префикса, добавляем его
                        if current_prefix_name and current_description:
                            arch_for_prev_prefix = None
                            if "_x86_" in current_prefix_name or "_i586_" in current_prefix_name:
                                arch_for_prev_prefix = 'win32'
                            elif "_x64_" in current_prefix_name or "_amd64_" in current_prefix_name:
                                arch_for_prev_prefix = 'win64'

                            if arch_for_prev_prefix:
                                description_to_add = current_description.strip().replace('\\n', '\n')
                                self.prepared_prefixes[arch_for_prev_prefix].append((current_prefix_name, description_to_add))

                        # Сбрасываем описание для нового префикса
                        current_description = ""

                        # Получаем имя нового префикса
                        current_prefix_name = line.split(maxsplit=1)[1].replace('.tar.xz', '') if len(line.split(maxsplit=1)) > 1 else None

                    elif line.startswith('# '):
                        # Накапливаем описание
                        comment_line = line[2:].strip()
                        if current_description:
                            current_description += "\n" + comment_line
                        else:
                            current_description = comment_line

            # После цикла добавляем описание для самого последнего префикса, если оно есть
            if current_prefix_name and current_description:
                arch = None
                if "_x86_" in current_prefix_name or "_i586_" in current_prefix_name: arch = 'win32'
                elif "_x64_" in current_prefix_name or "_amd64_" in current_prefix_name: arch = 'win64'
                if arch:
                    self.prepared_prefixes[arch].append((current_prefix_name, current_description.strip().replace('\\n', '\n')))

        except IOError as e:
            QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать файл с описаниями префиксов: {e}")

        # Добавляем опцию "Чистый префикс" для обеих архитектур
        self.prepared_prefixes['win32'].insert(0, ('none', 'Создать чистый префикс без дополнительных библиотек'))
        self.prepared_prefixes['win64'].insert(0, ('none', 'Создать чистый префикс без дополнительных библиотек'))

    def open_wine_version_dialog(self):
        """Открывает диалог выбора версии Wine."""
        architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
        dialog = WineVersionSelectionDialog(architecture, self)
        if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
            self.wine_version_edit.setText(dialog.selected_display_text)
            self.selected_wine_version_value = dialog.selected_version

    def on_architecture_changed(self):
        """Обновляет список шаблонов и сбрасывает выбор версии Wine при смене архитектуры."""
        # Сбрасываем выбор версии
        self.wine_version_edit.clear()
        self.selected_wine_version_value = None

        # Обновляем список шаблонов
        self.prefix_template_selector.blockSignals(True)
        self.prefix_template_selector.clear()

        architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
        templates = self.prepared_prefixes.get(architecture, [])

        for value, description in templates:
            # Для 'none' используем более понятное имя
            display_name = "Чистый префикс" if value == "none" else value
            self.prefix_template_selector.addItem(display_name, userData={'value': value, 'tooltip': description})

        self.prefix_template_selector.blockSignals(False)
        # Обновляем тултип при смене элемента
        self.prefix_template_selector.currentIndexChanged.connect(lambda i: self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(i)['tooltip']))
        self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(0)['tooltip'])

    def validate_prefix_name(self, text):
        """Проверяет имя префикса в реальном времени и показывает/скрывает предупреждение."""
        valid_pattern = r'^[a-zA-Z0-9_-]*$'
        if re.match(valid_pattern, text):
            self.name_warning_label.setVisible(False)
        else:
            # Удаляем недопустимые символы
            cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
            # Блокируем сигналы, чтобы избежать рекурсии при изменении текста
            self.prefix_name_edit.blockSignals(True)
            self.prefix_name_edit.setText(cleaned_text)
            self.prefix_name_edit.blockSignals(False)
            self.name_warning_label.setVisible(True)

        self.update_create_button_state()

    def update_create_button_state(self):
        """Включает или выключает кнопку 'Создать'."""
        name_ok = bool(self.prefix_name_edit.text().strip())
        version_ok = bool(self.wine_version_edit.text().strip())
        # Кнопка активна, только если имя валидно и версия выбрана
        self.create_button.setEnabled(name_ok and version_ok and not self.name_warning_label.isVisible())

    def accept_creation(self):
        """Валидирует данные, сохраняет их и закрывает диалог с успехом."""
        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

        # Save data
        self.prefix_name = prefix_name
        self.wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64"

        # Получаем значение `BASE_PFX` из выбранного шаблона
        current_index = self.prefix_template_selector.currentIndex()
        self.base_pfx = self.prefix_template_selector.itemData(current_index)['value']
        self.selected_wine_version_display = self.wine_version_edit.text()

        self.accept()

class FileAssociationsDialog(QDialog):
    """Диалог для управления ассоциациями файлов (WH_XDG_OPEN)."""

    def __init__(self, current_associations, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Настройка ассоциаций файлов")
        self.setMinimumWidth(450)
        self.setModal(True)

        self.new_associations = current_associations

        layout = QVBoxLayout(self)
        layout.setSpacing(10)  # Добавляем вертикальный отступ между виджетами

        info_label = QLabel(
            "Укажите расширения файлов, которые должны открываться нативными<br>"
            "приложениями Linux. Чтобы удалить все ассоциации, очистите поле.<br><br>"
            "<b>Пример:</b> <code>pdf docx txt</code>"
        )
        info_label.setWordWrap(True)
        info_label.setTextFormat(Qt.RichText)
        layout.addWidget(info_label)

        self.associations_edit = QLineEdit()
        # Если ассоциации не заданы (значение "0"), поле будет пустым, чтобы показать подсказку
        if current_associations != "0":
            self.associations_edit.setText(current_associations)

        self.associations_edit.setPlaceholderText("Введите расширения через пробел...")
        layout.addWidget(self.associations_edit)

        # Запрещенные расширения
        forbidden_label = QLabel(
            "<small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small>"
        )
        forbidden_label.setTextFormat(Qt.RichText)  # Включаем обработку HTML
        layout.addWidget(forbidden_label)

        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.validate_and_accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

    def validate_and_accept(self):
        """Проверяет введенные данные перед закрытием."""
        forbidden_extensions = {"cpl", "dll", "exe", "lnk", "msi"}

        # Получаем введенные расширения, очищаем от лишних пробелов
        input_text = self.associations_edit.text().lower().strip()
        entered_extensions = {ext.strip() for ext in input_text.split() if ext.strip()}

        found_forbidden = entered_extensions.intersection(forbidden_extensions)

        if found_forbidden:
            msg_box = QMessageBox(self)
            msg_box.setIcon(QMessageBox.Warning)
            msg_box.setWindowTitle("Недопустимые расширения")
            msg_box.setTextFormat(Qt.RichText)
            msg_box.setText(
                "Следующие расширения запрещены и не могут быть использованы:<br><br>"
                f"<b>{', '.join(sorted(list(found_forbidden)))}</b>"
            )
            msg_box.exec_()
            return

        # Сохраняем результат в виде отсортированной строки
        self.new_associations = " ".join(sorted(list(entered_extensions)))
        self.accept()

class ComponentVersionSelectionDialog(QDialog):
    """Диалог для выбора версии компонента (DXVK, VKD3D)."""

    def __init__(self, component_group, title, parent=None, add_extra_options=True):
        super().__init__(parent)
        self.component_group = component_group
        self.selected_version = None
        self.versions_data = []

        self.setWindowTitle(title)
        self.setMinimumSize(600, 400)
        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.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        main_layout.addWidget(self.scroll_area)

        scroll_content = QWidget()
        self.scroll_area.setWidget(scroll_content)

        self.grid_layout = QGridLayout(scroll_content)
        self.grid_layout.setAlignment(Qt.AlignTop)

        self.buttons = []
        # Кнопка "Удалить" теперь находится вне сетки, поэтому начинаем с 0 строки.
        self.load_versions(start_row=0)

        # --- Панель с кнопками действий внизу диалога ---
        button_layout = QHBoxLayout()

        if add_extra_options:
            uninstall_btn = QPushButton("Удалить из префикса")
            uninstall_btn.setToolTip("Удаляет текущую установленную версию компонента из префикса.")
            uninstall_btn.clicked.connect(partial(self.on_version_selected, "none"))
            # Добавляем кнопку слева
            button_layout.addWidget(uninstall_btn)

        button_layout.addStretch(1)  # Растягиваем пространство, чтобы разнести кнопки

        cancel_button = QPushButton("Отмена")
        cancel_button.clicked.connect(self.reject)
        button_layout.addWidget(cancel_button)

        main_layout.addLayout(button_layout)

    def load_versions(self, start_row):
        """Загружает и отображает версии."""
        self._parse_sha256_list()
        self.populate_ui(start_row)

    def _parse_sha256_list(self):
        """Парсит sha256sum.list для получения списка версий."""
        sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list")
        if not os.path.exists(sha256_path):
            self.versions_data = []
            return

        current_group = None
        try:
            with open(sha256_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    match = re.match(r'^#+\s*([^#]+?)\s*#*$', line)
                    if match:
                        current_group = match.group(1).strip()
                        continue
                    if current_group == self.component_group and re.match(r'^[a-f0-9]{64}', line):
                        parts = line.split(maxsplit=1)
                        if len(parts) == 2:
                            filename = parts[1]
                            if filename.endswith('.tar.xz'):
                                version_name = filename[:-7]
                                self.versions_data.append(version_name)
        except IOError:
            self.versions_data = []

    def populate_ui(self, start_row):
        """Заполняет UI кнопками версий."""
        versions = sorted(self.versions_data, reverse=True)
        num_columns = 3
        row, col = start_row, 0
        for version_name in versions:
            btn = QPushButton(version_name)
            btn.clicked.connect(partial(self.on_version_selected, version_name))
            self.grid_layout.addWidget(btn, row, col)
            self.buttons.append(btn)
            col += 1
            if col >= num_columns:
                col = 0
                row += 1

    def filter_versions(self):
        """Фильтрует видимость кнопок версий и перестраивает сетку для плотного отображения."""
        search_text = self.search_edit.text().lower()

        visible_buttons = []
        for btn_widget in self.buttons:
            if search_text in btn_widget.text().lower():
                visible_buttons.append(btn_widget)

        # Сначала скроем все кнопки, чтобы очистить сетку
        for btn_widget in self.buttons:
            btn_widget.setVisible(False)

        # Затем добавим только видимые кнопки, перестраивая сетку
        num_columns = 3  # Как определено в populate_ui
        for i, btn_widget in enumerate(visible_buttons):
            row, col = divmod(i, num_columns)
            self.grid_layout.addWidget(btn_widget, row, col)
            btn_widget.setVisible(True)

    def on_version_selected(self, version_name):
        """Обрабатывает выбор версии."""
        self.selected_version = version_name
        self.accept()

class WineHelperGUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("WineHelper")
        self.setMinimumSize(950, 550)

        if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH):
            self.setWindowIcon(QIcon(Var.WH_ICON_PATH))

        # Центрирование окна
        screen = QApplication.primaryScreen()
        screen_geometry = screen.availableGeometry()
        self.move(
            (screen_geometry.width() - self.width()) // 2,
            (screen_geometry.height() - self.height()) // 2
        )

        # Стиль для кнопок в списках
        self.BUTTON_LIST_STYLE = """
            QPushButton {
                text-align: left;
                padding-left: 10px;
                padding-right: 10px;
                height: 42px; min-height: 42px; max-height: 42px;
            }
            QPushButton::icon {
                padding-left: 10px;
            }
        """

        self.INSTALLED_BUTTON_LIST_STYLE = self.BUTTON_LIST_STYLE.replace(
            "padding-left: 10px;", "padding-left: 15px;"
        )

        # Стиль для кнопок тестовых программ
        self.TEST_BUTTON_LIST_STYLE = """
            QPushButton {
                background-color: #ffdc64; /* Более темный желтый фон */
                color: black; /* Черный цвет текста для контраста */
                text-align: left;
                padding-left: 10px;
                padding-right: 10px;
                height: 42px; min-height: 42px; max-height: 42px;
            }
            QPushButton::icon { padding-left: 10px; }
        """

        # Стили для оберток кнопок (для рамки выделения)
        self.FRAME_STYLE_DEFAULT = "QFrame { border: 2px solid transparent; border-radius: 8px; padding: 0px; }"
        self.FRAME_STYLE_SELECTED = "QFrame { border: 2px solid #0078d7; border-radius: 8px; padding: 0px; }"

        # Стили для кнопок Запустить/Остановить
        self.RUN_BUTTON_STYLE = """
            QPushButton {
                background-color: #4CAF50; color: white;
                font-weight: bold;
            }
        """
        self.STOP_BUTTON_STYLE = """
            QPushButton { background-color: #d32f2f; color: white; font-weight: bold; }
        """

        # Основные переменные
        self.winehelper_path = Var.RUN_SCRIPT
        self.process = None
        self.current_script = None
        self.install_process = None
        self.current_display_name = None
        self.install_dialog = None
        self.current_active_button = None
        self.installed_buttons = []
        self.install_tabs_data = {}
        self.running_apps = {}  # {desktop_path: QProcess}
        self.current_selected_app = None
        self.icon_animators = {}
        self.previous_tab_index = 0
        self.current_managed_prefix_name = None  # Имя префикса, выбранного в выпадающем списке
        self.prefixes_before_install = set()

        self.is_quitting = False # Флаг для корректного выхода из приложения
        self.command_output_buffer = ""
        self.command_last_line_was_progress = False
        # Создаем главный виджет и layout
        self.main_widget = QWidget()
        self.setCentralWidget(self.main_widget)
        self.main_layout = QVBoxLayout()
        self.main_widget.setLayout(self.main_layout)

        # Создаем кастомную панель вкладок и виджет со страницами
        self.tab_bar = QTabBar()
        self.stacked_widget = QStackedWidget()
        self.main_layout.addWidget(self.tab_bar)

        # Горизонтальный layout для страниц и инфо-панели
        content_layout = QHBoxLayout()
        content_layout.addWidget(self.stacked_widget, stretch=1)

        # Создаем панель информации о скрипте
        self.create_info_panel()
        content_layout.addWidget(self.info_panel, stretch=1)

        self.main_layout.addLayout(content_layout)

        # Фиксируем минимальные размеры
        self.stacked_widget.setMinimumWidth(535)
        self.info_panel.setMinimumWidth(395)

        # Вкладки
        self.create_auto_install_tab()
        self.create_manual_install_tab()
        self.create_installed_tab()
        self.create_prefix_tab()
        self.create_help_tab()

        # Загружаем состояние после создания всех виджетов
        self._load_created_prefixes()
        # После загрузки выбираем первый элемент, если он доступен
        if self.created_prefix_selector.count() > 0:
            self.created_prefix_selector.setCurrentIndex(0)
        else:
            # Если список пуст, сбрасываем панель управления
            self.on_created_prefix_selected(-1)

        # Инициализируем состояние, которое будет использоваться для логов
        self._reset_log_state()

        # Обновляем список установленных приложений
        self.update_installed_apps()

        # Соединяем сигнал смены вкладок с функцией
        self.tab_bar.currentChanged.connect(self.stacked_widget.setCurrentIndex)
        self.tab_bar.currentChanged.connect(self.on_tab_changed)
        # Устанавливаем начальное состояние видимости панели
        self.on_tab_changed(self.tab_bar.currentIndex())

    def activate(self):
        """
        Активирует и показывает окно приложения, поднимая его из свернутого состояния
        и перемещая на передний план.
        """
        # Убеждаемся, что окно не свернуто
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
        self.show()
        self.raise_()
        self.activateWindow()

    def create_colorized_icon(self, icon_path, color):
        """Загружает иконку (SVG) и применяет к ней указанный цвет."""
        target_pixmap = QPixmap(icon_path)
        if target_pixmap.isNull():
            return QIcon()

        painter = QPainter(target_pixmap)
        painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
        painter.fillRect(target_pixmap.rect(), QColor(color))
        painter.end()
        return QIcon(target_pixmap)

    def create_tray_icon(self):
        """Создает и настраивает иконку в системном трее."""
        if not QSystemTrayIcon.isSystemTrayAvailable():
            print("Системный трей не доступен.")
            return

        self.tray_icon = QSystemTrayIcon(self)

        # --- Определение цвета иконки в зависимости от темы ---
        if Var.WH_ICON_TRAY and os.path.exists(Var.WH_ICON_TRAY):
            # Получаем цвет текста окна из палитры приложения.
            # Это хороший индикатор для определения контрастного цвета.
            window_text_color = self.palette().color(QPalette.WindowText)

            # Если цвет текста светлый (высокая яркость), значит, тема темная.
            # И наоборот: если цвет текста темный, тема светлая.
            # Яркость > 127 обычно указывает на светлый цвет.
            is_dark_theme = window_text_color.lightness() > 127

            if is_dark_theme:
                # Для темных тем используем белую иконку
                self.tray_icon.setIcon(self.create_colorized_icon(Var.WH_ICON_TRAY, Qt.white))
            else:
                # Для светлых тем используем черную иконку
                self.tray_icon.setIcon(self.create_colorized_icon(Var.WH_ICON_TRAY, Qt.black))

        # Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось
        self.tray_menu = QMenu(self)

        toggle_visibility_action = self.tray_menu.addAction("Показать/Скрыть")
        toggle_visibility_action.triggered.connect(self.toggle_visibility)
        self.tray_menu.addSeparator()

        quit_action = self.tray_menu.addAction("Выход")
        quit_action.triggered.connect(self.quit_application)

        self.tray_icon.activated.connect(self.on_tray_icon_activated)
        self.tray_icon.show()

    def on_tray_icon_activated(self, reason):
        """Обрабатывает клики по иконке в трее."""
        # Показываем меню при левом клике
        if reason == QSystemTrayIcon.Trigger:
            # Получаем позицию курсора и показываем меню
            self.tray_menu.popup(QCursor.pos())

    def toggle_visibility(self):
        """Переключает видимость главного окна."""
        if self.isVisible() and self.isActiveWindow():
            self.hide()
        else:
            # Сначала скрываем, чтобы "сбросить" состояние, затем активируем.
            # Это помогает обойти проблемы с фокусом и переключением рабочих столов.
            self.hide()
            self.activate()

    def add_tab(self, widget, title):
        """Добавляет вкладку в кастомный TabBar и страницу в StackedWidget."""
        self.tab_bar.addTab(title)
        self.stacked_widget.addWidget(widget)

    def _reset_info_panel_to_default(self, tab_name):
        """Сбрасывает правую панель в состояние по умолчанию для указанной вкладки."""
        if tab_name == "Автоматическая установка":
            title = "Автоматическая установка"
            html_content = ("<h3>Автоматическая установка</h3>"
                            "<p>Скрипты из этого списка скачают, установят и настроят приложение за вас. Просто выберите программу и нажмите «Установить».</p>"
                            "<p>Для доступа к экспериментальным скриптам установки отметьте опцию <b>«Показать тестовые версии»</b> внизу списка.</p>")
            show_global = False
        elif tab_name == "Ручная установка":
            title = "Ручная установка"
            html_content = ("<h3>Ручная установка</h3>"
                            "<p>Эти скрипты подготовят окружение для установки.</p>"
                            "<p>Вам нужно будет указать только путь к установочному файлу (<code>.exe</code> или <code>.msi</code>), который вы скачали самостоятельно и нажать «Установить».</p>")
            show_global = False
        elif tab_name == "Установленные":
            title = "Установленные программы"
            html_content = ("<h3>Установленные программы</h3>"
                            "<p>Здесь отображаются все приложения, установленные с помощью WineHelper.</p>"
                            "<p>Выберите программу, чтобы увидеть доступные действия.</p>"
                            "<p>Также на этой вкладке можно восстановить префикс из резервной копии с помощью соответствующей кнопки.</p>")
            show_global = True
        else:
            return

        self.script_title.setText(title)
        self.script_description.setHtml(html_content)
        self.script_description.setVisible(True)
        self.script_title.setPixmap(QPixmap())
        self.install_action_widget.setVisible(False)
        self.installed_action_widget.setVisible(False)
        self.installed_global_action_widget.setVisible(show_global)
        self.manual_install_path_widget.setVisible(False)
        if show_global:
            self.backup_button.setVisible(False)
            self.create_log_button.setVisible(False)
            self.open_log_dir_button.setVisible(False)
            self.uninstall_button.setVisible(False)
        self.current_selected_app = None

    def on_tab_changed(self, index):
        """Скрывает или показывает панель информации в зависимости от активной вкладки."""
        # Очищаем поле поиска на вкладке, которую покинули
        previous_widget = self.stacked_widget.widget(self.previous_tab_index)
        if previous_widget:
            # Ищем QLineEdit в дочерних элементах
            search_edit = previous_widget.findChild(QLineEdit)
            if search_edit:
                search_edit.clear()

        # Обновляем индекс предыдущей вкладки для следующего переключения
        self.previous_tab_index = index

        current_tab_text = self.tab_bar.tabText(index)

        # Сбрасываем растяжение к состоянию по умолчанию:
        # растягивается виджет с описанием (индекс 1), а не виджет с действиями (индекс 4)
        self.info_panel_layout.setStretch(1, 1)
        self.info_panel_layout.setStretch(4, 0)

        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()

    def create_info_panel(self):
        """Создает правую панель с информацией о скрипте"""
        self.info_panel = QFrame()
        self.info_panel.setFrameShape(QFrame.StyledPanel)
        self.info_panel.setMinimumWidth(400)  # Размер инф. панели
        self.info_panel.setFont(QFont('Arial', 10))  # Шрифт и размер шрифта в инф. панели
        self.info_panel_layout = QVBoxLayout()
        self.info_panel.setLayout(self.info_panel_layout)

        # Заголовок
        self.script_title = QLabel("Выберите программу")
        self.script_title.setFont(QFont('Arial', 12, QFont.Bold))  # Шрифт и размер шрифта в заголовке инф. панели
        self.script_title.setAlignment(Qt.AlignCenter)
        self.info_panel_layout.addWidget(self.script_title)

        # Описание
        self.script_description = QTextBrowser()
        self.script_description.setReadOnly(True)
        self.script_description.setOpenExternalLinks(True)
        self.info_panel_layout.addWidget(self.script_description)

        # Строка для ввода пути установочного файла
        self.manual_install_path_layout = QVBoxLayout()
        self.install_path_label = QLabel("Путь к установочному файлу:")
        self.manual_install_path_layout.addWidget(self.install_path_label)

        path_input_layout = QHBoxLayout()
        self.install_path_edit = QLineEdit()
        self.install_path_edit.setPlaceholderText("Укажите путь к установочному файлу...")
        self.browse_button = QPushButton("Обзор...")
        self.browse_button.clicked.connect(self.browse_install_file)

        path_input_layout.addWidget(self.install_path_edit)
        path_input_layout.addWidget(self.browse_button)
        self.manual_install_path_layout.addLayout(path_input_layout)

        self.manual_install_path_widget = QWidget()
        self.manual_install_path_widget.setLayout(self.manual_install_path_layout)
        self.manual_install_path_widget.setVisible(False)
        self.info_panel_layout.addWidget(self.manual_install_path_widget)

        # --- ВИДЖЕТЫ ДЛЯ ДЕЙСТВИЙ ---
        # Виджет для действий установщика
        self.install_action_widget = QWidget()
        install_action_layout = QVBoxLayout()
        install_action_layout.setContentsMargins(0, 0, 0, 0)
        self.install_button = QPushButton("Установить")
        self.install_button.setFont(QFont('Arial', 12, QFont.Bold))  # Шрифт и размер шрифта в кнопке Установить
        self.install_button.setStyleSheet("background-color: #4CAF50; color: white;")
        self.install_button.clicked.connect(self.install_current_script)
        install_action_layout.addWidget(self.install_button)
        self.install_action_widget.setLayout(install_action_layout)
        self.info_panel_layout.addWidget(self.install_action_widget)

        # Виджет для действий с установленным приложением
        self.installed_action_widget = QWidget()
        installed_action_layout = QVBoxLayout()
        installed_action_layout.setContentsMargins(0, 0, 0, 0)
        installed_action_layout.setSpacing(5)

        # --- Верхний ряд кнопок ---
        top_buttons_layout = QHBoxLayout()
        self.run_button = QPushButton("Запустить")
        self.run_button.setStyleSheet(self.RUN_BUTTON_STYLE)
        self.run_button.clicked.connect(self.toggle_run_stop_app)
        top_buttons_layout.addWidget(self.run_button)
        installed_action_layout.addLayout(top_buttons_layout)

        self.installed_action_widget.setLayout(installed_action_layout)
        self.info_panel_layout.addWidget(self.installed_action_widget)

        self.installed_global_action_widget = QWidget()
        installed_global_layout = QVBoxLayout()
        installed_global_layout.setContentsMargins(0, 10, 0, 0)

        self.create_log_button = QPushButton("Создать лог запуска программы")
        self.create_log_button.setIcon(QIcon.fromTheme("view-list-text"))
        self.create_log_button.clicked.connect(self.run_installed_app_with_debug)
        installed_global_layout.addWidget(self.create_log_button)

        self.backup_button = QPushButton("Создать резервную копию префикса")
        self.backup_button.setIcon(QIcon.fromTheme("document-save"))
        self.backup_button.clicked.connect(self.backup_prefix_for_app)
        installed_global_layout.addWidget(self.backup_button)

        self.open_log_dir_button = QPushButton("Открыть папку с логом/резервной копией префикса")
        self.open_log_dir_button.setIcon(QIcon.fromTheme("folder-open"))
        self.open_log_dir_button.clicked.connect(self.open_log_directory)
        self.open_log_dir_button.setVisible(False) # Скрыта по умолчанию
        installed_global_layout.addWidget(self.open_log_dir_button)

        self.uninstall_button = QPushButton("Удалить префикс")
        self.uninstall_button.setIcon(QIcon.fromTheme("user-trash"))
        self.uninstall_button.clicked.connect(self.uninstall_app)
        installed_global_layout.addWidget(self.uninstall_button)

        self.restore_prefix_button_panel = QPushButton("Восстановить префикс из резервной копии")
        self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert"))
        self.restore_prefix_button_panel.clicked.connect(self.restore_prefix)
        installed_global_layout.addWidget(self.restore_prefix_button_panel)

        self.installed_global_action_widget.setLayout(installed_global_layout)
        self.info_panel_layout.addWidget(self.installed_global_action_widget)

        # Изначально скрыть все виджеты действий
        self.install_action_widget.setVisible(False)
        self.installed_action_widget.setVisible(False)
        self.installed_global_action_widget.setVisible(False)

    def browse_install_file(self):
        """Открывает диалог выбора файла для ручной установки"""
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Выберите установочный файл",
            os.path.expanduser("~"),
            "Все файлы (*);;Исполняемые файлы (*.exe *.msi)"
        )
        if file_path:
            self.install_path_edit.setText(file_path)

    def _start_icon_fade_animation(self, button):
        """Запускает анимацию плавного перехода для иконки на кнопке с помощью QPropertyAnimation."""
        if button not in self.icon_animators:
            return

        anim_data = self.icon_animators[button]

        # Получаем или создаем объект анимации один раз
        animation = anim_data.get('animation')
        if not animation:
            # Устанавливаем динамическое свойство, чтобы избежать предупреждений
            button.setProperty("fadeProgress", 0.0)
            animation = QPropertyAnimation(button, b"fadeProgress", self)
            animation.setDuration(700)
            animation.setEasingCurve(QEasingCurve.InOutQuad)

            # Сигналы подключаются только один раз при создании
            animation.valueChanged.connect(
                lambda value, b=button: self._update_icon_frame(b, value)
            )
            animation.finished.connect(
                lambda b=button: self._on_fade_animation_finished(b)
            )
            anim_data['animation'] = animation

        # Останавливаем предыдущую анимацию, если она еще идет
        if animation.state() == QPropertyAnimation.Running:
            animation.stop()

        # Определяем текущую и следующую иконки
        current_icon_path = anim_data['icons'][anim_data['current_index']]
        next_icon_index = (anim_data['current_index'] + 1) % len(anim_data['icons'])
        next_icon_path = anim_data['icons'][next_icon_index]

        # Сохраняем QPixmap для использования в функции обновления кадра
        anim_data['pixmaps'] = (QPixmap(current_icon_path), QPixmap(next_icon_path))

        # Устанавливаем начальное и конечное значения и запускаем
        animation.setStartValue(0.0)
        animation.setEndValue(1.0)
        animation.start()  # Без DeleteWhenStopped

    def _on_fade_animation_finished(self, button):
        """Вызывается по завершении анимации для обновления индекса иконки."""
        if button in self.icon_animators:
            anim_data = self.icon_animators[button]
            anim_data['current_index'] = (anim_data['current_index'] + 1) % len(anim_data['icons'])

    def _update_icon_frame(self, button, progress):
        """Обновляет кадр анимации, смешивая две иконки в зависимости от прогресса."""
        anim_data = self.icon_animators.get(button)
        if not anim_data or 'pixmaps' not in anim_data:
            return

        old_pixmap, new_pixmap = anim_data['pixmaps']

        # На последнем кадре просто устанавливаем новую иконку
        if progress >= 1.0:
            button.setIcon(QIcon(new_pixmap))
            return

        # Создаем холст для отрисовки смешанной иконки
        canvas = QPixmap(button.iconSize())
        canvas.fill(Qt.transparent)
        painter = QPainter(canvas)
        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setRenderHint(QPainter.SmoothPixmapTransform, True)

        # Плавно скрываем старую иконку
        painter.setOpacity(1.0 - progress)
        painter.drawPixmap(canvas.rect(), old_pixmap)

        # Плавно проявляем новую иконку
        painter.setOpacity(progress)
        painter.drawPixmap(canvas.rect(), new_pixmap)
        painter.end()
        button.setIcon(QIcon(canvas))

    def _create_app_button(self, text, icon_paths, style_sheet):
        """Создает и настраивает стандартную кнопку для списков приложений."""
        # Добавляем пробелы перед текстом для создания отступа от иконки
        btn = QPushButton("  " + text)
        btn.setStyleSheet(style_sheet)

        # Отфильтровываем несуществующие пути
        existing_icon_paths = [path for path in icon_paths if path and os.path.exists(path)]

        if existing_icon_paths:
            if len(existing_icon_paths) > 1:
                # Устанавливаем первую иконку и запускаем анимацию
                btn.setIcon(QIcon(existing_icon_paths[0]))
                main_timer = QTimer(btn)
                self.icon_animators[btn] = {'main_timer': main_timer, 'icons': existing_icon_paths, 'current_index': 0}
                main_timer.timeout.connect(lambda b=btn: self._start_icon_fade_animation(b))
                main_timer.start(2500)  # Интервал между сменами иконок
            else:
                # Устанавливаем одну иконку
                btn.setIcon(QIcon(existing_icon_paths[0]))

            btn.setIconSize(QSize(32, 32))
        else:
            # Устанавливаем пустую иконку, если ничего не найдено, для сохранения отступов
            btn.setIcon(QIcon())
            btn.setIconSize(QSize(32, 32))

        return btn

    def _populate_install_grid(self, grid_layout, scripts_list, script_folder, button_list, start_index=None):
        """
        Заполняет QGridLayout кнопками установщиков.
        Кнопки создаются только для скриптов, в которых найдена переменная PROG_NAME.

        :param grid_layout: QGridLayout для заполнения.
        :param scripts_list: Список имен скриптов.
        :param script_folder: Имя папки со скриптами ('autoinstall', 'manualinstall' или 'testinstall').
        :param button_list: Список для хранения созданных кнопок.
        """
        button_index = 0
        for script in scripts_list:
            script_path = os.path.join(Var.DATA_PATH, script_folder, script)
            prog_name = ScriptParser.extract_prog_name_from_script(script_path)

            # Создаем кнопку, только если для скрипта указано имя программы
            if not prog_name:
                continue

            icon_names = ScriptParser.extract_icons_from_script(script_path)
            icon_paths = [os.path.join(Var.DATA_PATH, "image", f"{name}.png") for name in icon_names]

            # Выбираем стиль в зависимости от папки
            if script_folder == 'testinstall':
                style_sheet = self.TEST_BUTTON_LIST_STYLE
            else:
                style_sheet = self.BUTTON_LIST_STYLE
            btn = self._create_app_button(prog_name, icon_paths, style_sheet)

            # Обертка для рамки выделения
            frame = QFrame()
            frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
            layout = QVBoxLayout(frame)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.addWidget(btn)

            btn.clicked.connect(lambda _, s=script, b=btn: self.show_script_info(s, b))
            row, column = divmod(len(button_list), 2)
            grid_layout.addWidget(frame, row, column)
            button_list.append(btn)
            button_index += 1

    def _create_searchable_grid_tab(self, placeholder_text, filter_slot, add_stretch=True):
        """
        Создает стандартную вкладку с полем поиска и сеточным макетом с прокруткой.
        Возвращает кортеж (главный виджет вкладки, сеточный макет, поле поиска, область прокрутки).
        """
        tab_widget = QWidget()
        layout = QVBoxLayout(tab_widget)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(5)

        search_edit = QLineEdit()
        search_edit.setPlaceholderText(placeholder_text)
        search_edit.textChanged.connect(filter_slot)
        layout.addWidget(search_edit)

        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(scroll_area)

        scroll_content_widget = QWidget()
        scroll_area.setWidget(scroll_content_widget)

        v_scroll_layout = QVBoxLayout(scroll_content_widget)
        v_scroll_layout.setContentsMargins(0, 0, 0, 0)
        v_scroll_layout.setSpacing(0)

        grid_layout = QGridLayout()
        grid_layout.setContentsMargins(5, 5, 5, 5)
        grid_layout.setSpacing(2)
        grid_layout.setColumnStretch(0, 1)
        grid_layout.setColumnStretch(1, 1)

        v_scroll_layout.addLayout(grid_layout)
        if add_stretch:
            v_scroll_layout.addStretch(1)

        return tab_widget, grid_layout, search_edit, scroll_area

    def _create_and_populate_install_tab(self, tab_title, script_folders, search_placeholder, filter_slot):
        """
        Создает и заполняет вкладку для установки (автоматической или ручной).
        Возвращает кортеж со скриптами, кнопками и виджетами.
        """
        tab_widget, grid_layout, search_edit, scroll_area = self._create_searchable_grid_tab(
            search_placeholder, filter_slot
        )

        scripts = []
        buttons_list = []
        for folder in script_folders:
            script_path = os.path.join(Var.DATA_PATH, folder)
            if os.path.isdir(script_path):
                try:
                    folder_scripts = sorted(os.listdir(script_path))
                    scripts.extend(folder_scripts)
                    self._populate_install_grid(grid_layout, folder_scripts, folder, buttons_list)
                except OSError as e:
                    print(f"Не удалось прочитать директорию {script_path}: {e}")

        self.add_tab(tab_widget, tab_title)

        return scripts, buttons_list, grid_layout, search_edit, scroll_area

    def create_auto_install_tab(self):
        """Создает вкладку для автоматической установки программ"""
        (
            scripts, buttons, layout,
            search_edit, scroll_area
        ) = self._create_and_populate_install_tab(
            "Автоматическая установка", ["autoinstall"], "Поиск скрипта автоматической установки...", partial(self.filter_buttons, 'auto')
        )
        self.autoinstall_scripts = scripts
        self.install_tabs_data['auto'] = {
            'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area
        }
        self.install_tabs_data['auto']['test_buttons'] = []

        # Добавляем чекбокс для тестовых версий
        test_checkbox = QCheckBox("Показать тестовые версии")
        test_checkbox.setToolTip("Показать/скрыть экспериментальные скрипты установки")

        # Находим layout вкладки, чтобы добавить чекбокс
        tab_widget = self.stacked_widget.widget(self.stacked_widget.count() - 1)
        if tab_widget and tab_widget.layout():
            tab_widget.layout().addWidget(test_checkbox)

        # Подключаем сигнал к слоту обновления
        test_checkbox.stateChanged.connect(self.update_auto_install_list)

        # Сохраняем чекбокс для доступа в будущем
        self.install_tabs_data['auto']['test_checkbox'] = test_checkbox

    def create_manual_install_tab(self):
        """Создает вкладку для ручной установки программ"""
        (
            scripts, buttons, layout,
            search_edit, scroll_area
        ) = self._create_and_populate_install_tab(
            "Ручная установка", ["manualinstall"], "Поиск скрипта ручной установки...", partial(self.filter_buttons, 'manual')
        )
        self.manualinstall_scripts = scripts
        self.install_tabs_data['manual'] = {
            'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area
        }

    def update_auto_install_list(self):
        """Обновляет список на вкладке 'Автоматическая установка' при изменении чекбокса."""
        data = self.install_tabs_data.get('auto')
        if not data:
            return

        is_checked = data['test_checkbox'].isChecked()
        test_buttons = data.get('test_buttons', [])

        # Если нужно показать тестовые версии и они еще не добавлены
        if is_checked and not test_buttons:
            test_script_folder = "testinstall"
            script_path = os.path.join(Var.DATA_PATH, test_script_folder)
            if os.path.isdir(script_path):
                try:
                    folder_scripts = sorted(os.listdir(script_path))
                    # Запоминаем, какие кнопки являются тестовыми
                    new_test_buttons = []
                    self._populate_install_grid(data['layout'], folder_scripts, test_script_folder, new_test_buttons)
                    data['test_buttons'] = new_test_buttons
                    data['buttons'].extend(new_test_buttons)
                    self.autoinstall_scripts.extend(folder_scripts)

                    # Применяем фильтр и прокручиваем к первому новому элементу
                    self.filter_buttons('auto')
                    if new_test_buttons:
                        first_new_button = new_test_buttons[0]
                        frame = first_new_button.parent()
                        if isinstance(frame, QFrame):
                            # Даем время на отрисовку перед прокруткой
                            QTimer.singleShot(100, lambda: data['scroll_area'].ensureWidgetVisible(frame, 50, 50))

                except OSError as e:
                    print(f"Не удалось прочитать директорию {test_script_folder}: {e}")

        # Если нужно скрыть тестовые версии и они были добавлены
        elif not is_checked and test_buttons:
            # Если текущая активная кнопка находится среди удаляемых, сбрасываем ее
            if self.current_active_button in test_buttons:
                self._reset_info_panel_to_default("Автоматическая установка")
                self.current_active_button = None

            # Останавливаем анимацию и удаляем виджеты тестовых кнопок
            for btn in test_buttons:
                if btn in self.icon_animators:
                    anim_data = self.icon_animators.pop(btn)
                    if 'main_timer' in anim_data:
                        anim_data['main_timer'].stop()
                    if 'animation' in anim_data and anim_data['animation']:
                        anim_data['animation'].stop()

                # Удаляем кнопку из основного списка
                if btn in data['buttons']:
                    data['buttons'].remove(btn)

                # Удаляем фрейм кнопки из layout
                frame = btn.parent()
                if frame:
                    frame.deleteLater()

            # Очищаем список тестовых кнопок
            data['test_buttons'].clear()
            # Обновляем список скриптов
            self.autoinstall_scripts = [s for s in self.autoinstall_scripts if not os.path.exists(os.path.join(Var.DATA_PATH, "testinstall", s))]

        # В любом случае применяем фильтр, чтобы скрыть/показать кнопки в соответствии с поиском
        if data['test_checkbox'].isChecked():
            self.filter_buttons('auto')

    def create_installed_tab(self):
        """Создает вкладку для отображения установленных программ в виде кнопок"""
        installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab(
            "Поиск установленной программы...", self.filter_installed_buttons, add_stretch=True
        )
        self.add_tab(installed_tab, "Установленные")

    def create_prefix_tab(self):
        """Создает вкладку 'Менеджер префиксов'"""
        self.prefix_tab = QWidget()
        layout = QVBoxLayout(self.prefix_tab)
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(10)

        # --- Контейнер для создания нового префикса ---
        creation_groupbox = QGroupBox()
        creation_layout = QVBoxLayout(creation_groupbox)
        create_prefix_button = QPushButton("Создать новый префикс")
        create_prefix_button.setFont(QFont('Arial', 12, QFont.Bold))
        create_prefix_button.setStyleSheet("background-color: #0078d7; color: white; padding: 5px;")
        create_prefix_button.clicked.connect(self.open_create_prefix_dialog)
        creation_layout.addWidget(create_prefix_button)
        layout.addWidget(creation_groupbox)

        # --- Контейнер для выбора и управления созданными префиксами ---
        self.management_container_groupbox = QGroupBox()
        self.management_container_groupbox.setVisible(True)  # Всегда виден
        container_layout = QVBoxLayout(self.management_container_groupbox)

        selector_layout = QHBoxLayout()
        self.created_prefix_selector = QComboBox()
        self.created_prefix_selector.setPlaceholderText("Выберите префикс для управления")
        self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected)
        selector_layout.addWidget(self.created_prefix_selector, 1)

        self.open_prefix_folder_button = QPushButton()
        self.open_prefix_folder_button.setIcon(QIcon.fromTheme("folder-open"))
        self.open_prefix_folder_button.setToolTip("Открыть папку префикса в файловом менеджере")
        self.open_prefix_folder_button.setEnabled(False)
        self.open_prefix_folder_button.clicked.connect(self.open_selected_prefix_folder)
        selector_layout.addWidget(self.open_prefix_folder_button)

        self.create_base_pfx_button = QPushButton()
        self.create_base_pfx_button.setIcon(QIcon.fromTheme("document-export"))
        self.create_base_pfx_button.setToolTip("Создать шаблон из выбранного префикса (для опытных пользователей)")
        self.create_base_pfx_button.setEnabled(False)
        self.create_base_pfx_button.clicked.connect(self.create_base_prefix_from_selected)
        selector_layout.addWidget(self.create_base_pfx_button)

        self.delete_prefix_button = QPushButton()
        self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash"))
        self.delete_prefix_button.setToolTip("Удалить выбранный префикс")
        self.delete_prefix_button.setEnabled(False)
        self.delete_prefix_button.clicked.connect(self.delete_selected_prefix)
        selector_layout.addWidget(self.delete_prefix_button)

        container_layout.addLayout(selector_layout)

        # --- Виджет для управления выбранным префиксом ---
        self.prefix_management_groupbox = QWidget()
        self.prefix_management_groupbox.setEnabled(False)
        management_layout = QGridLayout(self.prefix_management_groupbox)
        management_layout.setSpacing(5)

        # --- Левая сторона: Кнопки ---
        self.prefix_winetricks_button = QPushButton("Менеджер компонентов")
        self.prefix_winetricks_button.setMinimumHeight(32)
        self.prefix_winetricks_button.clicked.connect(
            lambda: self.open_winetricks_manager(prefix_name=self.current_managed_prefix_name))
        self.prefix_winetricks_button.setToolTip(
            "Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.")
        management_layout.addWidget(self.prefix_winetricks_button, 0, 0)

        self.prefix_winecfg_button = QPushButton("Редактор настроек")
        self.prefix_winecfg_button.setMinimumHeight(32)
        self.prefix_winecfg_button.clicked.connect(
            lambda: self._run_wine_util('winecfg', prefix_name=self.current_managed_prefix_name))
        self.prefix_winecfg_button.setToolTip(
            "Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).")
        management_layout.addWidget(self.prefix_winecfg_button, 0, 1)

        self.prefix_regedit_button = QPushButton("Редактор реестра")
        self.prefix_regedit_button.setMinimumHeight(32)
        self.prefix_regedit_button.clicked.connect(
            lambda: self._run_wine_util('regedit', prefix_name=self.current_managed_prefix_name))
        self.prefix_regedit_button.setToolTip(
            "Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.")
        management_layout.addWidget(self.prefix_regedit_button, 1, 0)

        self.prefix_uninstaller_button = QPushButton("Удаление программ")
        self.prefix_uninstaller_button.setMinimumHeight(32)
        self.prefix_uninstaller_button.clicked.connect(
            lambda: self._run_wine_util('uninstaller', prefix_name=self.current_managed_prefix_name))
        self.prefix_uninstaller_button.setToolTip(
            "Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.")
        management_layout.addWidget(self.prefix_uninstaller_button, 1, 1)

        self.prefix_cmd_button = QPushButton("Командная строка")
        self.prefix_cmd_button.setMinimumHeight(32)
        self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.current_managed_prefix_name))
        self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.")
        management_layout.addWidget(self.prefix_cmd_button, 2, 0)

        self.prefix_winefile_button = QPushButton("Файловый менеджер")
        self.prefix_winefile_button.setMinimumHeight(32)
        self.prefix_winefile_button.clicked.connect(
            lambda: self._run_wine_util('winefile', prefix_name=self.current_managed_prefix_name))
        self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.")
        management_layout.addWidget(self.prefix_winefile_button, 2, 1)

        self.change_wine_version_button = QPushButton("Управление Wine/Proton")
        self.change_wine_version_button.setMinimumHeight(32)
        self.change_wine_version_button.clicked.connect(self.open_wine_version_manager)
        self.change_wine_version_button.setToolTip("Изменение версии Wine или Proton для выбранного префикса.")
        management_layout.addWidget(self.change_wine_version_button, 3, 0, 1, 2)

        self.esync_button = QPushButton("ESync")
        self.esync_button.setCheckable(True)
        self.esync_button.setToolTip("Включить/выключить Eventfd-based synchronization.")
        self.esync_button.clicked.connect(lambda: self.update_sync_option("WINEESYNC", self.esync_button.isChecked()))
        management_layout.addWidget(self.esync_button, 4, 0)

        self.fsync_button = QPushButton("FSync")
        self.fsync_button.setCheckable(True)
        self.fsync_button.setToolTip("Включить/выключить Futex-based synchronization.")
        self.fsync_button.clicked.connect(lambda: self.update_sync_option("WINEFSYNC", self.fsync_button.isChecked()))
        management_layout.addWidget(self.fsync_button, 4, 1)

        self.dxvk_manage_button = QPushButton("Управление DXVK")
        self.dxvk_manage_button.setMinimumHeight(32)
        self.dxvk_manage_button.clicked.connect(lambda: self.open_component_version_manager('dxvk'))
        self.dxvk_manage_button.setToolTip("Установка или удаление определенной версии DXVK в префиксе.")
        management_layout.addWidget(self.dxvk_manage_button, 5, 0)

        self.vkd3d_manage_button = QPushButton("Управление VKD3D")
        self.vkd3d_manage_button.setMinimumHeight(32)
        self.vkd3d_manage_button.clicked.connect(lambda: self.open_component_version_manager('vkd3d-proton'))
        self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.")
        management_layout.addWidget(self.vkd3d_manage_button, 5, 1)

        self.file_associations_button = QPushButton("Ассоциации файлов")
        self.file_associations_button.setMinimumHeight(32)
        self.file_associations_button.clicked.connect(self.open_file_associations_manager)
        self.file_associations_button.setToolTip(
            "Настройка открытия определенных типов файлов с помощью нативных приложений Linux.")
        management_layout.addWidget(self.file_associations_button, 6, 0, 1, 2)

        # --- Правая сторона: Информационный блок и кнопки установки ---
        right_column_widget = QWidget()
        right_column_layout = QVBoxLayout(right_column_widget)
        right_column_layout.setContentsMargins(0, 0, 0, 0)
        right_column_layout.setSpacing(10)

        self.prefix_info_display = QTextBrowser()
        self.prefix_info_display.setReadOnly(True)
        self.prefix_info_display.setFrameStyle(QFrame.StyledPanel)
        right_column_layout.addWidget(self.prefix_info_display)

        install_group = QWidget()
        install_layout = QVBoxLayout(install_group)
        install_layout.setContentsMargins(0, 0, 0, 0)
        install_layout.setSpacing(5)

        self.prefix_install_button = QPushButton("Установить приложение в префикс")
        self.prefix_install_button.setEnabled(False)
        self.prefix_install_button.clicked.connect(self.browse_and_run_prefix_installer)
        install_layout.addWidget(self.prefix_install_button)

        self.create_launcher_button = QPushButton("Создать ярлык для приложения в префиксе")
        self.create_launcher_button.setToolTip(
            "Создает ярлык в меню и на вкладке 'Установленные' для .exe файла внутри префикса.")
        self.create_launcher_button.clicked.connect(self.create_launcher_for_prefix)
        self.create_launcher_button.setEnabled(False)  # Изначально неактивна
        install_layout.addWidget(self.create_launcher_button)
        right_column_layout.addWidget(install_group)

        right_column_layout.setStretch(0, 1)  # Информационное окно растягивается
        right_column_layout.setStretch(1, 0)  # Группа кнопок не растягивается

        management_layout.addWidget(right_column_widget, 0, 2, 7, 1)

        management_layout.setColumnStretch(0, 1)
        management_layout.setColumnStretch(1, 1)
        management_layout.setColumnStretch(2, 2)

        container_layout.addWidget(self.prefix_management_groupbox)

        # --- Кнопка полного удаления ---
        # Добавляем разделитель и кнопку в основной контейнер управления
        container_layout.addSpacing(15)
        self.remove_all_button = QPushButton("Удалить все данные WineHelper")
        self.remove_all_button.setStyleSheet("""
            QPushButton:!disabled {
                background-color: #d32f2f;
                color: white;
                padding: 5px;
            }
        """)
        self.remove_all_button.setMinimumHeight(32)
        self.remove_all_button.setFont(QFont('Arial', 10, QFont.Bold))
        self.remove_all_button.setToolTip("ВНИМАНИЕ: Удаляет ВСЕ данные WineHelper, включая все префиксы, настройки и ярлыки.")
        self.remove_all_button.clicked.connect(self.remove_all_data)
        container_layout.addWidget(self.remove_all_button)

        layout.addWidget(self.management_container_groupbox)
        layout.addStretch()
        self.add_tab(self.prefix_tab, "Менеджер префиксов")

    def _get_current_prefixes(self):
        """Возвращает множество имен существующих префиксов."""
        prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
        if not os.path.isdir(prefixes_root_path):
            return set()
        try:
            return {
                name for name in os.listdir(prefixes_root_path)
                if os.path.isdir(os.path.join(prefixes_root_path, name))
            }
        except OSError as e:
            print(f"Предупреждение: не удалось прочитать директорию префиксов: {e}")
            return set()

    def _remove_prefix_from_gui_state(self, prefix_name):
        """Удаляет префикс из пользовательского интерфейса вкладки 'Менеджер префиксов'."""
        index_to_remove = self.created_prefix_selector.findText(prefix_name)
        if index_to_remove != -1:
            self.created_prefix_selector.removeItem(index_to_remove)

    def _load_created_prefixes(self):
        """Загружает и обновляет список созданных префиксов в выпадающем списке."""
        prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
        has_prefixes_dir = os.path.isdir(prefixes_root_path)
        if not has_prefixes_dir:
            return

        try:
            prefix_names = sorted([
                name for name in os.listdir(prefixes_root_path)
                if os.path.isdir(os.path.join(prefixes_root_path, name))
            ])
        except OSError as e:
            print(f"Предупреждение: не удалось прочитать директорию префиксов: {e}")
            prefix_names = []

        self.created_prefix_selector.blockSignals(True)
        self.remove_all_button.setEnabled(True)
        self.created_prefix_selector.clear()
        if prefix_names:
            self.created_prefix_selector.addItems(prefix_names)
        self.created_prefix_selector.blockSignals(False)

        if not prefix_names:
            self.on_created_prefix_selected(-1)  # Убедимся, что панель управления сброшена
            return

    def on_created_prefix_selected(self, index):
        """Обрабатывает выбор префикса из выпадающего списка."""
        if index == -1:
            self.current_managed_prefix_name = None
            self._setup_prefix_management_panel(None)
            self.delete_prefix_button.setEnabled(False)
            self.remove_all_button.setEnabled(True)
            self.create_base_pfx_button.setEnabled(False)
            self.open_prefix_folder_button.setEnabled(False)
        else:
            # Прокручиваем к выбранному элементу, чтобы он был виден в списке
            self.created_prefix_selector.view().scrollTo(
                self.created_prefix_selector.model().index(index, 0)
            )
            prefix_name = self.created_prefix_selector.itemText(index)
            self.current_managed_prefix_name = prefix_name
            self._setup_prefix_management_panel(prefix_name)
            self.delete_prefix_button.setEnabled(True)
            self.remove_all_button.setEnabled(True)
            self.create_base_pfx_button.setEnabled(True)
            self.open_prefix_folder_button.setEnabled(True)

    def delete_selected_prefix(self):
        """Удаляет префикс, выбранный в выпадающем списке на вкладке 'Менеджер префиксов'."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            return

        msg_box = QMessageBox(self)
        msg_box.setIcon(QMessageBox.Question)
        msg_box.setWindowTitle('Подтверждение удаления')
        msg_box.setText(f'Вы уверены, что хотите удалить префикс "{prefix_name}"?\n\n'
                        'Это действие необратимо и удалит все данные внутри префикса, а также все связанные с ним ярлыки.')

        yes_button = msg_box.addButton("Да, удалить", QMessageBox.YesRole)
        no_button = msg_box.addButton("Нет", QMessageBox.NoRole)
        msg_box.setDefaultButton(no_button)

        msg_box.exec_()

        if msg_box.clickedButton() != yes_button:
            return

        # Используем модальный диалог для отображения процесса удаления
        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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)

        self.command_process.finished.connect(
            lambda exit_code, exit_status: self._handle_prefix_deletion_finished(prefix_name, exit_code, exit_status)
        )

        args = ["remove-prefix", prefix_name, "--force"]
        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_deletion_finished(self, prefix_name, exit_code, exit_status):
        """Обрабатывает завершение процесса удаления префикса."""
        self._handle_command_finished(exit_code, exit_status)
        if exit_code == 0:
            # Успешное удаление, обновляем GUI
            self._remove_prefix_from_gui_state(prefix_name)
            self.update_installed_apps()
        else:
            QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}'.\nПодробности смотрите в логе.")

    def create_base_prefix_from_selected(self):
        """Создает шаблон префикса из выбранного в выпадающем списке."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            return

        msg_box = QMessageBox(self)
        msg_box.setIcon(QMessageBox.Question)
        msg_box.setWindowTitle("Создание шаблона префикса")
        msg_box.setText(
            f"Будет создан 'шаблон' из префикса '{prefix_name}'.\n"
            "Это продвинутая функция для создания базовых архивов префиксов.\n\n"
            "Продолжить?"
        )

        yes_button = msg_box.addButton("Да, создать", QMessageBox.YesRole)
        no_button = msg_box.addButton("Нет", QMessageBox.NoRole)
        msg_box.setDefaultButton(no_button)

        msg_box.exec_()

        if msg_box.clickedButton() != yes_button:
            return

        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)
        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._run_simple_command("create-base-pfx", [prefix_name])
        self.command_dialog.exec_()

    def open_selected_prefix_folder(self):
        """Открывает папку выбранного префикса в системном файловом менеджере."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            return

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        if os.path.isdir(prefix_path):
            try:
                subprocess.Popen(['xdg-open', prefix_path])
            except Exception as e:
                QMessageBox.warning(self, "Ошибка", f"Не удалось открыть директорию:\n{prefix_path}\n\nОшибка: {e}")
        else:
            QMessageBox.warning(self, "Ошибка", f"Директория префикса не найдена:\n{prefix_path}")

    def _setup_prefix_management_panel(self, prefix_name):
        """Настраивает панель управления префиксом на основе текущего состояния."""
        is_prefix_selected = bool(prefix_name)
        self.prefix_management_groupbox.setEnabled(is_prefix_selected)
        self.create_launcher_button.setEnabled(is_prefix_selected)
        self.prefix_install_button.setEnabled(is_prefix_selected)

        if is_prefix_selected:
            self.update_prefix_info_display(prefix_name)
        else:
            self.prefix_info_display.clear()
            # Сбрасываем состояние кнопок, когда префикс не выбран
            self.esync_button.setChecked(False)
            self.fsync_button.setChecked(False)

    def update_prefix_info_display(self, prefix_name):
        """Обновляет информационный блок для созданного префикса, читая данные из last.conf."""
        if not prefix_name:
            self.prefix_info_display.clear()
            self.esync_button.setChecked(False)
            self.fsync_button.setChecked(False)
            return

        last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")

        if not os.path.exists(last_conf_path):
            self.prefix_info_display.setHtml(f"<p>Файл конфигурации last.conf не найден для префикса '{prefix_name}'.</p>")
            self.esync_button.setChecked(False)
            self.fsync_button.setChecked(False)
            return

        # Словарь для хранения всех переменных из файла
        all_vars = {}
        try:
            with open(last_conf_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('export '):
                        parts = line[7:].split('=', 1)
                        if len(parts) == 2:
                            key = parts[0].strip()
                            value = parts[1].strip().strip('"\'')
                            all_vars[key] = value
        except IOError as e:
            self.prefix_info_display.setHtml(f"<p>Ошибка чтения last.conf: {e}</p>")
            return

        # --- Обновить кнопки ESync/FSync ---
        # Блокировать сигналы, чтобы предотвратить запуск метода обновления, когда мы устанавливаем состояние
        self.esync_button.blockSignals(True)
        self.fsync_button.blockSignals(True)

        self.esync_button.setChecked(all_vars.get("WINEESYNC") == "1")
        self.fsync_button.setChecked(all_vars.get("WINEFSYNC") == "1")

        self.esync_button.blockSignals(False)
        self.fsync_button.blockSignals(False)

        # --- Чтение и отображение установленных компонентов Winetricks ---
        winetricks_log_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "winetricks.log")
        installed_verbs = []
        if os.path.exists(winetricks_log_path):
            try:
                with open(winetricks_log_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        verb = line.split('#', 1)[0].strip()
                        if verb:
                            installed_verbs.append(verb)
            except IOError as e:
                print(f"Ошибка чтения winetricks.log: {e}")

        # Фильтруем служебные компоненты, чтобы не засорять вывод
        verbs_to_ignore = {
            'isolate_home', 'winxp', 'win7', 'win10', 'win11',
            'vista', 'win2k', 'win2k3', 'win2k8', 'win8', 'win81',
            'workaround', 'internal'
        }
        display_verbs = sorted([v for v in installed_verbs if v not in verbs_to_ignore])

        # Карта для красивого отображения известных переменных
        display_map = {
            "WINEPREFIX": ("Путь", lambda v: v),
            "WINEARCH": ("Архитектура", lambda v: "64-bit" if v == "win64" else "32-bit"),
            "WH_WINE_USE": ("Версия Wine", lambda v: "Системная" if v == "system" else v),
            "DXVK_VER": ("Версия DXVK", lambda v: v if v else "Не установлено"),
            "VKD3D_VER": ("Версия VKD3D", lambda v: v if v else "Не установлено"),
            "WINEESYNC": ("ESync", lambda v: "Включен" if v == "1" else "Выключен"),
            "WINEFSYNC": ("FSync", lambda v: "Включен" if v == "1" else "Выключен"),
            "WH_XDG_OPEN": ("Ассоциации файлов", lambda v: v if v and v != "0" else "Не заданы"),
        }
        display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC", "WH_XDG_OPEN"]

        html_content = f'<p style="line-height: 1.3; font-size: 9pt;">'
        html_content += f"<b>Имя:</b> {html.escape(prefix_name)}<br>"

        # Отображаем известные переменные в заданном порядке
        for key in display_order:
            if key in all_vars:
                label, formatter = display_map[key]
                value = formatter(all_vars[key])
                html_content += f"<b>{html.escape(label)}:</b> {html.escape(value)}<br>"

        # Отображаем остальные (неизвестные) переменные
        other_vars_html = ""
        for key, value in sorted(all_vars.items()):
            if key not in display_map:
                other_vars_html += f"&nbsp;&nbsp;<b>{html.escape(key)}:</b> {html.escape(value)}<br>"

        if other_vars_html:
            html_content += "<br><b>Дополнительные параметры:</b><br>"
            html_content += other_vars_html

        html_content += "<br><b>Компоненты (Winetricks):</b> "
        if display_verbs:
            # Используем span вместо div, чтобы избежать лишних отступов
            html_content += '<span style="max-height: 120px; overflow-y: auto;">'
            html_content += ", ".join(html.escape(v) for v in display_verbs)
            html_content += '</span>'
        else:
            html_content += "Не установлены"

        html_content += "</p>"
        self.prefix_info_display.setHtml(html_content)

    def browse_and_run_prefix_installer(self):
        """Открывает диалог выбора файла и запускает установку в созданный префикс."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс для установки.")
            return

        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Выберите установочный файл",
            os.path.expanduser("~"),
            "Исполняемые файлы (*.exe *.msi);;Все файлы (*)"
        )
        if file_path:
            self.run_prefix_installer(file_path)

    def run_prefix_installer(self, installer_path):
        """Запускает установку файла в выбранный префикс через скрипт winehelper."""
        prefix_name = self.current_managed_prefix_name

        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Не выбран префикс для установки.")
            return
        if not installer_path or not os.path.isfile(installer_path):
            QMessageBox.warning(self, "Ошибка", "Указан неверный путь к установочному файлу.")
            return

        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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(self._handle_prefix_install_finished)

        # Окружение полностью настраивается скриптом winehelper
        self.command_process.setProcessEnvironment(QProcessEnvironment.systemEnvironment())

        args = ["install-to-prefix", prefix_name, installer_path]
        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 _get_prefix_component_version(self, prefix_name, component_key):
        """
        Читает last.conf префикса и возвращает версию указанного компонента.
        :param prefix_name: Имя префикса.
        :param component_key: Ключ компонента (например, 'DXVK_VER').
        :return: Строку с версией или None, если не найдено или значение пустое.
        """
        if not prefix_name:
            return None

        last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
        if not os.path.exists(last_conf_path):
            return None

        try:
            with open(last_conf_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    # Ищем строку вида 'export KEY="value"' или 'export KEY=value'
                    if line.startswith('export '):
                        parts = line[7:].split('=', 1)
                        if len(parts) == 2:
                            key = parts[0].strip()
                            if key == component_key:
                                value = parts[1].strip().strip('"\'')
                                # Возвращаем значение, только если оно не пустое.
                                return value if value else None
        except IOError as e:
            print(f"Ошибка чтения last.conf для {prefix_name}: {e}")
            return None
        return None

    def open_component_version_manager(self, component):
        """Открывает диалог выбора версии для DXVK/VKD3D и запускает установку."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
            return

        component_key = None
        if component == 'dxvk':
            group = 'DXVK'
            title = f"Управление DXVK для префикса: {prefix_name}"
            command = 'install-dxvk'
            component_key = 'DXVK_VER'
        elif component == 'vkd3d-proton':
            group = 'VKD3D'
            title = f"Управление vkd3d для префикса: {prefix_name}"
            command = 'install-vkd3d'
            component_key = 'VKD3D_VER'
        else:
            return

        dialog = ComponentVersionSelectionDialog(group, title, self)
        if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
            version = dialog.selected_version

            if version == "none":
                # Удаление: сначала проверяем, есть ли что удалять.
                installed_version = self._get_prefix_component_version(prefix_name, component_key)
                if not installed_version:
                    QMessageBox.information(self, "Информация", "Установленных версий нет.")
                    return  # Прерываем выполнение, т.к. удалять нечего
                # Для удаления лицензия не нужна, запускаем сразу.
                self.run_component_install_command(prefix_name, command, version)
            else:
                # Установка: для DXVK и VKD3D лицензию не показываем.
                if component not in ['dxvk', 'vkd3d-proton']:
                    if not self._show_license_agreement_dialog():
                        return  # Пользователь отклонил лицензию

                # Запускаем установку.
                self.run_component_install_command(prefix_name, command, version)

    def open_wine_version_manager(self):
        """Открывает диалог для смены версии Wine/Proton для префикса."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
            return

        # Определяем архитектуру префикса
        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        last_conf_path = os.path.join(prefix_path, "last.conf")
        architecture = "win64"  # По умолчанию
        if os.path.exists(last_conf_path):
            try:
                with open(last_conf_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        if 'WINEARCH=' in line:
                            arch_val = line.split('=', 1)[1].strip().strip('"\'')
                            if arch_val:
                                architecture = arch_val
                                break
            except Exception as e:
                print(f"Предупреждение: не удалось прочитать архитектуру из {last_conf_path}: {e}")

        dialog = WineVersionSelectionDialog(architecture, self)
        if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
            new_version = dialog.selected_version
            new_version_display = dialog.selected_display_text

            self.run_change_wine_version_command(prefix_name, new_version, new_version_display)

    def run_change_wine_version_command(self, prefix_name, new_version, new_version_display):
        """Выполняет команду смены версии Wine/Proton через winehelper."""
        self.command_dialog = QDialog(self)
        self.command_dialog.setWindowTitle(f"Смена версии Wine на: {new_version_display}")
        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(
            lambda exit_code, exit_status: self._handle_change_wine_version_finished(
                prefix_name, exit_code, exit_status
            )
        )

        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name))
        self.command_process.setProcessEnvironment(env)

        args = ["change-wine", new_version]
        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 run_component_install_command(self, prefix_name, command, version):
        """Выполняет команду установки компонента (DXVK/VKD3D) через winehelper."""
        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)

        self.command_dialog = QDialog(self)
        self.command_dialog.setWindowTitle(f"Выполнение: {command} {version}")
        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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(
            lambda exit_code, exit_status: self._handle_component_install_finished(
                prefix_name, exit_code, exit_status
            )
        )

        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", prefix_path)
        self.command_process.setProcessEnvironment(env)

        args = [command, version]
        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_change_wine_version_finished(self, prefix_name, exit_code, exit_status):
        """Обрабатывает завершение смены версии Wine и обновляет информацию о префиксе."""
        # Обрабатываем остаток в буфере, если он есть
        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

        # Вызываем общий обработчик для обновления лога и кнопки закрытия
        self._handle_command_finished(exit_code, exit_status)

        # В случае успеха обновляем панель информации о префиксе
        if exit_code == 0:
            self.update_prefix_info_display(prefix_name)

    def _handle_component_install_finished(self, prefix_name, exit_code, exit_status):
        """Обрабатывает завершение установки компонента и обновляет информацию о префиксе."""
        # Вызываем общий обработчик для обновления лога и кнопки закрытия
        self._handle_command_finished(exit_code, exit_status)
        # В случае успеха обновляем панель информации о префиксе
        if exit_code == 0:
            self.update_prefix_info_display(prefix_name)

    def open_file_associations_manager(self):
        """Открывает диалог для управления ассоциациями файлов."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
            return

        current_associations = self._get_prefix_component_version(prefix_name, "WH_XDG_OPEN") or "0"

        dialog = FileAssociationsDialog(current_associations if current_associations != "0" else "", self)
        if dialog.exec_() == QDialog.Accepted:
            new_associations = dialog.new_associations

            # Запускаем обновление, только если значение изменилось
            if new_associations != (current_associations if current_associations != "0" else "0"):
                self.run_update_associations_command(prefix_name, new_associations)

    def run_update_associations_command(self, prefix_name, new_associations):
        """Выполняет команду обновления ассоциаций файлов."""
        # --- Прямое редактирование last.conf, чтобы обойти перезапись переменных в winehelper ---
        last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
        if not os.path.exists(last_conf_path):
            QMessageBox.critical(self, "Ошибка", f"Файл конфигурации last.conf не найден для префикса '{prefix_name}'.")
            return

        try:
            with open(last_conf_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            updated = False
            for i, line in enumerate(lines):
                if line.strip().startswith("export WH_XDG_OPEN="):
                    lines[i] = f'export WH_XDG_OPEN="{new_associations}"\n'
                    updated = True
                    break
            if not updated:
                lines.append(f'export WH_XDG_OPEN="{new_associations}"\n')

            with open(last_conf_path, 'w', encoding='utf-8') as f:
                f.writelines(lines)
        except IOError as e:
            QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf: {e}")
            return

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)

        self.command_dialog = QDialog(self)
        self.command_dialog.setWindowTitle("Обновление ассоциаций файлов")
        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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(
            lambda exit_code, exit_status: self._handle_component_install_finished(
                prefix_name, exit_code, exit_status))

        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", prefix_path)
        # Переменная WH_XDG_OPEN теперь читается из измененного last.conf
        self.command_process.setProcessEnvironment(env)

        # Вызываем init-prefix, который теперь прочитает правильное значение из last.conf
        args = ["init-prefix"]
        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 create_launcher_for_prefix(self):
        """
        Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.
        """
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
            return

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        drive_c_path = os.path.join(prefix_path, "drive_c")

        if not os.path.isdir(drive_c_path):
            QMessageBox.critical(self, "Ошибка", f"Диск C: для префикса '{prefix_name}' не найден.")
            return

        # 1. Открываем диалог выбора файла для .exe
        exe_path, _ = QFileDialog.getOpenFileName(
            self,
            "Выберите исполняемый файл (.exe) для создания ярлыка",
            drive_c_path,
            "Исполняемые файлы (*.exe)"
        )

        if not exe_path:
            return  # Пользователь отменил

        # 2. Запрашиваем имя для ярлыка
        app_name, ok = QInputDialog.getText(
            self,
            "Имя ярлыка",
            "Введите имя для нового ярлыка:",
            QLineEdit.Normal,
            os.path.splitext(os.path.basename(exe_path))[0]  # Предлагаем имя из .exe
        )

        if not ok or not app_name.strip():
            return  # Пользователь отменил или ввел пустое имя

        # 3. Вызываем winehelper.sh create-desktop
        self.command_dialog = QDialog(self)
        self.command_dialog.setWindowTitle(f"Создание ярлыка для: {app_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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(self._handle_launcher_creation_finished)

        env = QProcessEnvironment.systemEnvironment()
        env.insert("WINEPREFIX", prefix_path)
        self.command_process.setProcessEnvironment(env)

        args = ["desktop", app_name, exe_path, "auto"]

        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 create_help_tab(self):
        """Создает вкладку 'Справка' с подвкладками"""
        help_tab = QWidget()
        help_layout = QVBoxLayout(help_tab)
        help_layout.setContentsMargins(5, 5, 5, 5)

        help_subtabs = QTabWidget()
        help_layout.addWidget(help_subtabs)

        # Подвкладка "Общее"
        general_tab = QWidget()
        general_layout = QVBoxLayout(general_tab)
        general_text = QTextBrowser()
        general_text.setOpenExternalLinks(True)

        try:
            if not Var.GENERAL or not os.path.exists(Var.GENERAL):
                raise FileNotFoundError

            with open(Var.GENERAL, 'r', encoding='utf-8') as f:
                general_content = f.read()

            html_content = ""
            url_re = re.compile(r'(https?://[^\s]+)')

            for line in general_content.splitlines():
                line = line.strip()
                if not line:
                    html_content += "<br>"
                    continue

                line = html.escape(line)
                line = url_re.sub(r'<a href="\1">\1</a>', line)

                if line.startswith('# '):
                    html_content += f'<h2>{line[2:]}</h2>'
                elif line.startswith('Для '):
                    html_content += f'<p style="margin-left: 10px;">&bull; {line}</p>'
                else:
                    html_content += f'<p>{line}</p>'

            general_text.setHtml(html_content)
        except (FileNotFoundError, TypeError):
            general_text.setHtml(f'<h2>Ошибка</h2><p>Не удалось загрузить файл с общей информацией по пути:<br>{Var.GENERAL}</p>')

        general_layout.addWidget(general_text)
        help_subtabs.addTab(general_tab, "Общее")

        # Подвкладка "Авторы"
        authors_tab = QWidget()
        authors_layout = QVBoxLayout(authors_tab)
        authors_text = QTextEdit()
        authors_text.setReadOnly(True)
        authors_text.setHtml("""
            <div style="text-align: center; font-size: 10pt;">
                <p><span style="font-size: 11pt;"><b>Разработчики</b></span><br>
                Михаил Тергоев (fidel)<br>
                Сергей Пальчех (minergenon)</p>
                <p><span style="font-size: 11pt;"><b>Помощники</b></span><br>
                Иван Мажукин (vanomj)</p>
                <p><span style="font-size: 11pt;"><b>Идея и поддержка</b></span><br>
                ООО "Базальт СПО"<br>
                ALT Linux Team</p>
                <br>
                <p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
                тестирует и сообщает об ошибках!</p>
            </div>
        """)
        authors_layout.addWidget(authors_text)
        help_subtabs.addTab(authors_tab, "Авторы")

        # Подвкладка "Лицензия"
        license_tab = QWidget()
        license_layout = QVBoxLayout(license_tab)
        license_text = QTextBrowser()
        license_text.setOpenExternalLinks(True)

        try:
            if not Var.LICENSE_FILE or not os.path.exists(Var.LICENSE_FILE):
                raise FileNotFoundError

            with open(Var.LICENSE_FILE, 'r', encoding='utf-8') as f:
                license_content = f.read()

            escaped_license_content = html.escape(license_content)

            license_html = f"""
                <h2>Лицензия</h2>
                <pre style="font-family: 'DejaVu Sans Mono', monospace; font-size: 9pt; padding: 10px; border: 1px solid #a0a0a0; border-radius: 5px;">{escaped_license_content}</pre>
                <hr>
                <h2>Сторонние компоненты</h2>
                <p>Некоторые компоненты, используемые или устанавливаемые данным ПО, могут иметь собственные лицензии. Пользователь несет полную ответственность за соблюдение этих лицензионных соглашений.</p>
                <p>Ниже приведен список основных сторонних компонентов и ссылки на их исходный код:</p>
            """

            # Читаем и парсим файл THIRD-PARTY
            third_party_html = ""
            third_party_file_path = Var.THIRD_PARTY_FILE
            if third_party_file_path and os.path.exists(third_party_file_path):
                with open(third_party_file_path, 'r', encoding='utf-8') as f_tp:
                    third_party_content = f_tp.read()

                # Преобразуем контент в HTML
                third_party_html += '<blockquote>'
                for line in third_party_content.splitlines():
                    line = line.strip()
                    if not line:
                        third_party_html += '<br>'
                        continue
                    escaped_line = html.escape(line)
                    if line.startswith('http'):
                        third_party_html += f'&nbsp;&nbsp;&nbsp;&nbsp;<a href="{escaped_line}" style="font-size: 10pt;">{escaped_line}</a><br>'
                    else:
                        third_party_html += f'<b>{escaped_line}</b><br>'
                third_party_html += '</blockquote>'

            license_text.setHtml(license_html + third_party_html)
        except (FileNotFoundError, TypeError):
            license_text.setHtml(f'<h2>Лицензия</h2><p>Не удалось загрузить файл лицензии по пути:<br>{Var.LICENSE_FILE}</p>')
        except Exception as e:
            license_text.setHtml(f'<h2>Лицензия</h2><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>')

        license_layout.addWidget(license_text)
        help_subtabs.addTab(license_tab, "Лицензия")

        # Подвкладка "История изменений"
        changelog_tab = QWidget()
        changelog_layout = QVBoxLayout(changelog_tab)
        changelog_text = QTextEdit()
        changelog_text.setReadOnly(True)
        changelog_text.setFont(QFont('DejaVu Sans Mono', 9))

        try:
            if not Var.CHANGELOG_FILE or not os.path.exists(Var.CHANGELOG_FILE):
                raise FileNotFoundError
            with open(Var.CHANGELOG_FILE, 'r', encoding='utf-8') as f:
                changelog_content = f.read()
            changelog_text.setText(changelog_content)
        except (FileNotFoundError, TypeError):
            changelog_text.setText(f"Файл истории изменений не найден по пути:\n{Var.CHANGELOG_FILE}")
        except Exception as e:
            changelog_text.setText(f"Не удалось прочитать файл истории изменений:\n{str(e)}")

        changelog_layout.addWidget(changelog_text)
        help_subtabs.addTab(changelog_tab, "История изменений")

        self.add_tab(help_tab, "Справка")

    def open_create_prefix_dialog(self):
        """Открывает диалог создания нового префикса."""
        dialog = CreatePrefixDialog(self)
        if dialog.exec_() == QDialog.Accepted:
            self.start_prefix_creation(
                prefix_name=dialog.prefix_name,
                wine_arch=dialog.wine_arch,
                base_pfx=dialog.base_pfx,
                wine_use=dialog.selected_wine_version_value,
                wine_use_display=dialog.selected_wine_version_display
            )

    def start_prefix_creation(self, prefix_name, wine_arch, base_pfx, wine_use, wine_use_display):
        """Запускает создание префикса с заданными параметрами."""

        # Сбрасываем выбор в выпадающем списке, чтобы панель управления скрылась на время создания
        if self.created_prefix_selector.count() > 0:
            self.created_prefix_selector.setCurrentIndex(-1)

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)

        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_display}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n")
        self.command_log_output.textCursor().insertText(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}\n")
        QApplication.processEvents()
        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:
            # Добавляем новый префикс в список и выбираем его
            if self.created_prefix_selector.findText(prefix_name) == -1:
                self.created_prefix_selector.addItem(prefix_name)

            self.created_prefix_selector.setCurrentText(prefix_name)

    def update_installed_apps(self):
        """Обновляет список установленных приложений в виде кнопок"""
        # Если активная кнопка находится в списке удаляемых, сбрасываем ее
        if self.current_active_button in self.installed_buttons:
            self.current_active_button = None

        # Полностью очищаем layout перед обновлением, удаляя старые виджеты (рамки с кнопками)
        while self.installed_scroll_layout.count():
            item = self.installed_scroll_layout.takeAt(0)
            widget = item.widget()
            if widget:
                widget.deleteLater()
        self.installed_buttons.clear()

        if not os.path.exists(Var.USER_WORK_PATH):
            os.makedirs(Var.USER_WORK_PATH, exist_ok=True)
            return

        desktop_files = []
        for entry in os.scandir(Var.USER_WORK_PATH):
            if entry.is_file() and entry.name.endswith('.desktop'):
                desktop_files.append(entry.path)

        desktop_files.sort()

        for i, desktop_path in enumerate(desktop_files):
            display_name = os.path.splitext(os.path.basename(desktop_path))[0]
            icon_path = None
            try:
                with open(desktop_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        if line.startswith('Name='):
                            display_name = line.split('=', 1)[1].strip()
                        elif line.startswith('Icon='):
                            icon_file = line.split('=', 1)[1].strip()
                            if os.path.exists(icon_file):
                                icon_path = icon_file
            except Exception as e:
                print(f"Error reading {desktop_path}: {str(e)}")

            btn = self._create_app_button(display_name, [icon_path], self.INSTALLED_BUTTON_LIST_STYLE)
            # Обертка для рамки выделения
            frame = QFrame()
            frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
            layout = QVBoxLayout(frame)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.addWidget(btn)

            # Клик по кнопке показывает информацию о приложении
            btn.clicked.connect(lambda _, p=desktop_path, b=btn: self.show_installed_app_info(p, b))

            row = i // 2
            column = i % 2
            self.installed_scroll_layout.addWidget(frame, row, column)
            self.installed_buttons.append(btn)

    def _handle_prefix_install_finished(self, exit_code, exit_status):
        """Обрабатывает завершение установки в префикс."""
        if exit_code == 0:
            self.command_log_output.append("\n=== Установка успешно завершена ===")
            self.create_launcher_button.setEnabled(True)  # Активируем кнопку создания ярлыка
        else:
            self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")

        if self.command_process:
            self.command_process.deleteLater()
            self.command_process = None
        self.command_close_button.setEnabled(True)
        self.update_installed_apps()

    def _set_active_button(self, button_widget):
        """Устанавливает активную кнопку и обновляет стили ее обертки (QFrame)."""
        # Сброс стиля предыдущей активной кнопки
        if self.current_active_button and self.current_active_button != button_widget:
            parent_frame = self.current_active_button.parent()
            if isinstance(parent_frame, QFrame):
                parent_frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)

        # Применение стиля к новой кнопке
        parent_frame = button_widget.parent()
        if isinstance(parent_frame, QFrame):
            parent_frame.setStyleSheet(self.FRAME_STYLE_SELECTED)

        self.current_active_button = button_widget

    def show_installed_app_info(self, desktop_path, button_widget):
        """Показывает информацию об установленном приложении в правой панели."""
        self._set_active_button(button_widget)
        # Если в поиске был текст, очищаем его и перерисовываем список.
        # Это предотвращает "прыжок", если список не был отфильтрован.
        if self.installed_search_edit.text():
            self.installed_search_edit.blockSignals(True)
            self.installed_search_edit.clear()
            self.installed_search_edit.blockSignals(False)
            self.filter_installed_buttons()

        # Прокручиваем к выбранному элементу
        frame = button_widget.parent()
        if isinstance(frame, QFrame):
            # Даем циклу событий обработать перерисовку перед прокруткой
            QTimer.singleShot(0, lambda: self.installed_scroll_area.ensureWidgetVisible(frame))

        self.current_selected_app = {'desktop_path': desktop_path}

        # Меняем растяжение: убираем у описания (индекс 1) и добавляем
        # виджету с действиями для приложения (индекс 4), чтобы он оттолкнул кнопку "Восстановить" вниз.
        self.info_panel_layout.setStretch(1, 0)
        self.info_panel_layout.setStretch(4, 1)

        try:
            with open(desktop_path, 'r', encoding='utf-8') as f:
                content = f.read()
                # Парсим .desktop файл для получения информации
                name = ""
                comment = ""
                exec_cmd = ""
                icon = ""

                for line in content.split('\n'):
                    if line.startswith('Name='):
                        name = line.split('=', 1)[1].strip()
                    elif line.startswith('Comment='):
                        comment = line.split('=', 1)[1].strip()
                    elif line.startswith('Exec='):
                        exec_cmd = line.split('=', 1)[1].strip()
                    elif line.startswith('Icon='):
                        icon = line.split('=', 1)[1].strip()

                self.current_selected_app['name'] = name
                self.current_selected_app['exec'] = exec_cmd

                self._set_run_button_state(desktop_path in self.running_apps)

                # Показываем панель информации
                self.info_panel.setVisible(True)

                # Показываем информацию в правой панели
                self.script_title.setPixmap(QPixmap())  # Сначала очищаем иконку, чтобы отобразился текст
                self.script_title.setText(name)
                self.script_description.setVisible(False)

                # Управляем видимостью кнопок
                self.install_action_widget.setVisible(False)
                self.installed_action_widget.setVisible(True)
                self.installed_global_action_widget.setVisible(True)
                self.backup_button.setVisible(True)
                self.create_log_button.setVisible(True)
                self.update_open_log_dir_button_visibility()
                self.uninstall_button.setVisible(True)
                self.manual_install_path_widget.setVisible(False)

        except Exception as e:
            QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать информацию о приложении: {str(e)}")
            self.current_selected_app = None
            self.info_panel.setVisible(False)

    def update_open_log_dir_button_visibility(self):
        """Проверяет наличие лог-файла или бэкапов и обновляет видимость кнопки."""
        log_dir_path = os.path.join(os.path.expanduser("~"), "winehelper_backup_log")
        is_visible = False
        if os.path.isdir(log_dir_path):
            # Кнопка должна быть видна, если директория не пуста.
            if os.listdir(log_dir_path):
                is_visible = True
        self.open_log_dir_button.setVisible(is_visible)

    def open_log_directory(self):
        """Открывает директорию с лог-файлами."""
        log_dir_path = os.path.join(os.path.expanduser("~"), "winehelper_backup_log")
        if os.path.isdir(log_dir_path):
            try:
                subprocess.Popen(['xdg-open', log_dir_path])
            except Exception as e:
                QMessageBox.warning(self, "Ошибка", f"Не удалось открыть директорию:\n{log_dir_path}\n\nОшибка: {e}")
        else:
            QMessageBox.information(self, "Информация", f"Директория с логами не найдена:\n{log_dir_path}")

    def _get_prefix_name_for_selected_app(self):
        """Извлекает имя префикса для выбранного приложения."""
        if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
            return None

        desktop_file = self.current_selected_app['desktop_path']
        if os.path.exists(desktop_file):
            try:
                with open(desktop_file, 'r', encoding='utf-8') as f:
                    for line in f:
                        if line.startswith('Exec='):
                            exec_line = line.strip()
                            # Ищем часть пути, например: .../prefixes/some_prefix_name/...
                            if "prefixes/" in exec_line:
                                prefix_part = exec_line.split("prefixes/")[1].split("/")[0]
                                if prefix_part:
                                    return prefix_part
            except Exception as e:
                print(f"Error getting prefix name from {desktop_file}: {e}")
        return None

    def _get_current_app_title(self):
        """Возвращает отображаемое имя для текущей выбранной программы."""
        # Если display_name не установлено (например, при ошибке), используем имя скрипта
        return self.current_display_name or self.current_script

    def backup_prefix_for_app(self):
        """Создает резервную копию префикса для выбранного приложения."""
        prefix_name = self._get_prefix_name_for_selected_app()
        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
            return

        # Создаем кастомные кнопки
        yes_button = QPushButton("Да")
        no_button = QPushButton("Нет")

        msg_box = QMessageBox(self)
        msg_box.setWindowTitle("Создание резервной копии")
        msg_box.setText(
            f"Будет создана резервная копия префикса '{prefix_name}'.\n\n"
            f"Файл будет сохранен в домашней директории в папке winehelper_backup_log/ в формате .whpack.\n\nПродолжить?"
        )
        msg_box.addButton(yes_button, QMessageBox.YesRole)
        msg_box.addButton(no_button, QMessageBox.NoRole)
        msg_box.setDefaultButton(no_button)

        msg_box.exec_()

        if msg_box.clickedButton() != yes_button:
            return  # Отмена, если нажато "Нет" или крестик
        # Используем модальный диалог для отображения процесса резервного копирования (бэкап)
        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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(self._handle_command_finished)
        self.command_process.finished.connect(self.update_open_log_dir_button_visibility)

        winehelper_path = self.winehelper_path
        args = ["backup-prefix", prefix_name]

        self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
        self.command_process.start(winehelper_path, args)
        self.command_dialog.exec_()

    def restore_prefix(self):
        """Восстанавливает префикс из резервной копии."""
        backup_path, _ = QFileDialog.getOpenFileName(
            self,
            "Выберите файл резервной копии для восстановления",
            os.path.expanduser("~"),
            "WineHelper Backups (*.whpack);;Все файлы (*)"
        )

        if not backup_path:
            return

        # Используем модальный диалог для отображения процесса восстановления из бэкапа
        self.command_dialog = QDialog(self)
        self.command_dialog.setWindowTitle(f"Восстановление из: {os.path.basename(backup_path)}")
        self.command_dialog.setMinimumSize(600, 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_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(self._handle_restore_finished)

        winehelper_path = self.winehelper_path
        args = ["restore-prefix", backup_path]

        self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
        self.command_process.start(winehelper_path, args)
        self.command_dialog.exec_()

    def run_installed_app_with_debug(self):
        """Запускает выбранное установленное приложение с созданием лога"""
        if self.current_selected_app and self.current_selected_app.get('desktop_path') in self.running_apps:
            QMessageBox.information(self, "Приложение запущено",
                                    "Приложение уже запущено. Остановите его, прежде чем запускать с отладкой.")
            return

        # Создаем кастомные кнопки
        yes_button = QPushButton("Да")
        no_button = QPushButton("Нет")

        msg_box = QMessageBox(self)
        msg_box.setWindowTitle("Создание лога")
        msg_box.setText(
            "Приложение будет запущено в режиме отладки.\n\n"
            "После закрытия приложения лог будет сохранен в папке 'winehelper_backup_log' "
            "в вашем домашнем каталоге под именем (пример: prefix_program.log).\n\n"
            "Продолжить?"
        )
        msg_box.addButton(yes_button, QMessageBox.YesRole)
        msg_box.addButton(no_button, QMessageBox.NoRole)
        msg_box.exec_()

        if msg_box.clickedButton() == yes_button:
            self._run_app_launcher(debug=True)

    def open_winetricks_manager(self, prefix_name=None):
        """Открывает новый диалог для управления компонентами Winetricks."""
        if not prefix_name:
            prefix_name = self._get_prefix_name_for_selected_app()

        if not prefix_name:
            QMessageBox.warning(self, "Менеджер Winetricks",
                                "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.")
            return

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        if not os.path.isdir(prefix_path):
            QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}")
            return

        winetricks_path = Var.WH_WINETRICKS
        wine_executable = self._get_wine_executable_for_prefix(prefix_name)
        dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self, wine_executable=wine_executable)
        dialog.installation_complete.connect(lambda: self.update_prefix_info_display(prefix_name))
        dialog.exec_()

    def _get_wine_executable_for_prefix(self, prefix_name):
        """Определяет и возвращает путь к исполняемому файлу wine для указанного префикса."""
        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        last_conf_path = os.path.join(prefix_path, "last.conf")
        wh_wine_use = None

        if os.path.exists(last_conf_path):
            try:
                with open(last_conf_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        if 'WH_WINE_USE=' in line:
                            value = line.split('=', 1)[1].strip().strip('"\'')
                            if value:
                                wh_wine_use = value
                                break
            except Exception as e:
                print(f"Предупреждение: не удалось прочитать или обработать {last_conf_path}: {e}")

        if wh_wine_use and not wh_wine_use.startswith('system'):
            local_wine_path = os.path.join(Var.USER_WORK_PATH, "dist", wh_wine_use, "bin", "wine")
            if os.path.exists(local_wine_path):
                return local_wine_path
            QMessageBox.warning(self, "Предупреждение",
                                f"Локальная версия Wine '{wh_wine_use}' не найдена по пути:\n{local_wine_path}\n\n"
                                "Будет использована системная версия Wine.")
        return 'wine'  # По умолчанию системный wine

    def update_sync_option(self, var_name, is_enabled):
        """Обновляет значение WINEESYNC или WINEFSYNC в last.conf."""
        prefix_name = self.current_managed_prefix_name
        if not prefix_name:
            return

        last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
        if not os.path.exists(last_conf_path):
            QMessageBox.warning(self, "Ошибка", f"Файл last.conf не найден для префикса '{prefix_name}'.")
            return

        new_value = "1" if is_enabled else "0"
        updated = False
        lines = []

        try:
            with open(last_conf_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            for i, line in enumerate(lines):
                if line.strip().startswith(f"export {var_name}="):
                    lines[i] = f'export {var_name}="{new_value}"\n'
                    updated = True
                    break

            if not updated:
                lines.append(f'export {var_name}="{new_value}"\n')

            with open(last_conf_path, 'w', encoding='utf-8') as f:
                f.writelines(lines)

            # Обновляем информационную панель, чтобы отразить изменения
            self.update_prefix_info_display(prefix_name)

        except IOError as e:
            QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf:\n{e}")

    def _run_wine_util(self, util_name, prefix_name=None):
        """Запускает стандартную утилиту Wine для выбранного префикса."""
        if not prefix_name:
            prefix_name = self._get_prefix_name_for_selected_app()

        if not prefix_name:
            QMessageBox.warning(self, "Ошибка",
                                "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.")
            return

        prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
        if not os.path.isdir(prefix_path):
            QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}")
            return

        wine_executable = self._get_wine_executable_for_prefix(prefix_name)

        env = os.environ.copy()
        env["WINEPREFIX"] = prefix_path

        # 'wine cmd' - особый случай, требует запуска в терминале
        if util_name == 'cmd':
            terminal_command = f"env WINEPREFIX='{prefix_path}' {shlex.quote(wine_executable)} cmd"
            try:
                # x-terminal-emulator - стандартный способ вызова терминала по умолчанию
                subprocess.Popen(['x-terminal-emulator', '-e', terminal_command])
            except FileNotFoundError:
                QMessageBox.critical(self, "Ошибка", "Не удалось найти `x-terminal-emulator`.\nУбедитесь, что у вас установлен терминал по умолчанию (например, mate-terminal или xterm).")
            except Exception as e:
                QMessageBox.critical(self, "Ошибка", f"Не удалось запустить терминал: {e}")
            return

        # Для остальных утилит
        command = [wine_executable, util_name]
        try:
            subprocess.Popen(command, env=env)
        except Exception as e:
            QMessageBox.critical(self, "Ошибка запуска",
                                 f"Не удалось запустить команду:\n{' '.join(command)}\n\nОшибка: {str(e)}")

    def toggle_run_stop_app(self):
        """Запускает или останавливает выбранное приложение."""
        if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
            return

        desktop_path = self.current_selected_app['desktop_path']

        # Если приложение запущено, останавливаем его
        if desktop_path in self.running_apps:
            process = self.running_apps.get(desktop_path)
            if process and process.state() != QProcess.NotRunning:
                prefix_name = self._get_prefix_name_for_selected_app()
                if not prefix_name:
                    QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для остановки приложения.\n"
                                                      "Попробуйте закрыть приложение вручную.")
                    # Fallback to killing the wrapper script, though it might not work
                    process.terminate()
                    if not process.waitForFinished(1000):
                        process.kill()
                    return

                prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
                wine_executable = self._get_wine_executable_for_prefix(prefix_name)

                wineserver_path = os.path.join(os.path.dirname(wine_executable), "wineserver")
                if not shutil.which(wineserver_path):
                    wineserver_path = "wineserver"

                kill_proc = QProcess(self)
                kill_env = QProcessEnvironment.systemEnvironment()
                kill_env.insert("WINEPREFIX", prefix_path)
                kill_proc.setProcessEnvironment(kill_env)

                print(f"Остановка приложений в префиксе '{prefix_name}'...")
                kill_proc.start(wineserver_path, ["-k"])
                kill_proc.waitForFinished(5000)  # Даем до 5 секунд на выполнение.
            else:
                # Состояние не совпадает, убираем из словаря
                print(f"Процесс для {desktop_path} уже не запущен, очистка.")
                self._on_app_process_finished(desktop_path)

        # Если приложение не запущено, запускаем его
        else:
            # Запускаем без отладки. Кнопка отладки отдельная.
            self._run_app_launcher(debug=False)

    def _on_app_process_finished(self, desktop_path):
        """Обрабатывает завершение процесса запущенного приложения."""
        if desktop_path in self.running_apps:
            process = self.running_apps.pop(desktop_path)
            process.deleteLater()  # Clean up the QProcess object
            print(f"Процесс для {desktop_path} завершен.")

            # Если текущее выбранное приложение - то, что только что завершилось, обновляем кнопку
            if self.current_selected_app and self.current_selected_app.get('desktop_path') == desktop_path:
                self._set_run_button_state(False)
        else:
            print(f"Предупреждение: получен сигнал finished для неизвестного процесса {desktop_path}")

    def _set_run_button_state(self, is_running):
        """Устанавливает текст и стиль для кнопки Запустить/Остановить."""
        if is_running:
            self.run_button.setText("Остановить")
            self.run_button.setStyleSheet(self.STOP_BUTTON_STYLE)
            self.create_log_button.setEnabled(False)
            self.backup_button.setEnabled(False)
            self.uninstall_button.setEnabled(False)
            self.restore_prefix_button_panel.setEnabled(False)
        else:
            self.run_button.setText("Запустить")
            self.run_button.setStyleSheet(self.RUN_BUTTON_STYLE)
            self.create_log_button.setEnabled(True)
            self.backup_button.setEnabled(True)
            self.uninstall_button.setEnabled(True)
            self.restore_prefix_button_panel.setEnabled(True)

    def _run_app_launcher(self, debug=False):
        """Внутренний метод для запуска приложения (с отладкой или без) с использованием QProcess."""
        if not self.current_selected_app or 'exec' not in self.current_selected_app:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
            return

        desktop_path = self.current_selected_app['desktop_path']
        command_str = self.current_selected_app['exec']

        if desktop_path in self.running_apps:
            print(f"Приложение {self.current_selected_app.get('name')} уже запущено.")
            return

        try:
            command_parts = shlex.split(command_str)
            clean_command = [part for part in command_parts if not part.startswith('%')]

            if debug:
                # Команда имеет вид: ['env', '/path/to/winehelper', '/path/to/app.exe']
                # Нужно вставить '--debug' после скрипта winehelper
                try:
                    script_index = -1
                    for i, part in enumerate(clean_command):
                        if 'winehelper' in os.path.basename(part):
                            script_index = i
                            break

                    if script_index != -1:
                        clean_command.insert(script_index + 1, '--debug')
                    else:
                        QMessageBox.critical(self, "Ошибка", "Не удалось найти скрипт winehelper в команде запуска.")
                        return
                except Exception as e:
                    QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
                    return

            process = QProcess()
            env = QProcessEnvironment.systemEnvironment()

            cmd_start_index = 0
            if clean_command and clean_command[0] == 'env':
                cmd_start_index = 1
                while cmd_start_index < len(clean_command) and '=' in clean_command[cmd_start_index]:
                    key, value = clean_command[cmd_start_index].split('=', 1)
                    env.insert(key, value.strip('"\''))
                    cmd_start_index += 1

            if cmd_start_index >= len(clean_command):
                raise ValueError("Не найдена команда для выполнения в строке Exec.")

            program = clean_command[cmd_start_index]
            arguments = clean_command[cmd_start_index + 1:]

            process.setProcessEnvironment(env)
            # Используем functools.partial для надежной передачи аргументов
            # и избегания проблем с замыканием в lambda.
            process.finished.connect(partial(self._on_app_process_finished, desktop_path))

            process.finished.connect(self.update_open_log_dir_button_visibility)

            try:
                process.start(program, arguments)
                if not process.waitForStarted(3000):
                    raise RuntimeError(f"Не удалось запустить процесс: {process.errorString()}")

                self.running_apps[desktop_path] = process
                self._set_run_button_state(True)
                print(f"Запущено: {program} {' '.join(arguments)}")
            except Exception as e:
                QMessageBox.critical(self, "Ошибка запуска",
                                     f"Не удалось запустить команду:\n{command_str}\n\nОшибка: {str(e)}")
                if desktop_path in self.running_apps:
                    del self.running_apps[desktop_path]
        except Exception as e:
            QMessageBox.critical(self, "Ошибка",
                                 f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")

    def remove_all_data(self):
        """Запускает процесс полного удаления всех данных WineHelper."""
        # Первое подтверждение
        msg_box1 = QMessageBox(self)
        msg_box1.setIcon(QMessageBox.Critical)
        msg_box1.setWindowTitle('Подтверждение полного удаления')
        msg_box1.setText(
            "<h2>ВНИМАНИЕ!</h2>"
            "<p>Это действие полностью и безвозвратно удалит <b>ВСЕ</b> данные, связанные с WineHelper, включая:</p>"
            "<ul>"
            "<li>Все созданные префиксы и установленные в них программы.</li>"
            "<li>Все ярлыки в меню и на рабочем столе.</li>"
            "<li>Все настройки, кэш и резервные копии.</li>"
            "</ul>"
            "<p>Продолжить?</p>"
        )
        msg_box1.setTextFormat(Qt.RichText)
        yes_button1 = msg_box1.addButton("Да, я понимаю", QMessageBox.YesRole)
        no_button1 = msg_box1.addButton("Отмена", QMessageBox.NoRole)
        msg_box1.setDefaultButton(no_button1)
        msg_box1.exec_()

        if msg_box1.clickedButton() != yes_button1:
            return

        # Второе, финальное подтверждение
        msg_box2 = QMessageBox(self)
        msg_box2.setIcon(QMessageBox.Critical)
        msg_box2.setWindowTitle('Последнее предупреждение')
        msg_box2.setText("<h3>Вы уверены, что хотите удалить ВСЁ?</h3><p>Это действие необратимо.</p>")
        msg_box2.setTextFormat(Qt.RichText)
        yes_button2 = msg_box2.addButton("Да, удалить всё", QMessageBox.DestructiveRole)
        no_button2 = msg_box2.addButton("Нет, я передумал", QMessageBox.RejectRole)
        msg_box2.setDefaultButton(no_button2)
        msg_box2.exec_()

        if msg_box2.clickedButton() != yes_button2:
            return

        # Запускаем команду и выходим из приложения
        try:
            # Запускаем команду в фоне и не ждем ее завершения
            subprocess.Popen([self.winehelper_path, "remove-all", "--force"])
            # Сообщаем пользователю и закрываем GUI
            QMessageBox.information(self, "Удаление", "Запущена процедура удаления WineHelper. Приложение будет закрыто.")
            self.quit_application()
        except Exception as e:
            QMessageBox.critical(self, "Ошибка", f"Не удалось запустить команду удаления: {e}")

    def quit_application(self):
        """Инициирует процесс выхода из приложения."""
        self.is_quitting = True
        self.close()  # Инициируем событие закрытия, которое будет обработано в closeEvent

    def closeEvent(self, event):
        """Обрабатывает событие закрытия главного окна."""
        # Теперь любое закрытие окна (крестик или выход из меню) инициирует выход
        if self.running_apps:
            msg_box = QMessageBox(self)
            msg_box.setWindowTitle('Подтверждение выхода')
            msg_box.setTextFormat(Qt.RichText)
            msg_box.setText('<font color="red">Все запущенные приложения будут закрыты вместе с WineHelper.</font><br><br>'
                            "Вы уверены, что хотите выйти?")
            msg_box.setIcon(QMessageBox.Question)

            yes_button = msg_box.addButton("Да", QMessageBox.YesRole)
            no_button = msg_box.addButton("Нет", QMessageBox.NoRole)
            msg_box.setDefaultButton(no_button)

            msg_box.exec_()

            if msg_box.clickedButton() == yes_button:
                # Отключаем обработчики сигналов от всех запущенных процессов,
                # так как мы собираемся их принудительно завершить и выйти.
                # Это предотвращает ошибку RuntimeError при закрытии.
                for process in self.running_apps.values():
                    process.finished.disconnect()

                # Используем встроенную команду killall для надежного завершения всех процессов wine
                print("Завершение всех запущенных приложений через 'winehelper killall'...")
                # Используем subprocess.run, который дождется завершения команды
                subprocess.run([self.winehelper_path, "killall"], check=False, capture_output=True)

                # Принудительно дожидаемся завершения всех дочерних процессов
                for process in self.running_apps.values():
                    process.waitForFinished(5000) # Ждем до 5 секунд

                QApplication.instance().quit()
                event.accept()
            else:
                event.ignore()
        else:
            QApplication.instance().quit()  # Если нет запущенных приложений, просто выходим

    def uninstall_app(self):
        """Удаляет выбранное установленное приложение и его префикс"""
        if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
            QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
            return

        app_name = self.current_selected_app.get('name', 'это приложение')
        prefix_name = self._get_prefix_name_for_selected_app()

        if not prefix_name:
            QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
            return

        # Создаем кастомные кнопки
        yes_button = QPushButton("Да")
        no_button = QPushButton("Нет")

        msg_box = QMessageBox(self)
        msg_box.setWindowTitle('Подтверждение')
        msg_box.setText(
            f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
        )
        msg_box.addButton(yes_button, QMessageBox.YesRole)
        msg_box.addButton(no_button, QMessageBox.NoRole)
        msg_box.setDefaultButton(no_button)

        msg_box.exec_()

        if msg_box.clickedButton() == yes_button:
            try:
                # Полный путь к префиксу
                prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)

                # 1. Сначала собираем ВСЕ .desktop файлы, связанные с этим префиксом
                all_desktop_files = set()
                # Добавляем основной .desktop файл
                if 'desktop_path' in self.current_selected_app:
                    all_desktop_files.add(self.current_selected_app['desktop_path'])

                desktop_locations = [
                    Var.USER_WORK_PATH,
                    os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper"),
                    os.path.join(os.path.expanduser("~"), "Desktop"),
                    os.path.join(os.path.expanduser("~"), "Рабочий стол"),
                ]

                # Проверяем все .desktop файлы в стандартных местах
                for location in desktop_locations:
                    if not os.path.exists(location):
                        continue

                    for file in os.listdir(location):
                        if file.endswith('.desktop'):
                            file_path = os.path.join(location, file)
                            try:
                                with open(file_path, 'r', encoding='utf-8') as f:
                                    content = f.read()
                                    if f"prefixes/{prefix_name}/" in content:
                                        all_desktop_files.add(file_path)
                            except:
                                continue

                # 2. Удаляем сам префикс
                try:
                    if os.path.exists(prefix_path):
                        shutil.rmtree(prefix_path)
                        print(f"Удален префикс: {prefix_path}")
                    else:
                        print(f"Префикс не найден: {prefix_path}")
                except Exception as e:
                    raise RuntimeError(f"Ошибка удаления префикса: {str(e)}")

                # Обновляем состояние на вкладке "Создать префикс"
                self._remove_prefix_from_gui_state(prefix_name)

                # 3. Удаляем ВСЕ найденные .desktop файлы, связанные с этим префиксом
                removed_files = []
                for file_path in all_desktop_files:
                    try:
                        os.remove(file_path)
                        removed_files.append(file_path)
                    except Exception as e:
                        print(f"Ошибка удаления {file_path}: {str(e)}")

                # 4. Удаляем категорию меню, если она пуста
                menu_dir = os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper")
                if os.path.exists(menu_dir) and not os.listdir(menu_dir):
                    try:
                        os.rmdir(menu_dir)
                        # Удаляем связанные файлы меню
                        menu_files = [
                            os.path.join(os.path.expanduser("~"),
                                         ".local/share/desktop-directories/WineHelper.directory"),
                            os.path.join(os.path.expanduser("~"), ".config/menus/applications-merged/WineHelper.menu")
                        ]
                        for f in menu_files:
                            if os.path.exists(f):
                                os.remove(f)
                    except Exception as e:
                        print(f"Ошибка удаления пустой категории меню: {str(e)}")

                # Обновляем кэш desktop файлов
                try:
                    subprocess.run(
                        ["update-desktop-database", os.path.join(os.path.expanduser("~"), ".local/share/applications")])
                except Exception as e:
                    print(f"Ошибка обновления кэша desktop файлов: {str(e)}")

                # Обновляем список установленных приложений
                self.update_installed_apps()

                # Формируем отчет об удалении
                report = f"Удаление завершено:\n"
                report += f"- Префикс: {prefix_path}\n"
                report += f"- Удаленные .desktop файлы ({len(removed_files)}):\n"
                report += "\n".join(f"  - {f}" for f in removed_files) if removed_files else "  (не найдены)"

                # Создаем кастомный диалог, чтобы кнопка была на русском
                success_box = QMessageBox(self)
                success_box.setWindowTitle("Успех")
                success_box.setText(report)
                success_box.setIcon(QMessageBox.Information)
                success_box.addButton("Готово", QMessageBox.AcceptRole)
                success_box.exec_()

                self._reset_info_panel_to_default("Установленные")

            except Exception as e:
                QMessageBox.critical(self, "Ошибка",
                                     f"Не удалось удалить приложение: {str(e)}\n\n"
                                     f"Desktop файл: {self.current_selected_app.get('desktop_path', 'не определен')}\n"
                                     f"Префикс: {prefix_name}\n"
                                     f"Путь к префиксу: {prefix_path if 'prefix_path' in locals() else 'не определен'}")
    def _filter_buttons_in_grid(self, search_text, button_list, grid_layout):
        """Общий метод для фильтрации кнопок и перестроения сетки (helper)."""
        search_text_lower = search_text.lower()
        visible_frames = []
        for btn in button_list:
            frame = btn.parent()
            if isinstance(frame, QFrame):
                frame.setVisible(False)
                if search_text_lower in btn.text().lower():
                    visible_frames.append(frame)

        for i, frame in enumerate(visible_frames):
            row, column = divmod(i, 2)
            grid_layout.addWidget(frame, row, column)
            frame.setVisible(True)

    def filter_buttons(self, tab_type):
        """Фильтрует кнопки для указанной вкладки установки ('auto' или 'manual')."""
        if tab_type not in self.install_tabs_data:
            return
        data = self.install_tabs_data[tab_type]
        self._filter_buttons_in_grid(
            data['search_edit'].text(), data['buttons'], data['layout']
        )

    def filter_installed_buttons(self):
        """Фильтрует кнопки установленных программ."""
        self._filter_buttons_in_grid(
            self.installed_search_edit.text(), self.installed_buttons, self.installed_scroll_layout
        )

    def show_script_info(self, script_name, button_widget):
        """Показывает информацию о выбранном скрипте"""
        self._set_active_button(button_widget)
        self.current_script = script_name

        # Определяем виджеты и действия в зависимости от типа скрипта
        if script_name in self.autoinstall_scripts:
            script_path = os.path.join(Var.DATA_PATH, "autoinstall", script_name)
            tab_type = 'auto'
            if not os.path.exists(script_path): # Проверяем в testinstall, если не нашли в autoinstall
                script_path = os.path.join(Var.DATA_PATH, "testinstall", script_name)
            self.manual_install_path_widget.setVisible(False)
        else:
            script_path = os.path.join(Var.DATA_PATH, "manualinstall", script_name)
            tab_type = 'manual'
            self.manual_install_path_widget.setVisible(True)

        tab_data = self.install_tabs_data[tab_type]
        search_edit = tab_data['search_edit']
        scroll_area = tab_data['scroll_area']

        # Если в поиске был текст, очищаем его и перерисовываем список.
        # Это предотвращает "прыжок", если список не был отфильтрован.
        if search_edit.text():
            search_edit.blockSignals(True)
            search_edit.clear()
            search_edit.blockSignals(False)
            self.filter_buttons(tab_type)

        frame = button_widget.parent()
        if isinstance(frame, QFrame):
            QTimer.singleShot(0, lambda: scroll_area.ensureWidgetVisible(frame))

        # Обновляем информацию в правой панели
        description = ScriptParser.extract_info_ru(script_path)
        icon_names = ScriptParser.extract_icons_from_script(script_path)
        prog_name = ScriptParser.extract_prog_name_from_script(script_path)
        prog_url = ScriptParser.extract_prog_url_from_script(script_path)
        display_name = prog_name if prog_name else script_name
        self.current_display_name = display_name

        if icon_names:
            # Для заголовка используем первую иконку из списка
            icon_path = os.path.join(Var.DATA_PATH, "image", f"{icon_names[0]}.png")
            if os.path.exists(icon_path):
                self.script_title.setPixmap(QPixmap(icon_path).scaled(64, 64, Qt.KeepAspectRatio))
            else:
                self.script_title.setPixmap(QPixmap())
        else:
            self.script_title.setPixmap(QPixmap())

        self.script_title.setText(display_name)

        html_description = f"<p>{description}</p>"
        if prog_url:
            html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>'

        self.script_description.setHtml(html_description)
        self.script_description.setVisible(True)
        self.install_action_widget.setVisible(True)
        self.installed_action_widget.setVisible(False)
        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"""
                <pre style="font-family: sans-serif; font-size: 10pt; white-space: pre-wrap; word-wrap: break-word;">{escaped_license_content}</pre>
            """)
        except (FileNotFoundError, TypeError):
            license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Не удалось загрузить файл лицензионного соглашения по пути:<br>{Var.LICENSE_AGREEMENT_FILE}</p>')
        except Exception as e:
            license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>')

        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:
            QMessageBox.warning(self, "Ошибка", "Не выбрана программа для установки")
            return

        if self.current_script in self.manualinstall_scripts and not self.install_path_edit.text().strip():
            QMessageBox.warning(self, "Ошибка", "Укажите путь к установочному файлу")
            return

        if not self._show_license_agreement_dialog():
            return  # Пользователь отклонил лицензию

        self.prefixes_before_install = self._get_current_prefixes()

        self.installation_cancelled = False

        # Создаем диалоговое окно установки
        self.install_dialog = QDialog(self)
        title_name = self._get_current_app_title()
        self.install_dialog.setWindowTitle(f"Установка «{title_name}»")
        self.install_dialog.setMinimumSize(750, 400)
        self.install_dialog.setWindowModality(Qt.WindowModal)
        self.install_dialog.setAttribute(Qt.WA_DeleteOnClose)  # Удалять диалог при закрытии
        log_layout = QVBoxLayout(self.install_dialog)

        self.log_output = QTextEdit()
        self.log_output.setReadOnly(True)
        self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
        log_layout.addWidget(self.log_output)

        control_buttons = QWidget()
        btn_layout = QHBoxLayout(control_buttons)
        self.btn_abort = QPushButton("Прервать")
        self.btn_abort.clicked.connect(self.abort_installation)
        btn_layout.addWidget(self.btn_abort)

        self.btn_close = QPushButton("Закрыть")
        self.btn_close.setEnabled(False)
        self.btn_close.clicked.connect(self.install_dialog.close)
        btn_layout.addWidget(self.btn_close)
        log_layout.addWidget(control_buttons)

        # Назначение кастомного обработчика закрытия окна
        def dialog_close_handler(event):
            self.handle_install_dialog_close(event)
        self.install_dialog.closeEvent = dialog_close_handler

        self.install_dialog.show()
        # Сразу же готовим и запускаем установку
        self._prepare_installation()

    def _reset_log_state(self):
        """Сбрасывает состояние буфера и флага прогресса для лога установки."""
        self.output_buffer = ""
        self.last_line_was_progress = False

    def _prepare_installation(self):
        """Подготавливает и запускает процесс установки"""
        self._reset_log_state()  # Сбрасываем состояние для обработки лога

        winehelper_path = self.winehelper_path
        script_path = os.path.join(Var.DATA_PATH,
                                   "autoinstall" if os.path.exists(os.path.join(Var.DATA_PATH, "autoinstall", self.current_script))
                                   else "testinstall" if os.path.exists(os.path.join(Var.DATA_PATH, "testinstall", self.current_script))
                                   else "manualinstall",
                                   self.current_script)

        if not os.path.exists(winehelper_path):
            QMessageBox.critical(self.install_dialog, "Ошибка", f"winehelper не найден по пути:\n{winehelper_path}")
            return
        if not os.path.exists(script_path):
            QMessageBox.critical(self.install_dialog, "Ошибка", f"Скрипт установки не найден:\n{script_path}")
            return

        if self.current_script in self.manualinstall_scripts:
            install_file = self.install_path_edit.text().strip()
            if not install_file:
                QMessageBox.critical(self.install_dialog, "Ошибка", "Не указан путь к установочному файлу")
                return
            QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path, install_file))
        else:
            QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path))

    def _start_installation(self, winehelper_path, script_path, install_file=None):
        """Запускает процесс установки"""
        # Устанавливаем родителя для QProcess, чтобы он корректно удалялся вместе с диалогом
        self.install_process = QProcess(self.install_dialog)
        self.install_process.setProcessChannelMode(QProcess.MergedChannels)
        self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path))

        env = QProcessEnvironment.systemEnvironment()
        env.insert("GUI_MODE", "1")
        self.install_process.setProcessEnvironment(env)

        self.install_process.readyReadStandardOutput.connect(self.handle_process_output)
        self.install_process.finished.connect(self.handle_process_finished)

        args = ["install", os.path.basename(script_path)]
        if install_file:
            args.append(install_file)

        title_name = self._get_current_app_title()
        self.append_log(f"=== Начало установки «{title_name}» ===")
        self.append_log(f"Исполняемый файл: {winehelper_path}")
        self.append_log(f"Аргументы: {' '.join(shlex.quote(a) for a in args)}")

        try:
            self.install_process.start(winehelper_path, args)
            if not self.install_process.waitForStarted(3000):
                raise RuntimeError("Не удалось запустить процесс установки")
            self.append_log("Процесс установки запущен...")
        except Exception as e:
            self.append_log(f"\n=== ОШИБКА: {str(e)} ===", is_error=True)
            QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}")
            self.cleanup_process()

    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:
            # Для ошибок всегда добавляем перенос строки для лучшей читаемости
            cursor.insertHtml(f'<font color="red">{text}</font><br>')
        else:
            # Вставляем текст. Добавляем перенос строки, если нужно.
            formatted_text = f"{text}\n" if add_newline else text
            cursor.insertText(formatted_text)

        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

        # Фильтруем "мусорные" строки прогресса (например, '-=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.log_output.textCursor()

        # Если новая строка - это прогресс, и предыдущая тоже была прогрессом,
        # то мы удаляем старую, чтобы заменить ее новой.
        if is_progress_line and self.last_line_was_progress:
            cursor.movePosition(QTextCursor.End)
            cursor.select(QTextCursor.LineUnderCursor)
            cursor.removeSelectedText()
        elif not is_progress_line and self.last_line_was_progress:
            # Это переход от строки прогресса к финальной строке.
            # Вместо добавления переноса, мы заменяем предыдущую строку новой.
            cursor.movePosition(QTextCursor.End)
            cursor.select(QTextCursor.LineUnderCursor)
            cursor.removeSelectedText()

        # Добавляем новую очищенную строку.
        # Для прогресса - без переноса строки, для обычных строк - с переносом.
        self.append_log(clean_line, add_newline=not is_progress_line)

        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')
        self.output_buffer += new_data

        while True:
            # Ищем ближайший разделитель (\n или \r)
            idx_n = self.output_buffer.find('\n')
            idx_r = self.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.output_buffer[:split_idx + 1]
            self.output_buffer = self.output_buffer[split_idx + 1:]

            self._process_log_line(line)

    def handle_process_finished(self, exit_code, exit_status):
        """Обрабатывает завершение процесса"""
        # Обрабатываем остаток в буфере, если он есть
        if self.output_buffer:
            self._process_log_line(self.output_buffer)

        # Если последней строкой был прогресс, "завершаем" его переносом строки.
        if self.last_line_was_progress:
            cursor = self.log_output.textCursor()
            cursor.movePosition(QTextCursor.End)
            cursor.insertText("\n")

        self._reset_log_state()

        if hasattr(self, 'installation_cancelled') and self.installation_cancelled:
            self.append_log("\n=== Установка была прервана. ===")
            self.cleanup_process()
            if self.install_dialog:
                self.install_dialog.close()
            return

        if exit_code == 0 and exit_status == QProcess.NormalExit:
            self.append_log("\n=== Установка успешно завершена ===")

            # --- Обновление списка префиксов ---
            # Определяем, какой префикс был создан
            prefixes_after_install = self._get_current_prefixes()
            new_prefixes = prefixes_after_install - getattr(self, 'prefixes_before_install', set())

            # Перезагружаем список префиксов на вкладке "Менеджер префиксов"
            self._load_created_prefixes()

            new_prefix_name = None
            selected_new = False
            if new_prefixes:
                # Обычно создается один префикс, берем первый из найденных.
                new_prefix_name = new_prefixes.pop()
                # Находим и выбираем его в выпадающем списке.
                index = self.created_prefix_selector.findText(new_prefix_name)
                if index != -1:
                    self.created_prefix_selector.setCurrentIndex(index)
                    selected_new = True

            if not selected_new and self.created_prefix_selector.count() > 0:
                self.created_prefix_selector.setCurrentIndex(0)
            # --- Конец обновления списка префиксов ---

            self.update_installed_apps()

        # Кнопка закрыть
        self.btn_close.setEnabled(True)
        # Кнопка прервать
        self.btn_abort.setEnabled(False)
        self.install_process = None

    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)

    def handle_install_dialog_close(self, event):
        """Обрабатывает событие закрытия диалога установки."""
        # Проверяем, запущен ли еще процесс установки
        if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
            yes_button = QPushButton("Да, прервать")
            no_button = QPushButton("Нет")

            msg_box = QMessageBox(self.install_dialog)
            msg_box.setWindowTitle("Прервать установку?")
            msg_box.setText("Установка еще не завершена.\nВы действительно хотите прервать процесс?")
            msg_box.setIcon(QMessageBox.Question)
            msg_box.addButton(yes_button, QMessageBox.YesRole)
            msg_box.addButton(no_button, QMessageBox.NoRole)
            msg_box.setDefaultButton(no_button)

            msg_box.exec_()

            if msg_box.clickedButton() == yes_button:
                self.append_log("\n=== Пользователь прервал установку через закрытие окна. Ожидание... ===", is_error=True)
                self.installation_cancelled = True
                self.install_process.terminate()
                event.ignore()  # Запрещаем закрытие, handle_process_finished обработает его
            else:
                # Пользователь нажал "Нет", поэтому игнорируем событие закрытия
                event.ignore()
        else:
            # Процесс не запущен (установка завершена или еще не началась),
            # поэтому просто закрываем окно
            event.accept()

    def abort_installation(self):
        """Прерывает текущую установку"""
        if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
            yes_button = QPushButton("Да")
            no_button = QPushButton("Нет")
            msg_box = QMessageBox(self.install_dialog)
            msg_box.setWindowTitle("Подтверждение")
            msg_box.setText("Вы действительно хотите прервать установку?")
            msg_box.addButton(yes_button, QMessageBox.YesRole)
            msg_box.addButton(no_button, QMessageBox.NoRole)
            msg_box.setDefaultButton(no_button)
            msg_box.exec_()
            if msg_box.clickedButton() == yes_button:
                self.append_log("\n=== Пользователь прервал установку ===", is_error=True)
                self.installation_cancelled = True
                self.install_process.terminate()

    def _handle_command_output(self):
        """Обрабатывает вывод для общих команд в модальном диалоге."""
        if hasattr(self, 'command_process') and self.command_process:
            # Используем readAll, чтобы получить и stdout, и stderr
            output_bytes = self.command_process.readAll()
            output = output_bytes.data().decode('utf-8', errors='ignore').strip()
            if output and hasattr(self, 'command_log_output'):
                self.command_log_output.append(output)
                QApplication.processEvents()

    def _run_simple_command(self, command, args=None):
        """Запускает простую команду winehelper и выводит лог."""
        self.command_process = QProcess(self.command_dialog)
        self.command_process.setProcessChannelMode(QProcess.MergedChannels)
        self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
        self.command_process.finished.connect(self._handle_command_finished)
        self.command_process.start(self.winehelper_path, [command] + (args or []))
    def _handle_command_finished(self, exit_code, exit_status):
        """Обрабатывает завершение для диалога команды"""
        if exit_code == 0:
            self.command_log_output.append(f"\n=== Команда успешно завершена ===")
        else:
            self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
        if self.command_process:
            self.command_process.deleteLater()
            self.command_process = None
        self.command_close_button.setEnabled(True)
        self.command_log_output.ensureCursorVisible()

    def _handle_launcher_creation_finished(self, exit_code, exit_status):
        """Обрабатывает завершение создания ярлыка."""
        self._handle_command_finished(exit_code, exit_status)
        if exit_code == 0:
            self.update_installed_apps()
            # Переключаемся на вкладку "Установленные"
            for i in range(self.tab_bar.count()):
                if self.tab_bar.tabText(i) == "Установленные":
                    self.tab_bar.setCurrentIndex(i)
                    break

    def _handle_restore_finished(self, exit_code, exit_status):
        """Обрабатывает завершение для диалога команды восстановления."""
        if exit_code == 0:
            self.command_log_output.append(f"\n=== Восстановление успешно завершено ===")
            self.update_installed_apps()
            self._load_created_prefixes()
            self.filter_installed_buttons()
        else:
            self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
        if self.command_process:
            self.command_process.deleteLater()
            self.command_process = None
        self.command_close_button.setEnabled(True)

    def cleanup_process(self):
        """Очищает ресурсы процесса, принудительно завершая его, если он активен."""
        if hasattr(self, 'install_process') and self.install_process:
            if self.install_process.state() == QProcess.Running:
                self.install_process.terminate()
                if not self.install_process.waitForFinished(3000):
                    self.append_log("Процесс не ответил на terminate, отправляется kill...", is_error=True)
                    self.install_process.kill()
                    self.install_process.waitForFinished()
            self.install_process.deleteLater()
            self.install_process = None

def main():
    """Основная точка входа в приложение."""
    # Включаем поддержку HiDPI до создания QApplication для надежного и предсказуемого масштабирования.
    # AA_EnableHighDpiScaling включает автоматическое масштабирование на основе DPI дисплея.
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    # AA_UseHighDpiPixmaps позволяет использовать высококачественные иконки (например, @2x).
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    # QApplication должен быть создан до использования любых других частей Qt
    app = QApplication(sys.argv)

    # Создаем уникальное имя для сокета на основе ID пользователя,
    # чтобы у каждого пользователя был свой экземпляр.
    socket_name = f"winehelper_{os.getuid()}"

    # Пытаемся подключиться к существующему экземпляру
    socket = QLocalSocket()
    socket.connectToServer(socket_name)

    # Если подключение успешно в течение 500 мс, значит, другой экземпляр уже запущен.
    if socket.waitForConnected(500):
        # Отправляем сообщение (любое, сам факт подключения - это сигнал)
        socket.write(b"activate")
        socket.waitForBytesWritten(500)
        socket.disconnectFromServer()
        # Успешно выходим, не запуская новый экземпляр
        return 0

    # Если подключиться не удалось, это первый экземпляр.
    # На всякий случай удаляем старый файл сокета, если он остался от сбоя.
    QLocalServer.removeServer(socket_name)

    dependency_manager = DependencyManager()
    if dependency_manager.run():
        window = WineHelperGUI()

        # Создаем локальный сервер для приема "сигналов" от последующих запусков
        server = QLocalServer(window)

        # Функция для обработки входящих подключений
        def handle_new_connection():
            client_socket = server.nextPendingConnection()
            if client_socket:
                # Ждем данные (не обязательно, но для надежности)
                client_socket.waitForReadyRead(100)
                client_socket.readAll()  # Очищаем буфер
                window.activate()
                client_socket.close()

        server.newConnection.connect(handle_new_connection)

        # Начинаем слушать. Если не удалось, программа все равно будет работать,
        # но без функции активации существующего окна.
        if not server.listen(socket_name):
            print(f"Предупреждение: не удалось запустить сервер {socket_name}: {server.errorString()}")

        # Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора
        window.server = server
        window.show()
        # Создаем иконку в системном трее после создания окна
        # window.create_tray_icon()  # Временно отключено
        return app.exec_()

    return 1

if __name__ == "__main__":
    sys.exit(main())
