added a prefix creation tab

This commit is contained in:
Sergey Palcheh
2025-08-24 20:56:34 +06:00
parent eea04f0d91
commit 34713bb61a
2 changed files with 495 additions and 12 deletions

View File

@@ -2176,6 +2176,7 @@ case "$arg1" in
remove-prefix) remove_prefix "$@" ;; remove-prefix) remove_prefix "$@" ;;
create-base-pfx) create_base_pfx "$@" ;; create-base-pfx) create_base_pfx "$@" ;;
generate-db) generate_wine_metadata "$@" ;; generate-db) generate_wine_metadata "$@" ;;
init-prefix) prepair_wine ; wait_wineserver ;;
*) *)
if [[ -f "$arg1" ]] ; then if [[ -f "$arg1" ]] ; then
WIN_FILE_EXEC="$(readlink -f "$arg1")" WIN_FILE_EXEC="$(readlink -f "$arg1")"

View File

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