@@ -28,6 +28,7 @@ class Var:
WH_ICON_PATH = os . environ . get ( " WH_ICON_PATH " )
LICENSE_FILE = os . environ . get ( " LICENSE_FILE " )
LICENSE_AGREEMENT_FILE = os . environ . get ( " AGREEMENT " )
THIRD_PARTY_FILE = os . environ . get ( " THIRD_PARTY_FILE " )
class DependencyManager :
""" Класс для управления проверкой и установкой системных зависимостей. """
@@ -42,14 +43,10 @@ class DependencyManager:
def _get_dependencies_path ( self ) :
""" Определяет и возвращает путь к скрипту dependencies.sh. """
if Var . DATA_PATH :
base_path = Var . DATA_PATH
elif Var . RUN_SCRIPT and os . path . exists ( Var . RUN_SCRIPT ) :
base_path = os . path . dirname ( Var . RUN_SCRIPT )
else :
if not Var . DATA_PATH :
return None
return os . path . join ( base_path , ' dependencies.sh ' )
return os . path . join ( Var . DATA_PATH , ' dependencies.sh ' )
def _calculate_file_hash ( self ) :
""" Вычисляет хэш SHA256 файла зависимостей. """
@@ -1574,6 +1571,12 @@ class WineHelperGUI(QMainWindow):
# Загружаем состояние после создания всех виджетов
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 ( )
@@ -2109,39 +2112,51 @@ class WineHelperGUI(QMainWindow):
self . prefix_winefile_button . setToolTip ( " Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса. " )
management_layout . addWidget ( self . prefix_winefile_button , 2 , 1 )
# Добавляем небольшой отступ
spacer_widget = QWidget ( )
spacer_widget . setFixedHeight ( 5 )
m ana gement_layout . addWidget ( spacer_widget , 3 , 0 , 1 , 2 )
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 . ch ange_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 , 4 , 0 )
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 , 4 , 1 )
management_layout . addWidget ( self . vkd3d_manage_button , 5 , 1 )
# --- Правая сторона: Информационный блок ---
self . prefix_info_display = QTextBrowser ( )
self . prefix_info_display . setReadOnly ( True )
self . prefix_info_display . setFrameStyle ( QFrame . StyledPanel )
# Увеличиваем rowspan, чтобы учесть добавленный отступ
management_layout . addWidget ( self . prefix_info_display , 0 , 2 , 5 , 1 )
# Увеличиваем rowspan, чтобы охватить все строки с кнопками
management_layout . addWidget ( self . prefix_info_display , 0 , 2 , 6 , 1 )
management_layout . setColumnStretch ( 0 , 1 )
management_layout . setColumnStretch ( 1 , 1 )
management_layout . setColumnStretch ( 2 , 2 )
# --- Separator and Installer ---
separator = QFrame ( )
separator . setFrameShape ( QFrame . HLine )
separator . setFrameShadow ( QFrame . Sunken )
management_layout . addWidget ( separator , 5 , 0 , 1 , 3 )
management_layout . addWidget ( separator , 6 , 0 , 1 , 3 )
install_group = QWidget ( )
install_layout = QVBoxLayout ( install_group )
@@ -2173,8 +2188,7 @@ class WineHelperGUI(QMainWindow):
self . create_launcher_button . setEnabled ( False ) # Изначально неактивна
action_buttons_layout . addWidget ( self . create_launcher_button )
install_layout . addLayout ( action_buttons_layout )
management_layout . addWidget ( install_group , 6 , 0 , 1 , 3 )
management_layout . addWidget ( install_group , 7 , 0 , 1 , 3 )
container_layout . addWidget ( self . prefix_management_groupbox )
layout . addWidget ( self . management_container_groupbox )
@@ -2204,7 +2218,7 @@ class WineHelperGUI(QMainWindow):
self . created_prefix_selector . removeItem ( index_to_remove )
def _load_created_prefixes ( self ) :
""" Загружает список созданных префиксов, сканируя файловую систему, и восстанавливает последнее выбранное состояни е . """
""" Загружает и обновляет список созданных префиксов в выпадающем списк е . """
prefixes_root_path = os . path . join ( Var . USER_WORK_PATH , " prefixes " )
if not os . path . isdir ( prefixes_root_path ) :
self . management_container_groupbox . setVisible ( False )
@@ -2232,13 +2246,6 @@ class WineHelperGUI(QMainWindow):
self . management_container_groupbox . setVisible ( True )
# По умолчанию выбираем первый элемент в списке, если он есть.
if self . created_prefix_selector . count ( ) > 0 :
self . created_prefix_selector . setCurrentIndex ( 0 )
else :
# Если список пуст, убедимся, что панель управления сброшена.
self . on_created_prefix_selected ( - 1 )
def on_created_prefix_selected ( self , index ) :
""" Обрабатывает выбор префикса из выпадающего списка. """
if index == - 1 :
@@ -2330,6 +2337,9 @@ class WineHelperGUI(QMainWindow):
else :
self . prefix_info_display . clear ( )
self . prefix_install_path_edit . clear ( )
# Сбрасываем состояние кнопок, когда префикс не выбран
self . esync_button . setChecked ( False )
self . fsync_button . setChecked ( False )
self . update_prefix_install_button_state ( )
@@ -2337,12 +2347,16 @@ class WineHelperGUI(QMainWindow):
""" Обновляет информационный блок для созданного префикса, читая данные из 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
# Словарь для хранения всех переменных из файла
@@ -2361,6 +2375,17 @@ class WineHelperGUI(QMainWindow):
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 )
# Карта для красивого отображения известных переменных
display_map = {
" WINEPREFIX " : ( " Путь " , lambda v : v ) ,
@@ -2369,8 +2394,10 @@ class WineHelperGUI(QMainWindow):
" BASE_PFX " : ( " Тип " , lambda v : ' Чистый ' if v == " none " else ' С рекомендуемыми библиотеками' ) ,
" 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 " Выключен " ) ,
}
display_order = [ " WINEPREFIX " , " WINEARCH " , " WH_WINE_USE " , " BASE_PFX " , " DXVK_VER " , " VKD3D_VER " ]
display_order = [ " WINEPREFIX " , " WINEARCH " , " WH_WINE_USE " , " BASE_PFX " , " DXVK_VER " , " VKD3D_VER " , " WINEESYNC " , " WINEFSYNC " ]
html_content = f ' <p style= " line-height: 1.3; font-size: 9pt; " > '
html_content + = f " <b>Имя:</b> { html . escape ( prefix_name ) } <br> "
@@ -2532,6 +2559,81 @@ class WineHelperGUI(QMainWindow):
# Если лицензия принята, запускаем установку.
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
if not self . _show_license_agreement_dialog ( ) :
return # Пользователь отклонил лицензию
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 )
@@ -2572,11 +2674,31 @@ class WineHelperGUI(QMainWindow):
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 )
@@ -2723,8 +2845,8 @@ class WineHelperGUI(QMainWindow):
# Читаем и парсим файл THIRD-PARTY
third_party_html = " "
third_party_file_path = os . path . join ( Var . DATA_PATH , " THIRD- PARTY" )
if os . path . exists ( third_party_file_path ) :
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 ( )
@@ -3196,20 +3318,20 @@ class WineHelperGUI(QMainWindow):
QMessageBox . critical ( self , " Ошибка " , f " Каталог префикса не найден: \n { prefix_path } " )
return
winehelper_dir = os . path . dirname ( self . winehelper_path )
winetricks_search_dir = Var . DATA_PATH
winetricks_path = None
try :
# Ищем файл, который начинается с 'winetricks_'
for filename in os . listdir ( winehelper _dir ) :
for filename in os . listdir ( winetricks_search _dir ) :
if filename . startswith ( " winetricks_ " ) :
winetricks_path = os . path . join ( winehelper _dir , filename )
winetricks_path = os . path . join ( winetricks_search _dir , filename )
break # Нашли, выходим из цикла
except OSError as e :
QMessageBox . critical ( self , " Ошибка " , f " Н е удалось прочитать директорию { winehelper _dir } : { e } " )
QMessageBox . critical ( self , " Ошибка " , f " Н е удалось прочитать директорию { winetricks_search _dir } : { e } " )
return
if not winetricks_path :
QMessageBox . critical ( self , " Ошибка " , f " Скрипт winetricks не найден в директории: \n { winehelper _dir } " )
QMessageBox . critical ( self , " Ошибка " , f " Скрипт winetricks не найден в директории: \n { winetricks_search _dir } " )
return
wine_executable = self . _get_wine_executable_for_prefix ( prefix_name )
@@ -3243,6 +3365,43 @@ class WineHelperGUI(QMainWindow):
" Будет использована системная версия 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 :
@@ -3951,13 +4110,18 @@ class WineHelperGUI(QMainWindow):
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 )
# --- Конец обновления списка префиксов ---
# Создаем кастомный диалог, чтобы кнопка была на русском