From ab0e23952e19b6d407e311d7b2902b22c82ef76e Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Tue, 2 Sep 2025 16:23:39 +0600 Subject: [PATCH] added a button to create a program shortcut the function of extracting an icon from an exe file is combined with the creation of a desktop file added the function of automatic prefix detection if it is not specified --- winehelper | 150 ++++++++++++++++++++++--------------- winehelper_gui.py | 183 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 254 insertions(+), 79 deletions(-) diff --git a/winehelper b/winehelper index 6fc4da4..1963635 100755 --- a/winehelper +++ b/winehelper @@ -509,63 +509,29 @@ var_ld_library_path_update () { return 0 } -extract_icon() { - check_prefix_var - local exe_file="$1" - local ico_name="$(basename "$exe_file" .exe).ico" - local png_name="$(basename "$exe_file" .exe).png" - local tmp_ico_dir="$WH_TMP_DIR/icons" - local user_icons="$WINEPREFIX/icons" - - create_new_dir "$tmp_ico_dir" - - if ! wrestool -x -t 14 "$exe_file" -o "$tmp_ico_dir/$ico_name" ; then - print_warning "Не удалось извлечь иконку из $exe_file" - try_remove_file "$tmp_ico_dir" - return 1 - fi - - if ! icotool -x -i 1 "$tmp_ico_dir/$ico_name" -o "$tmp_ico_dir/$png_name" ; then - print_warning "Не удалось извлечь иконку из $ico_name" - try_remove_file "$tmp_ico_dir" - return 1 - fi - - create_new_dir "$user_icons" - - if ! try_copy_file "$tmp_ico_dir/$png_name" "$user_icons" ; then - print_warning "Не удалось копировать иконку в префикс" - try_remove_file "$user_icons" - return 1 - fi - - try_remove_dir "$tmp_ico_dir" - print_ok "Иконка сохранена: $user_icons/$png_name" - return 0 -} - create_desktop () { - local name_desktop exe_file desktop_filename icon_file desktop_path + local name_desktop exe_file desktop_filename icon_file desktop_path icon_arg desktop_filename_arg name_desktop="$1" exe_file="$2" - if [[ -n $4 ]] && [[ $4 != "nocopy" ]]; - then desktop_filename="$4" - else desktop_filename="$(basename "$exe_file" .exe | sed "s| |_|")" - fi - if [[ "$RESTORE_FROM_BACKUP" == "1" ]] && [[ -f "$3" ]] - then icon_file="$3" - elif [[ -f "$WH_IMAGE_PATH/$3.png" ]] - then icon_file="$WH_IMAGE_PATH/$3.png" - else icon_file="wine" + icon_arg="$3" + desktop_filename_arg="$4" + + # Определяем имя desktop-файла + if [[ -n "$desktop_filename_arg" ]] && [[ "$desktop_filename_arg" != "nocopy" ]]; then + desktop_filename="$desktop_filename_arg" + else + desktop_filename="$(basename "$exe_file" .exe | sed "s| |_|g")" fi + # Проверяем обязательные аргументы и наличие exe-файла if [[ -z "$name_desktop" ]] || [[ -z "$exe_file" ]] ; then - fatal "Used: $SCRIPT_NAME --desktop \"desktop_name\" \"path_to_exe\" \"name_png_from_image\"" + fatal "Использование: $0 create-desktop \"Имя ярлыка\" \"/путь/к/файлу.exe\" [иконка|auto] [имя_desktop_файла]" elif [[ ! -f "$exe_file" ]] ; then print_warning "Для создания ярлыка не найден исполняемый файл: $exe_file" - BASENAME_EXE="$(basename "$exe_file")" + local BASENAME_EXE="$(basename "$exe_file")" print_info "Запускаем поиск $BASENAME_EXE" + local FIND_PATH if [[ -z "$DRIVE_C" ]] || [[ ! -d "$DRIVE_C" ]] then FIND_PATH="$WH_PREFIXES_DIR" else FIND_PATH="$DRIVE_C" @@ -575,11 +541,55 @@ create_desktop () { -iname "$BASENAME_EXE")" if [[ -z "$exe_file" ]] || [[ ! -f "$exe_file" ]] then fatal "Для создания ярлыка не найден исполняемый файл: $BASENAME_EXE" - else print_ok "Исполняемый файл $BASENAME_EXE найден по пути $(dirname "$exe_file")/" + else print_ok "Исполняемый файл $BASENAME_EXE найден по пути: $(dirname "$exe_file")/" fi fi - # создаем .desktop файл + # --- Логика обработки иконки --- + local user_icons_dir="$WINEPREFIX/icons" + create_new_dir "$user_icons_dir" + + # Случай 1: Восстановление из бэкапа (передан явный путь) + if [[ "$RESTORE_FROM_BACKUP" == "1" ]] && [[ -f "$icon_arg" ]]; then + icon_file="$icon_arg" + # Случай 2: 'auto' или пустой аргумент - пытаемся извлечь из EXE + elif [[ -z "$icon_arg" ]] || [[ "$icon_arg" == "auto" ]]; then + print_info "Попытка извлечь иконку из $exe_file..." + local png_name="$(basename "$exe_file" .exe).png" + local extracted_icon_path="$user_icons_dir/$png_name" + + # Проверяем, существует ли иконка, чтобы избежать повторного извлечения + if [[ -f "$extracted_icon_path" ]]; then + print_info "Иконка уже существует: $extracted_icon_path" + icon_file="$extracted_icon_path" + else + local tmp_ico_dir="$WH_TMP_DIR/icons_$$" # Используем PID для временного каталога + create_new_dir "$tmp_ico_dir" + local ico_name="$(basename "$exe_file" .exe).ico" + + if wrestool -x -t 14 "$exe_file" -o "$tmp_ico_dir/$ico_name" &>/dev/null && \ + icotool -x -i 1 "$tmp_ico_dir/$ico_name" -o "$tmp_ico_dir/$png_name" &>/dev/null && \ + try_copy_file "$tmp_ico_dir/$png_name" "$user_icons_dir/"; then + + icon_file="$extracted_icon_path" + print_ok "Иконка успешно извлечена и сохранена: $icon_file" + else + print_warning "Не удалось извлечь иконку из $exe_file. Используется иконка по умолчанию." + icon_file="wine" # Запасной вариант + fi + try_remove_dir "$tmp_ico_dir" + fi + # Случай 3: Передано конкретное имя иконки + elif [[ -f "$WH_IMAGE_PATH/$icon_arg.png" ]]; then + icon_file="$WH_IMAGE_PATH/$icon_arg.png" + # Случай 4: Запасной вариант по умолчанию + else + print_info "Иконка '$icon_arg' не найдена. Используется иконка по умолчанию." + icon_file="wine" + fi + # --- Конец логики обработки иконки --- + + # Создаем .desktop файл create_new_dir "$WH_MENU_DIR" { echo "[Desktop Entry]" @@ -596,19 +606,29 @@ create_desktop () { cp -f "$USER_WORK_PATH/$desktop_filename.desktop" "$WH_MENU_DIR/" if [[ "$RESTORE_FROM_BACKUP" == "1" ]] ; then - print_info "Пропускаем обновление desktop.list (режим восстановления из бэкапа)" + print_info "Пропускаем обновление desktop.list (режим восстановления)" else - # добавляем информацию о приложении в "$WINEPREFIX/desktop.list" + # Добавляем информацию о приложении в "$WINEPREFIX/desktop.list" if [[ -f "$WINEPREFIX/desktop.list" ]] \ && grep -qe "^${name_desktop}=" "$WINEPREFIX/desktop.list" then sed -i "/^$name_desktop=/d" "$WINEPREFIX/desktop.list" fi - create_new_dir "$WINEPREFIX/icons" - try_copy_file "$icon_file" "$WINEPREFIX/icons/" - echo "$name_desktop=${exe_file//$WINEPREFIX/}=$(basename "$icon_file")" >> "$WINEPREFIX/desktop.list" + + # Копируем финальную иконку в директорию иконок префикса, если ее там нет + local final_icon_name + if [[ -f "$icon_file" ]]; then + final_icon_name=$(basename "$icon_file") + if [[ ! -f "$user_icons_dir/$final_icon_name" ]]; then + try_copy_file "$icon_file" "$user_icons_dir/" + fi + else + final_icon_name="$icon_file" # например, "wine" + fi + + echo "$name_desktop=${exe_file//$WINEPREFIX/}=${final_icon_name}" >> "$WINEPREFIX/desktop.list" fi - # создаем файл категории для меню + # Создаем файл категории для меню create_new_dir "$HOME/.local/share/desktop-directories" if [[ ! -f "$WH_MENU_CATEGORY" ]] ; then cat > "$WH_MENU_CATEGORY" < "$WH_MENU_CONFIG" < i + 3: icon_name = parts[i + 3] - if icon_name: + # Игнорируем служебные слова, которые не являются иконками + if icon_name and icon_name.lower() not in ('auto', 'nocopy'): icon_names.append(icon_name) except (ValueError, IndexError): continue @@ -1975,7 +1976,7 @@ class WineHelperGUI(QMainWindow): install_path_layout = QHBoxLayout() self.prefix_install_path_edit = QLineEdit() - self.prefix_install_path_edit.setPlaceholderText("Путь к .exe или .msi файлу...") + self.prefix_install_path_edit.setPlaceholderText("Укажите путь к установочному файлу .exe или .msi...") install_path_layout.addWidget(self.prefix_install_path_edit) self.prefix_browse_button = QPushButton("Обзор...") @@ -1983,10 +1984,21 @@ class WineHelperGUI(QMainWindow): install_path_layout.addWidget(self.prefix_browse_button) install_layout.addLayout(install_path_layout) + # Layout для кнопок установки и создания ярлыка + action_buttons_layout = QHBoxLayout() + self.prefix_install_button = QPushButton("Установить приложение в префикс") self.prefix_install_button.setEnabled(False) self.prefix_install_button.clicked.connect(self.run_prefix_installer) - install_layout.addWidget(self.prefix_install_button) + action_buttons_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) # Изначально неактивна + action_buttons_layout.addWidget(self.create_launcher_button) + install_layout.addLayout(action_buttons_layout) management_layout.addWidget(install_group, 4, 0, 1, 3) @@ -2001,6 +2013,19 @@ class WineHelperGUI(QMainWindow): self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) self.prefix_install_path_edit.textChanged.connect(self.update_prefix_install_button_state) + def _remove_prefix_from_gui_state(self, prefix_name): + """Удаляет префикс из внутреннего состояния и пользовательского интерфейса вкладки 'Создать префикс'.""" + if prefix_name in self.created_prefixes_info: + del self.created_prefixes_info[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) + + # Сохраняем состояние после удаления. on_created_prefix_selected также вызовет сохранение, + # но этот вызов гарантирует сохранение, даже если сигналы были заблокированы. + self._save_state() + def _load_state(self): """Загружает последнее состояние GUI из файла.""" if not os.path.exists(self.state_file): @@ -2075,31 +2100,59 @@ class WineHelperGUI(QMainWindow): msg_box.setIcon(QMessageBox.Question) msg_box.setWindowTitle('Подтверждение удаления') msg_box.setText(f'Вы уверены, что хотите удалить префикс "{prefix_name}"?\n\n' - 'Это действие необратимо и удалит все данные внутри префикса.') + 'Это действие необратимо и удалит все данные внутри префикса, а также все связанные с ним ярлыки.') - yes_button = msg_box.addButton("Да", QMessageBox.YesRole) + 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 - 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) - try: - if os.path.isdir(prefix_path): - shutil.rmtree(prefix_path) - if prefix_name in self.created_prefixes_info: - del self.created_prefixes_info[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) - QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' был успешно удален.") - except Exception as e: - QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}':\n{e}") + 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() + QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' и все связанные с ним данные были успешно удалены.") + else: + QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}'.\nПодробности смотрите в логе.") def on_prefix_name_edited(self, text): """Сбрасывает состояние управления префиксом, когда пользователь вводит новое имя.""" @@ -2116,6 +2169,9 @@ class WineHelperGUI(QMainWindow): self.prefix_management_groupbox.setEnabled(False) self.prefix_info_display.clear() self.prefix_install_path_edit.clear() + # Кнопка "Создать ярлык" должна быть активна, если выбран действительный префикс + is_prefix_selected = bool(prefix_name and prefix_name in self.created_prefixes_info) + self.create_launcher_button.setEnabled(is_prefix_selected) self.update_prefix_install_button_state() def update_prefix_info_display(self, prefix_name): @@ -2197,6 +2253,79 @@ class WineHelperGUI(QMainWindow): self.command_process.start(wine_executable, 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() @@ -2503,6 +2632,7 @@ class WineHelperGUI(QMainWindow): """Обрабатывает завершение установки в префикс.""" 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}) ===") @@ -2962,6 +3092,9 @@ class WineHelperGUI(QMainWindow): 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: @@ -3514,6 +3647,18 @@ class WineHelperGUI(QMainWindow): self.command_process = None self.command_close_button.setEnabled(True) + def _handle_launcher_creation_finished(self, exit_code, exit_status): + """Обрабатывает завершение создания ярлыка.""" + self._handle_command_finished(exit_code, exit_status) + if exit_code == 0: + QMessageBox.information(self, "Успех", "Ярлык успешно создан.") + 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: