@ -2,8 +2,10 @@
import os
import subprocess
import sys
import re
import shlex
import shutil
import html
from PyQt5 . QtWidgets import ( QApplication , QMainWindow , QWidget , QVBoxLayout , QHBoxLayout , QPushButton , QLabel , QTabWidget ,
QTextEdit , QFileDialog , QMessageBox , QLineEdit , QCheckBox , QStackedWidget , QScrollArea ,
QGridLayout , QFrame , QDialog , QTextBrowser )
@ -20,6 +22,7 @@ class Var:
CHANGELOG_FILE = os . environ . get ( " CHANGELOG_FILE " )
WH_ICON_PATH = os . environ . get ( " WH_ICON_PATH " )
LICENSE_FILE = os . environ . get ( " LICENSE_FILE " )
LICENSE_AGREEMENT_FILE = os . environ . get ( " AGREEMENT " )
class WineHelperGUI ( QMainWindow ) :
def __init__ ( self ) :
@ -56,14 +59,15 @@ class WineHelperGUI(QMainWindow):
)
# Стили для оберток кнопок (для рамки выделения)
self . FRAME_STYLE_DEFAULT = " QFrame { border: 2px solid transparent; border-radius: 2 px; padding: 0px; } "
self . FRAME_STYLE_SELECTED = " QFrame { border: 2px solid #0078d7; border-radius: 2 px; padding: 0px; } "
self . FRAME_STYLE_DEFAULT = " QFrame { border: 2px solid transparent; border-radius: 8 px; padding: 0px; } "
self . FRAME_STYLE_SELECTED = " QFrame { border: 2px solid #0078d7; border-radius: 8 px; padding: 0px; } "
# Основные переменные
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 = [ ]
@ -78,10 +82,15 @@ class WineHelperGUI(QMainWindow):
# Создаем табы
self . tabs = QTabWidget ( )
self . main_layout . addWidget ( self . tabs , stretch = 2 )
self . main_layout . addWidget ( self . tabs , stretch = 1 )
# Создаем панель информации о скрипте
self . create_info_panel ( )
self . main_layout . addWidget ( self . info_panel , stretch = 1 )
# Фиксируем минимальные размеры
self . tabs . setMinimumWidth ( 520 )
self . info_panel . setMinimumWidth ( 415 )
# Вкладки
self . create_auto_install_tab ( )
@ -89,6 +98,9 @@ class WineHelperGUI(QMainWindow):
self . create_installed_tab ( )
self . create_help_tab ( )
# Инициализируем состояние, которое будет использоваться для логов
self . _reset_log_state ( )
# Обновляем список установленных приложений
self . update_installed_apps ( )
@ -162,7 +174,7 @@ class WineHelperGUI(QMainWindow):
# Заголовок
self . script_title = QLabel ( " Выберите программу " )
self . script_title . setFont ( QFont ( ' Arial ' , 14 , QFont . Bold ) ) # Шрифт и размер шрифта в заголовке инф. панели
self . script_title . setFont ( QFont ( ' Arial ' , 12 , QFont . Bold ) ) # Шрифт и размер шрифта в заголовке инф. панели
self . script_title . setAlignment ( Qt . AlignCenter )
self . info_panel_layout . addWidget ( self . script_title )
@ -198,7 +210,7 @@ class WineHelperGUI(QMainWindow):
install_action_layout = QVBoxLayout ( )
install_action_layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . install_button = QPushButton ( " Установить " )
self . install_button . setFont ( QFont ( ' Arial ' , 13 , QFont . Bold ) )
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 )
@ -249,8 +261,6 @@ class WineHelperGUI(QMainWindow):
self . installed_action_widget . setVisible ( False )
self . installed_global_action_widget . setVisible ( False )
self . main_layout . addWidget ( self . info_panel , stretch = 1 )
def browse_install_file ( self ) :
""" Открывает диалог выбора файла для ручной установки """
file_path , _ = QFileDialog . getOpenFileName (
@ -604,7 +614,6 @@ class WineHelperGUI(QMainWindow):
# Подвкладка "Лицензия"
license_tab = QWidget ( )
license_layout = QVBoxLayout ( license_tab )
import html
license_text = QTextBrowser ( )
license_text . setOpenExternalLinks ( True )
@ -836,6 +845,11 @@ class WineHelperGUI(QMainWindow):
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 ( )
@ -1209,6 +1223,7 @@ class WineHelperGUI(QMainWindow):
prog_name = self . extract_prog_name_from_script ( script_path )
prog_url = self . 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 :
# Для заголовка используем первую иконку из списка
@ -1231,7 +1246,7 @@ class WineHelperGUI(QMainWindow):
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 } " )
self . install_button . setText ( f " Установить « { display_name } » ")
def install_current_script ( self ) :
""" Устанавливает текущий выбранный скрипт """
@ -1245,7 +1260,8 @@ class WineHelperGUI(QMainWindow):
# Создаем диалоговое окно установки
self . install_dialog = QDialog ( self )
self . install_dialog . setWindowTitle ( f " Установка { self . current_script } " )
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 )
@ -1258,38 +1274,32 @@ class WineHelperGUI(QMainWindow):
license_page = QWidget ( )
license_layout = QVBoxLayout ( license_page )
license_text = QTextEdit ( )
license_text . setReadOnly ( True )
license_found = False
# Получаем текст лицензии из скрипта winehelper
script_path = os . path . join ( Var . DATA_PATH , " winehelper " )
license_content = " "
license_text = QTextBrowser ( )
# Получаем текст лицензионного соглашения из файла
try :
with open ( script_path , ' r ' , encoding = ' utf-8 ' ) as f :
capturing = False
for line in f :
if ' print_warning " Лицензионные соглашения использования сторонних компонентов: ' in line :
capturing = True
continue
license_file_path = Var . LICENSE_AGREEMENT_FILE
if not license_file_path or not os . path . exists ( license_file_path ) :
raise FileNotFoundError
if capturing :
if ' Подтверждая продолжение установки ' in line :
break
# Очищаем строку от лишних символов
clean_line = line . strip ( )
clean_line = clean_line . replace ( ' print_warning " ' , ' ' ) . replace ( ' \\ n ' , ' \n ' )
clean_line = clean_line . rstrip ( ' " ' )
license_content + = clean_line + ' \n '
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 """
<h3>Лицензионные соглашения использования сторонних компонентов:</h3 >
<p> { license_content } </p>
<pre style= " font-family: sans-serif; font-size: 10pt; white-space: pre-wrap; word-wrap: break-word; " > { escaped_license_content } </pre >
""" )
license_found = True
except ( FileNotFoundError , TypeError ) :
license_text . setHtml ( f ' <h3>Лицензионные соглашения</h3><p>Н е удалось загрузить файл лицензионного соглашения по пути:<br> { Var . LICENSE_AGREEMENT_FILE } </p> ' )
except Exception as e :
print ( f " Ошибка чтения файла для извлечения лицензии: { str ( e ) } " )
license_text . setHtml ( """
print ( f " Ошибка чтения файла лицензии: { str ( e ) } " )
license_text . setHtml ( f """
<h3>Лицензионные соглашения</h3>
<p>Н е удалось загрузить текст лицензионного соглашения. </p>
<p>Произошла ошибка при чтении файла лицензии:<br> { str ( e ) } </p>
""" )
license_layout . addWidget ( license_text )
@ -1336,11 +1346,20 @@ class WineHelperGUI(QMainWindow):
lambda state : self . btn_continue . setEnabled ( state == Qt . Checked )
)
if not license_found :
self . license_checkbox . setEnabled ( False )
self . install_dialog . show ( )
def _reset_log_state ( self ) :
""" Сбрасывает состояние буфера и флага прогресса для лога установки. """
self . output_buffer = " "
self . last_line_was_progress = False
def _prepare_installation ( self ) :
""" Подготавливает и запускает процесс установки """
self . stacked_widget . setCurrentIndex ( 1 )
self . _reset_log_state ( ) # Сбрасываем состояние для обработки лога
winehelper_path = self . winehelper_path
script_path = os . path . join ( Var . DATA_PATH ,
@ -1380,7 +1399,8 @@ class WineHelperGUI(QMainWindow):
if install_file :
args . append ( install_file )
self . append_log ( f " === Начало установки { self . current_script } === " )
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 ) } " )
@ -1388,40 +1408,106 @@ class WineHelperGUI(QMainWindow):
self . install_process . start ( winehelper_path , args )
if not self . install_process . waitForStarted ( 3000 ) :
raise RuntimeError ( " Н е удалось запустить процесс установки" )
self . append_log ( " Процесс установки успешно запущен... " )
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_log ( self , text , is_error = False ) :
def append_log ( self , text , is_error = False , add_newline = True ):
""" Добавляет сообщение в лог """
if not hasattr ( self , ' log_output ' ) : return
cursor = self . log_output . textCursor ( )
cursor . movePosition ( QTextCursor . End )
if is_error :
# Для ошибок всегда добавляем перенос строки для лучшей читаемости
cursor . insertHtml ( f ' <font color= " red " > { text } </font><br> ' )
else :
cursor . insertText ( f " { text } \n " )
# Вставляем текст. Добавляем перенос строки, если нужно.
formatted_text = f " { text } \n " if add_newline else text
cursor . insertText ( formatted_text )
self . log_output . ensureCursorVisible ( )
QApplication . processEvents ( )
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 handle_process_output ( self ) :
""" Обрабатывает вывод процесса """
output = self . install_process . readAllStandardOutput ( ) . data ( ) . decode ( ' utf-8 ' , errors = ' ignore ' ) . strip ( )
if output :
self . append_log ( output )
""" Обрабатывает вывод процесса, корректно отображая однострочный прогресс. """
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 exit_code == 0 and exit_status == QProcess . NormalExit :
self . append_log ( " \n === Установка успешно завершена === " )
# Создаем кастомный диалог, чтобы кнопка была на русском
success_box = QMessageBox ( self . install_dialog )
success_box . setWindowTitle ( " Успех " )
success_box . setText ( f " Программа { self . current_script } установлена успешно! " )
title_name = self . _get_ current_app_title ( )
success_box . setText ( f " Программа « { title_name } » установлена успешно! " )
success_box . setIcon ( QMessageBox . Information )
success_box . addButton ( " Готово " , QMessageBox . AcceptRole )
success_box . exec_ ( )