diff --git a/winehelper b/winehelper
index a8d44be..038a3ae 100755
--- a/winehelper
+++ b/winehelper
@@ -1184,6 +1184,16 @@ init_wineprefix () {
# добавление ассоциаций файлов для запуска нативного приложения из wine
# пример переменной: WH_XDG_OPEN="txt doc pdf"
check_variables WH_XDG_OPEN "0"
+ # Сохраняем старые ассоциации, чтобы потом удалить те, что больше не нужны
+ local old_xdg_open
+ if [[ -f "$WINEPREFIX/last.conf" ]]; then
+ old_xdg_open=$(grep "WH_XDG_OPEN=" "$WINEPREFIX/last.conf" | sed -e 's/.*WH_XDG_OPEN="//' -e 's/"$//')
+ fi
+
+ # Если переменная WH_XDG_OPEN была установлена извне (например, из GUI),
+ # то мы должны принудительно установить ее значение в "0", если она пуста,
+ # чтобы корректно удалить старые ассоциации.
+ [[ -z "$WH_XDG_OPEN" ]] && WH_XDG_OPEN="0"
local WRAPPER="${WH_TMP_DIR}/wh-xdg-open.sh"
local XDG_OPEN_REG="Software\Classes\xdg-open\shell\open\command"
if [[ $WH_XDG_OPEN != "0" ]] ; then
@@ -1206,13 +1216,25 @@ init_wineprefix () {
# добавляем новую команду xdg-open в реестр
get_and_set_reg_file --add "$XDG_OPEN_REG" '@=' 'REG_SZ' "$WRAPPER %1" "system"
+ # Удаляем старые ассоциации, которых нет в новом списке
+ if [[ -n "$old_xdg_open" ]]; then
+ for old_ext in $old_xdg_open; do
+ if ! echo " $WH_XDG_OPEN " | grep -q " $old_ext "; then
+ get_and_set_reg_file --delete "Software\Classes\.$old_ext" '@='
+ fi
+ done
+ fi
+
# добавляем ассоциации файлов для запуска с помощью xdg-open
for ext in $WH_XDG_OPEN ; do
get_and_set_reg_file --add "Software\Classes\.$ext" '@=' 'REG_SZ' "xdg-open" "system"
done
print_info "Используются ассоциации с нативными приложениями для файлов: \"$WH_XDG_OPEN\""
else
- # удаление команды xdg-open из реестра
+ # удаление всех ассоциаций
+ for old_ext in $old_xdg_open; do
+ get_and_set_reg_file --delete "Software\Classes\.$old_ext" '@='
+ done
get_and_set_reg_file --delete "$XDG_OPEN_REG" '@='
# удаяем скрипт-обёртку
try_remove_file "$WRAPPER"
@@ -1353,6 +1375,13 @@ init_database () {
. "$WHDB_FILE"
elif check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then
print_info "Найдены настройки из предыдущего использования префикса: $WINEPREFIX"
+ # Сохраняем значение WH_XDG_OPEN, если оно было установлено извне (например, из GUI).
+ # Это предотвращает его перезапись старым значением из last.conf.
+ # Используем `declare -p` для надежного определения, была ли переменная установлена,
+ # даже если она пустая (что означает "удалить все ассоциации").
+ if declare -p WH_XDG_OPEN &>/dev/null; then
+ wh_xdg_open_from_env="$WH_XDG_OPEN"
+ fi
cat "$WINEPREFIX/last.conf"
. "$WINEPREFIX/last.conf"
else
@@ -1361,10 +1390,19 @@ init_database () {
}
prepair_wine () {
+ # Объявляем переменную здесь, чтобы она была доступна после вызова init_database
+ local wh_xdg_open_from_env
+
if [[ -n "$INSTALL_SCRIPT_NAME" ]]
then print_info "Используются настройки из скрипта установки: $INSTALL_SCRIPT_NAME"
else init_database
fi
+ # Восстанавливаем значение WH_XDG_OPEN, если оно было установлено извне.
+ # Проверяем, была ли переменная сохранена, а не ее значение.
+ # Это позволяет корректно передать пустую строку.
+ if declare -p wh_xdg_open_from_env &>/dev/null; then
+ export WH_XDG_OPEN="$wh_xdg_open_from_env"
+ fi
init_wine_ver
init_wineprefix
use_winetricks
@@ -1394,9 +1432,9 @@ wine_run () {
echo "##### Лог WINE #####" | tee -a "$LOG_FILE"
$MANGOHUD_RUN "$WINELOADER" "$@" $LAUNCH_PARAMETERS 2>&1 | tee -a "$LOG_FILE"
else
- $MANGOHUD_RUN "$WINELOADER" "$@" $LAUNCH_PARAMETERS
+ exec $MANGOHUD_RUN "$WINELOADER" "$@" $LAUNCH_PARAMETERS
fi
- wait_wineserver
+ # wait_wineserver
}
wine_run_install () {
diff --git a/winehelper_gui.py b/winehelper_gui.py
index 528329c..eec3f65 100644
--- a/winehelper_gui.py
+++ b/winehelper_gui.py
@@ -12,7 +12,7 @@ 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)
+ QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
@@ -474,10 +474,9 @@ class WinetricksManagerDialog(QDialog):
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
- # Кнопки управления
+ # Кнопки управления, выровненные по правому краю
button_layout = QHBoxLayout()
- self.status_label = QLabel("Загрузка компонентов...")
- button_layout.addWidget(self.status_label, 1)
+ button_layout.addStretch(1)
self.apply_button = QPushButton("Применить")
self.apply_button.setEnabled(False)
@@ -548,7 +547,6 @@ class WinetricksManagerDialog(QDialog):
def load_all_categories(self):
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
- self.category_statuses = {name: "загрузка..." for name in self.categories.keys()}
for internal_name in self.categories.values():
self._start_load_process(internal_name)
@@ -602,13 +600,6 @@ class WinetricksManagerDialog(QDialog):
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
- def _update_status_label(self):
- """Обновляет текстовую метку состояния загрузки."""
- status_parts = []
- for name, status in self.category_statuses.items():
- status_parts.append(f"{name}: {status}")
- self.status_label.setText(" | ".join(status_parts))
-
def _parse_winetricks_log(self):
"""Читает winetricks.log и возвращает множество установленных компонентов."""
installed_verbs = set()
@@ -681,22 +672,15 @@ class WinetricksManagerDialog(QDialog):
if exit_code != 0 or exit_status != QProcess.NormalExit:
error_string = process.errorString() if process else "N/A"
- self._log(f"--- Ошибка загрузки категории '{category}' (код: {exit_code}) ---", "red")
- self.category_statuses[category_display_name] = "ошибка"
- self._update_status_label() # Показываем ошибку в статусе
+ 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:
- self.category_statuses[category_display_name] = "готово"
installed_verbs = self._parse_winetricks_log()
- # Обновляем статус только если это была сетевая загрузка
- if from_cache is None:
- self._update_status_label()
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget)
if from_cache is None: # Только если мы не читали из кэша
@@ -721,7 +705,6 @@ class WinetricksManagerDialog(QDialog):
self.loading_count -= 1
if self.loading_count == 0:
- self.status_label.setText("Готово.")
self._update_ui_state()
def _on_item_changed(self, item):
@@ -862,11 +845,6 @@ class WinetricksManagerDialog(QDialog):
# 3. Обрабатываем успех
self._log("\n=== Все операции успешно завершены ===")
- self._show_message_box("Успех",
- "Операции с компонентами были успешно выполнены.",
- QMessageBox.Information,
- {"buttons": {"Да": QMessageBox.AcceptRole}})
-
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние
self.close_button.setEnabled(True)
@@ -876,7 +854,6 @@ class WinetricksManagerDialog(QDialog):
search_edit.clear()
# Перезагружаем данные, чтобы обновить состояние
- self.status_label.setText("Обновление данных...")
self.initial_states.clear()
self.load_all_categories()
self.installation_finished = True
@@ -1355,6 +1332,75 @@ class CreatePrefixDialog(QDialog):
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(
+ "Укажите расширения файлов, которые должны открываться нативными
"
+ "приложениями Linux. Чтобы удалить все ассоциации, очистите поле.
"
+ "Пример: pdf docx txt
"
+ )
+ 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(
+ "Запрещено использовать: cpl, dll, exe, lnk, msi"
+ )
+ 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(
+ "Следующие расширения запрещены и не могут быть использованы:
"
+ f"{', '.join(sorted(list(found_forbidden)))}"
+ )
+ msg_box.exec_()
+ return
+
+ # Сохраняем результат в виде отсортированной строки
+ self.new_associations = " ".join(sorted(list(entered_extensions)))
+ self.accept()
+
class ComponentVersionSelectionDialog(QDialog):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
@@ -2142,6 +2188,13 @@ class WineHelperGUI(QMainWindow):
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)
@@ -2174,7 +2227,7 @@ class WineHelperGUI(QMainWindow):
right_column_layout.setStretch(0, 1) # Информационное окно растягивается
right_column_layout.setStretch(1, 0) # Группа кнопок не растягивается
- management_layout.addWidget(right_column_widget, 0, 2, 6, 1)
+ management_layout.addWidget(right_column_widget, 0, 2, 7, 1)
management_layout.setColumnStretch(0, 1)
management_layout.setColumnStretch(1, 1)
@@ -2381,8 +2434,9 @@ class WineHelperGUI(QMainWindow):
"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", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC"]
+ display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC", "WH_XDG_OPEN"]
html_content = f'
'
html_content += f"Имя: {html.escape(prefix_name)}
"
@@ -2682,6 +2736,64 @@ class WineHelperGUI(QMainWindow):
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 ""
+
+ dialog = FileAssociationsDialog(current_associations, self)
+ if dialog.exec_() == QDialog.Accepted:
+ new_associations = dialog.new_associations
+ # Запускаем обновление, только если значение изменилось
+ if new_associations != current_associations:
+ self.run_update_associations_command(prefix_name, new_associations)
+
+ def run_update_associations_command(self, prefix_name, new_associations):
+ """Выполняет команду обновления ассоциаций файлов."""
+ 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)
+ # Устанавливаем новую переменную окружения для скрипта
+ env.insert("WH_XDG_OPEN", new_associations)
+ self.command_process.setProcessEnvironment(env)
+
+ 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):
"""
Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.
@@ -3514,7 +3626,7 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
return
- process = QProcess(self)
+ process = QProcess()
env = QProcessEnvironment.systemEnvironment()
cmd_start_index = 0
@@ -3532,7 +3644,10 @@ class WineHelperGUI(QMainWindow):
arguments = clean_command[cmd_start_index + 1:]
process.setProcessEnvironment(env)
- process.finished.connect(lambda: self._on_app_process_finished(desktop_path))
+ # Используем functools.partial для надежной передачи аргументов
+ # и избегания проблем с замыканием в lambda.
+ process.finished.connect(partial(self._on_app_process_finished, desktop_path))
+
try:
process.start(program, arguments)
@@ -3551,6 +3666,36 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка",
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
+ def closeEvent(self, event):
+ """Обрабатывает событие закрытия главного окна."""
+ if self.running_apps:
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle('Подтверждение выхода')
+ msg_box.setTextFormat(Qt.RichText)
+ msg_box.setText('Все запущенные приложения будут закрыты вместе с WineHelper.
'
+ "Вы уверены, что хотите выйти?")
+ 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:
+ # Корректно завершаем все дочерние процессы
+ for desktop_path, process in list(self.running_apps.items()):
+ if process.state() == QProcess.Running:
+ print(f"Завершение процесса для {desktop_path}...")
+ process.terminate()
+ if not process.waitForFinished(2000): # Ждем 2 сек
+ process.kill() # Если не закрылся, убиваем
+ event.accept()
+ else:
+ event.ignore()
+ else:
+ super().closeEvent(event)
+
def uninstall_app(self):
"""Удаляет выбранное установленное приложение и его префикс"""
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app: