From b97b2169ab4f75bfbef0526026ec4b7af5a92738 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Fri, 22 Aug 2025 13:14:48 +0600 Subject: [PATCH 01/20] added prefix creation --- auto_completion/bash_completion/winehelper | 2 +- auto_completion/zsh_completion/_winehelper | 3 + winehelper | 72 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/auto_completion/bash_completion/winehelper b/auto_completion/bash_completion/winehelper index 0c78907..83c8109 100644 --- a/auto_completion/bash_completion/winehelper +++ b/auto_completion/bash_completion/winehelper @@ -4,7 +4,7 @@ _winehelper_completions() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="--help --version --debug install installed -r -i remove-all --clear-pfx killall remove-prefix backup-prefix restore-prefix --changelog changelog" + opts="--help --version --debug install installed -r -i remove-all --clear-pfx killall remove-prefix backup-prefix restore-prefix create-prefix --changelog changelog" wine_cmd="winecfg winereg winefile wineconsole winetricks desktop regedit explorer cmd run" case "${prev}" in diff --git a/auto_completion/zsh_completion/_winehelper b/auto_completion/zsh_completion/_winehelper index f094e47..fc12c01 100644 --- a/auto_completion/zsh_completion/_winehelper +++ b/auto_completion/zsh_completion/_winehelper @@ -14,6 +14,7 @@ _winehelper() { 'remove-all[Удалить WineHelper и все связанные данные]' '--clear-pfx[Очистить префикс \[имя_префикса\]]' 'killall[Убить все процессы]' + 'create-prefix[Создать новый префикс]' 'remove-prefix[Удалить префикс и все связанные данные]' 'backup-prefix[Создать резерную копию префикса]' 'restore-prefix[восстановить префикс из резервной копии "путь/до/whpack"]' @@ -55,6 +56,8 @@ _winehelper() { remove-prefix|backup-prefix) _get_prefixes ;; + create-prefix) + ;; restore-prefix) _files ;; diff --git a/winehelper b/winehelper index 8cf7d98..970c776 100755 --- a/winehelper +++ b/winehelper @@ -1465,6 +1465,76 @@ remove_prefix() { fi } +create_prefix() { + print_info "Существующие префиксы:" + local prefixes=() + for prefix in "$WH_PREFIXES_DIR"/*; do + if [[ -d "$prefix" ]] ; then + prefixes+=("$(basename "$prefix")") + echo " - $(basename "$prefix")" + fi + done + + if [[ ${#prefixes[@]} -eq 0 ]]; then + print_info "Нет существующих префиксов." + fi + echo + + read -p "Введите имя для нового префикса или 0 для отмены (по умолчанию: default): " prefix_name + if [[ "$prefix_name" == "0" ]] ; then + print_info "Создание префикса отменено." + exit 0 + fi + + prefix_name=${prefix_name:-default} + + if [[ ! "$prefix_name" =~ ^[a-zA-Z0-9_.-]+$ ]] ; then + fatal "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания" + fi + + if [[ -d "$WH_PREFIXES_DIR/$prefix_name" ]] ; then + fatal "Префикс с именем '$prefix_name' уже существует. Создание отменено." + fi + + print_info "Создается префикс с именем: \"$prefix_name\"" + + print_info "Выберите разрядность префикса:" + echo " 0) Отмена создания префикса" + echo " 1) 32-bit" + echo " 2) 64-bit" + echo + local arch_choice + read -p "Ваш выбор [0-2] (по умолчанию 1): " arch_choice + case "${arch_choice:-1}" in + 0) print_info "Создание префикса отменено." ; exit 0 ;; + 1) export WINEARCH="win32" ;; + 2) export WINEARCH="win64" ;; + *) fatal "Неверный выбор. Операция отменена." ;; + esac + + print_info "Выберите тип создаваемого префикса:" + echo " 0) Отмена создания префикса" + echo " 1) С рекомендуемыми библиотеками (по умолчанию)" + echo " 2) Чистый префикс (без библиотек)" + echo + local pfx_type_choice + read -p "Ваш выбор [0-2] (по умолчанию 1): " pfx_type_choice + case "${pfx_type_choice:-1}" in + 0) print_info "Создание префикса отменено." ; exit 0 ;; + 1) ;; # Оставляем BASE_PFX пустым, чтобы init_wineprefix использовал значение по умолчанию + 2) export BASE_PFX="none" ;; + *) fatal "Неверный выбор. Операция отменена." ;; + esac + + export WINEPREFIX="$WH_PREFIXES_DIR/$prefix_name" + + if ! init_wine_ver || ! init_wineprefix; then + fatal "Ошибка инициализации префикса." + fi + + print_ok "Префикс '$prefix_name' (${WINEARCH}) успешно создан." +} + remove_winehelper () { local answer if [[ $1 =~ --force|-y ]] ; then @@ -1748,6 +1818,7 @@ wh_info () { installed список установленных программ run [программа] запуск программы (отладка) remove-all удалить WineHelper и все связанные данные + create-prefix создать префикс remove-prefix [имя_префикса] удалить префикс и все связанные данные backup-prefix [имя_префикса] создать резервную копию префикса restore-prefix \"путь/до/whpack\" восстановить префикс из резервной копии @@ -1796,6 +1867,7 @@ case "$arg1" in backup-prefix) backup_prefix "$@" ;; restore-prefix) restore_prefix "$@" ;; remove-all) remove_winehelper "$@" ;; + create-prefix) create_prefix "$@" ;; remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; *) From c7564599938147955649cc1a176d4e0a9025703f Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Fri, 22 Aug 2025 18:09:12 +0600 Subject: [PATCH 02/20] added generation of wine metadata --- winehelper | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/winehelper b/winehelper index 970c776..a0b192c 100755 --- a/winehelper +++ b/winehelper @@ -1465,6 +1465,130 @@ remove_prefix() { fi } +generate_wine_metadata () { + WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" + + if [[ -f "$WINE_METADATA_FILE" ]]; then + if find "$WINE_METADATA_FILE" -mmin -1440 | grep -q . ; then + print_info "Файл метаданных $WINE_METADATA_FILE обновлялся менее 24 часов назад. Пропускаем генерацию." + return 0 + fi + print_info "Файл метаданных $WINE_METADATA_FILE устарел." + print_info "Начинаем обновление..." + else + print_info "Генерации метаданных..." + fi + + TMP_WINE_META="$WH_TMP_DIR/wine_metadata" + local FETCH_ERROR_FLAG="$TMP_WINE_META/fetch.error" + rm -f "$FETCH_ERROR_FLAG" + + mkdir -p "$TMP_WINE_META" + + cleanup() { + rm -rf "$TMP_WINE_META" + } + trap cleanup EXIT + + fetch_github_releases() { + local repo="$1" + local wine_metadata_file="$2" + + local url="https://api.github.com/repos/$repo/releases" + + if ! curl -s --fail -H "Accept: application/vnd.github.v3+json" "$url" > "$wine_metadata_file"; then + touch "$FETCH_ERROR_FLAG" + return 1 + fi + + if ! jq -e 'type == "array"' "$wine_metadata_file" >/dev/null 2>&1; then + try_remove_file "$wine_metadata_file" + touch "$FETCH_ERROR_FLAG" + return 1 + fi + + print_info "Получение данных для $repo" + } + + create_wine_entries() { + local input_file="$1" + local file_extension="$2" + local exclude_patterns="$3" + + jq -r --arg ext "$file_extension" ' + .[] | + .assets[] | + select(.browser_download_url | test($ext)) | + { + name: (.name | gsub($ext; "")), + url: .browser_download_url + } + ' "$input_file" | \ + if [[ -n "$exclude_patterns" ]]; then + jq -c --arg patterns "$exclude_patterns" ' + select(.name | test($patterns) | not) + ' + else + jq -c '.' + fi + } + + # Формат: "ключ_json;репозиторий;расширение_файла;шаблон_исключения" + local sources=( + "proton_ge;GloriousEggroll/proton-ge-custom;\\.tar\\.gz$;github-action" + "wine_kron4ek;Kron4ek/Wine-Builds;\\.tar\\.xz$;-x86" + "proton_lg;Castro-Fidel/wine_builds;\\.tar\\.xz$;plugins" + "proton_cachyos;CachyOS/proton-cachyos;\\.tar\\.xz$;znver" + "proton_sarek;pythonlover02/Proton-Sarek;\\.tar\\.gz$;" + "proton_em;Etaash-mathamsetty/Proton;\\.tar\\.xz$;" + ) + + for source_data in "${sources[@]}"; do + ( + IFS=';' read -r key repo extension exclude_pattern <<< "$source_data" + + local releases_file="$TMP_WINE_META/${key}_releases.json" + local entries_file="$TMP_WINE_META/${key}.json" + + fetch_github_releases "$repo" "$releases_file" || exit 1 + create_wine_entries "$releases_file" "$extension" "$exclude_pattern" > "$entries_file" + ) & + done + + wait + + if [[ -f "$FETCH_ERROR_FLAG" ]]; then + fatal "Ошибка при получении релизов. Возможно, превышен лимит запросов к API GitHub или проблема с сетью." + fi + + print_ok "Все данные получены." + + print_info "Создание итогового JSON файла..." + + jq -n \ + --slurpfile proton_ge "$TMP_WINE_META/proton_ge.json" \ + --slurpfile wine_kron4ek "$TMP_WINE_META/wine_kron4ek.json" \ + --slurpfile proton_lg "$TMP_WINE_META/proton_lg.json" \ + --slurpfile proton_cachyos "$TMP_WINE_META/proton_cachyos.json" \ + --slurpfile proton_sarek "$TMP_WINE_META/proton_sarek.json" \ + --slurpfile proton_em "$TMP_WINE_META/proton_em.json" \ + '{ + proton_ge: $proton_ge, + wine_kron4ek: $wine_kron4ek, + proton_lg: $proton_lg, + proton_cachyos: $proton_cachyos, + proton_sarek: $proton_sarek, + proton_em: $proton_em + }' > "$WINE_METADATA_FILE" + + if jq empty "$WINE_METADATA_FILE" 2>/dev/null; then + print_ok "JSON файл создан успешно: $WINE_METADATA_FILE" + else + print_error "Ошибка создания JSON файла" + exit 1 + fi +} + create_prefix() { print_info "Существующие префиксы:" local prefixes=() @@ -1870,6 +1994,7 @@ case "$arg1" in create-prefix) create_prefix "$@" ;; remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; + generate-db) generate_wine_metadata "$@" ;; *) if [[ -f "$arg1" ]] ; then WIN_FILE_EXEC="$(readlink -f "$arg1")" From 88f1febf547867e9679cb84214c8b6eb2a250043 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sat, 23 Aug 2025 16:07:26 +0600 Subject: [PATCH 03/20] added the function of selecting the wine version with improved downloads --- winehelper | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/winehelper b/winehelper index a0b192c..9962fa3 100755 --- a/winehelper +++ b/winehelper @@ -811,10 +811,48 @@ init_wine_ver () { export WINEDIR="$WH_DIST_DIR/$WH_WINE_USE" if [[ ! -d "$WINEDIR" ]] ; then - local wine_package="$WH_TMP_DIR/$WH_WINE_USE.tar.xz" - try_download "$CLOUD_URL/$WH_WINE_USE.tar.xz" "$wine_package" check256sum + local download_url wine_package_name wine_package check_sum_arg + WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" + + # Сначала пытаемся сформировать URL по старой схеме, как основной для автоустановки + local old_schema_url="$CLOUD_URL/$WH_WINE_USE.tar.xz" + + # `curl -f -s -I` делает HEAD-запрос. `-f` заставляет его вернуть код ошибки при 404. + if curl -f -s -I "$old_schema_url" > /dev/null ; then + download_url="$old_schema_url" + elif [[ -f "$WINE_METADATA_FILE" ]]; then + # Если по старому URL ничего нет, ищем в метаданных + print_info "Версия '$WH_WINE_USE' не найдена на основном сервере, ищем в метаданных..." + download_url=$(jq -r --arg name "$WH_WINE_USE" '.[] | .[] | select(.name == $name) | .url' "$WINE_METADATA_FILE" | head -n 1) + else + # Если и метаданных нет, то считаем, что должен был быть старый URL + print_warning "Файл метаданных не найден. Предполагается, что версия '$WH_WINE_USE' находится на основном сервере." + download_url="$old_schema_url" + fi + + # Если URL так и не был найден, выводим ошибку. + if [[ -z "$download_url" ]]; then + fatal "Не удалось найти URL для скачивания версии '$WH_WINE_USE'." + fi + + wine_package_name="$(basename "$download_url")" + wine_package="$WH_TMP_DIR/$wine_package_name" + + # Проверяем хэш-сумму только для файлов с нашего сервера + if [[ "$download_url" == *"$CLOUD_URL"* ]]; then + check_sum_arg="check256sum" + fi + + try_download "$download_url" "$wine_package" "$check_sum_arg" unpack "$wine_package" "$WH_DIST_DIR/" try_remove_file "$wine_package" + + # Handle Proton's 'files' subdirectory structure by moving contents up + if [[ -d "$WINEDIR/files" ]]; then + print_info "Обнаружена структура каталогов Proton, исправляем пути..." + mv "$WINEDIR"/files/* "$WINEDIR/" + rmdir "$WINEDIR/files" + fi fi [[ ! -f "$WINEDIR/version" ]] && echo "$WH_WINE_USE" > "$WINEDIR/version" @@ -1589,6 +1627,38 @@ generate_wine_metadata () { fi } +select_wine_version() { + if ! command -v jq &> /dev/null; then + print_warning "Команда 'jq' не найдена. Невозможно отобразить список версий WINE/Proton." + print_warning "Будет использована версия по умолчанию: $WH_WINE_USE" + return + fi + + generate_wine_metadata + WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" + [[ ! -f "$WINE_METADATA_FILE" ]] && fatal "Файл метаданных WINE не найден." + + # Создание единого, отсортированного списка версий + local versions=() + mapfile -t versions < <(jq -r '.[] | .[] | .name' "$WINE_METADATA_FILE" | sort -V -r | uniq) + + local options=("system" "${versions[@]}") + + print_info "Выберите версию WINE/Proton:" + PS3="Ваш выбор: " + select opt in "Отмена" "${options[@]}"; do + if [[ "$REPLY" == "1" ]] || [[ "$opt" == "Отмена" ]]; then + print_info "Создание префикса отменено." + exit 0 + elif [[ -n "$opt" ]]; then + export WH_WINE_USE="$opt" + break + else + print_error "Неверный выбор. Попробуйте еще раз." + fi + done +} + create_prefix() { print_info "Существующие префиксы:" local prefixes=() @@ -1636,6 +1706,8 @@ create_prefix() { *) fatal "Неверный выбор. Операция отменена." ;; esac + select_wine_version + print_info "Выберите тип создаваемого префикса:" echo " 0) Отмена создания префикса" echo " 1) С рекомендуемыми библиотеками (по умолчанию)" From 45bc97d79656505d39f66194ac45fab3a5f15c22 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sat, 23 Aug 2025 20:00:47 +0600 Subject: [PATCH 04/20] added wine/proton separation by prefix bit depth --- winehelper | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/winehelper b/winehelper index 9962fa3..78c3322 100755 --- a/winehelper +++ b/winehelper @@ -1574,7 +1574,7 @@ generate_wine_metadata () { # Формат: "ключ_json;репозиторий;расширение_файла;шаблон_исключения" local sources=( "proton_ge;GloriousEggroll/proton-ge-custom;\\.tar\\.gz$;github-action" - "wine_kron4ek;Kron4ek/Wine-Builds;\\.tar\\.xz$;-x86" + "wine_kron4ek;Kron4ek/Wine-Builds;\\.tar\\.xz$;" "proton_lg;Castro-Fidel/wine_builds;\\.tar\\.xz$;plugins" "proton_cachyos;CachyOS/proton-cachyos;\\.tar\\.xz$;znver" "proton_sarek;pythonlover02/Proton-Sarek;\\.tar\\.gz$;" @@ -1638,13 +1638,40 @@ select_wine_version() { WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" [[ ! -f "$WINE_METADATA_FILE" ]] && fatal "Файл метаданных WINE не найден." + local jq_filter + if [[ "$WINEARCH" == "win64" ]]; then + print_info "Фильтруем версии для 64-битного префикса..." + # Для 64-битных префиксов показываем сборки с 'amd64', 'x86_64', 'wow64' + # или те, у которых нет явного указания на 32-битную архитектуру. + jq_filter=' + .[] | .[] | + select( + (.name | test("amd64|x86_64|wow64")) or + (.name | test("i[3-6]86|x86(?!_64)") | not) + ) | .name + ' + else # win32 + print_info "Фильтруем версии для 32-битного префикса..." + # Для 32-битных префиксов показываем только сборки с явным указанием 32-битной архитектуры. + jq_filter=' + .[] | .[] | + select(.name | test("i[3-6]86|x86(?!_64)")) | .name + ' + fi + # Создание единого, отсортированного списка версий local versions=() - mapfile -t versions < <(jq -r '.[] | .[] | .name' "$WINE_METADATA_FILE" | sort -V -r | uniq) + mapfile -t versions < <(jq -r "$jq_filter" "$WINE_METADATA_FILE" | sort -V -r | uniq) + + if [[ ${#versions[@]} -eq 0 ]]; then + print_warning "Не найдено подходящих версий WINE/Proton для архитектуры $WINEARCH." + print_warning "Будет использована версия по умолчанию: $WH_WINE_USE" + return + fi local options=("system" "${versions[@]}") - print_info "Выберите версию WINE/Proton:" + print_info "Выберите версию WINE/Proton для $WINEARCH префикса:" PS3="Ваш выбор: " select opt in "Отмена" "${options[@]}"; do if [[ "$REPLY" == "1" ]] || [[ "$opt" == "Отмена" ]]; then From eea04f0d918ea319aaa03eba3316c8670f048b76 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sat, 23 Aug 2025 22:04:29 +0600 Subject: [PATCH 05/20] added separation of wine/proton display by groups --- winehelper | 142 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 112 insertions(+), 30 deletions(-) diff --git a/winehelper b/winehelper index 78c3322..2bf6b38 100755 --- a/winehelper +++ b/winehelper @@ -847,7 +847,7 @@ init_wine_ver () { unpack "$wine_package" "$WH_DIST_DIR/" try_remove_file "$wine_package" - # Handle Proton's 'files' subdirectory structure by moving contents up + # Управление структурой подкаталога Proton "files", перемещая содержимое вверх if [[ -d "$WINEDIR/files" ]]; then print_info "Обнаружена структура каталогов Proton, исправляем пути..." mv "$WINEDIR"/files/* "$WINEDIR/" @@ -1638,50 +1638,132 @@ select_wine_version() { WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" [[ ! -f "$WINE_METADATA_FILE" ]] && fatal "Файл метаданных WINE не найден." - local jq_filter + local arch_filter_jq if [[ "$WINEARCH" == "win64" ]]; then print_info "Фильтруем версии для 64-битного префикса..." # Для 64-битных префиксов показываем сборки с 'amd64', 'x86_64', 'wow64' # или те, у которых нет явного указания на 32-битную архитектуру. - jq_filter=' - .[] | .[] | - select( - (.name | test("amd64|x86_64|wow64")) or - (.name | test("i[3-6]86|x86(?!_64)") | not) - ) | .name - ' + arch_filter_jq='select((.name | test("amd64|x86_64|wow64")) or (.name | test("i[3-6]86|x86(?!_64)") | not))' else # win32 print_info "Фильтруем версии для 32-битного префикса..." # Для 32-битных префиксов показываем только сборки с явным указанием 32-битной архитектуры. - jq_filter=' - .[] | .[] | - select(.name | test("i[3-6]86|x86(?!_64)")) | .name - ' + arch_filter_jq='select(.name | test("i[3-6]86|x86(?!_64)"))' fi - # Создание единого, отсортированного списка версий - local versions=() - mapfile -t versions < <(jq -r "$jq_filter" "$WINE_METADATA_FILE" | sort -V -r | uniq) + local options=() + local total_versions_found=0 - if [[ ${#versions[@]} -eq 0 ]]; then + # --- System --- + options+=("--- System ---") + options+=("system") + + # --- Other versions from JSON --- + local group_keys + mapfile -t group_keys < <(jq -r 'keys_unsorted | .[]' "$WINE_METADATA_FILE") + + for key in "${group_keys[@]}"; do + local group_versions + mapfile -t group_versions < <(jq -r --arg key "$key" '.[$key] | .[] | '"$arch_filter_jq"' | .name' "$WINE_METADATA_FILE" | sort -V -r | uniq) + + if [[ ${#group_versions[@]} -gt 0 ]]; then + # Prettify the group name (e.g., "proton_ge" -> "Proton Ge") + local pretty_key + pretty_key=$(echo "$key" | tr '_' ' ' | sed -e "s/\b\(.\)/\u\1/g") + + options+=("--- $pretty_key ---") + options+=("${group_versions[@]}") + ((total_versions_found+=${#group_versions[@]})) + fi + done + + if [[ $total_versions_found -eq 0 ]]; then print_warning "Не найдено подходящих версий WINE/Proton для архитектуры $WINEARCH." print_warning "Будет использована версия по умолчанию: $WH_WINE_USE" return fi - local options=("system" "${versions[@]}") + # --- Пользовательское меню с разделением на группы и пустыми строками --- + local selectable_options=("Отмена") + local display_groups=() + local current_group_items=() + local choice_idx=0 + + # Помощник для переноса текущей группы элементов в основной массив display_groups + flush_current_group() { + if ((${#current_group_items[@]} > 0)); then + # Объединяйте элементы с помощью уникального разделителя для последующего разделения + display_groups+=("$(IFS='@@@'; echo "${current_group_items[*]}")") + current_group_items=() + fi + } + + current_group_items+=(" 0) Отмена создания префикса") + + # Обработка массива основных параметров для создания групп + for opt in "${options[@]}"; do + if [[ "$opt" == "---"* ]]; then + flush_current_group + display_groups+=("$opt") # Добавьте заголовок как отдельный элемент + else + ((choice_idx++)) + current_group_items+=(" ${choice_idx}) $opt") + selectable_options+=("$opt") + fi + done + flush_current_group # Очистка последней группы print_info "Выберите версию WINE/Proton для $WINEARCH префикса:" - PS3="Ваш выбор: " - select opt in "Отмена" "${options[@]}"; do - if [[ "$REPLY" == "1" ]] || [[ "$opt" == "Отмена" ]]; then - print_info "Создание префикса отменено." - exit 0 - elif [[ -n "$opt" ]]; then - export WH_WINE_USE="$opt" + + # Показывать группы одну за другой + local first_block=true + for group_data in "${display_groups[@]}"; do + + if [[ "$group_data" == "---"* ]]; then + if [[ "$first_block" = false ]]; then + echo + fi + echo "$group_data" + else + # Это блок элементов для печати в столбцах + local items_to_print=() + IFS='@@@' read -r -a items_to_print <<< "$group_data" + + local num_items=${#items_to_print[@]} + local term_width=${COLUMNS:-80} + local max_len=0 + for item in "${items_to_print[@]}"; do + (( ${#item} > max_len )) && max_len=${#item} + done + + ((max_len+=2)) + local num_cols=$(( term_width / max_len )) + (( num_cols = num_cols > 0 ? num_cols : 1 )) + local num_rows=$(( (num_items + num_cols - 1) / num_cols )) + + for ((i=0; i= 0 && user_choice <= max_choice )); then + if [[ "$user_choice" == "0" ]]; then + print_info "Создание префикса отменено." + exit 0 + fi + local selected_opt="${selectable_options[$user_choice]}" + export WH_WINE_USE="$selected_opt" break else - print_error "Неверный выбор. Попробуйте еще раз." + print_error "Неверный выбор. Введите число от 0 до $max_choice." fi done } @@ -1737,15 +1819,15 @@ create_prefix() { print_info "Выберите тип создаваемого префикса:" echo " 0) Отмена создания префикса" - echo " 1) С рекомендуемыми библиотеками (по умолчанию)" - echo " 2) Чистый префикс (без библиотек)" + echo " 1) Чистый префикс (без библиотек)" + echo " 2) С рекомендуемыми библиотеками (по умолчанию)" echo local pfx_type_choice read -p "Ваш выбор [0-2] (по умолчанию 1): " pfx_type_choice case "${pfx_type_choice:-1}" in 0) print_info "Создание префикса отменено." ; exit 0 ;; - 1) ;; # Оставляем BASE_PFX пустым, чтобы init_wineprefix использовал значение по умолчанию - 2) export BASE_PFX="none" ;; + 1) export BASE_PFX="none" ;; + 2) ;; # Оставляем BASE_PFX пустым, чтобы init_wineprefix использовал значение по умолчанию *) fatal "Неверный выбор. Операция отменена." ;; esac From 34713bb61a9d7276a54672f31511126b993987e1 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Sun, 24 Aug 2025 20:56:34 +0600 Subject: [PATCH 06/20] added a prefix creation tab --- winehelper | 1 + winehelper_gui.py | 506 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 495 insertions(+), 12 deletions(-) diff --git a/winehelper b/winehelper index 2bf6b38..22b3bbd 100755 --- a/winehelper +++ b/winehelper @@ -2176,6 +2176,7 @@ case "$arg1" in remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; generate-db) generate_wine_metadata "$@" ;; + init-prefix) prepair_wine ; wait_wineserver ;; *) if [[ -f "$arg1" ]] ; then WIN_FILE_EXEC="$(readlink -f "$arg1")" diff --git a/winehelper_gui.py b/winehelper_gui.py index 2895c8a..1169e88 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -11,10 +11,10 @@ import json import hashlib from functools import partial 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) 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 @@ -1011,6 +1011,205 @@ class ScriptParser: except Exception as 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): def __init__(self): super().__init__() @@ -1063,6 +1262,9 @@ class WineHelperGUI(QMainWindow): self.icon_animators = {} 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 self.main_widget = QWidget() self.setCentralWidget(self.main_widget) @@ -1085,6 +1287,7 @@ class WineHelperGUI(QMainWindow): self.create_auto_install_tab() self.create_manual_install_tab() self.create_installed_tab() + self.create_prefix_tab() self.create_help_tab() # Инициализируем состояние, которое будет использоваться для логов @@ -1167,13 +1370,13 @@ class WineHelperGUI(QMainWindow): self.info_panel_layout.setStretch(1, 1) 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._reset_info_panel_to_default(current_tab_text) if current_tab_text == "Установленные": self.filter_installed_buttons() - else: - self.info_panel.setVisible(False) def create_info_panel(self): """Создает правую панель с информацией о скрипте""" @@ -1565,6 +1768,81 @@ class WineHelperGUI(QMainWindow): ) 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("Имя нового префикса:", 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("Разрядность:", 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("Наполнение:", 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("Версия Wine/Proton:", 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): """Создает вкладку 'Справка' с подвкладками""" help_tab = QWidget() @@ -1685,6 +1963,101 @@ class WineHelperGUI(QMainWindow): 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): """Обновляет список установленных приложений в виде кнопок""" # Если активная кнопка находится в списке удаляемых, сбрасываем ее @@ -1891,7 +2264,6 @@ class WineHelperGUI(QMainWindow): 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) @@ -1935,7 +2307,6 @@ class WineHelperGUI(QMainWindow): 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) @@ -2332,6 +2703,55 @@ class WineHelperGUI(QMainWindow): self.installed_global_action_widget.setVisible(False) 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""" +
{escaped_license_content}
+ """) + except (FileNotFoundError, TypeError): + license_text.setHtml(f'

Лицензионные соглашения

Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}

') + except Exception as e: + license_text.setHtml(f'

Лицензионные соглашения

Произошла ошибка при чтении файла лицензии:
{str(e)}

') + + 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): """Устанавливает текущий выбранный скрипт""" if not self.current_script: @@ -2499,10 +2919,11 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}") self.cleanup_process() - def append_log(self, text, is_error=False, add_newline=True): - """Добавляет сообщение в лог""" - if not hasattr(self, 'log_output'): return - cursor = self.log_output.textCursor() + 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 log_widget: + return + cursor = log_widget.textCursor() cursor.movePosition(QTextCursor.End) if is_error: @@ -2513,9 +2934,13 @@ class WineHelperGUI(QMainWindow): formatted_text = f"{text}\n" if add_newline else text cursor.insertText(formatted_text) - self.log_output.ensureCursorVisible() + log_widget.ensureCursorVisible() 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): """Обрабатывает одну строку лога, управляя заменой строк прогресса.""" is_progress_line = '\r' in line_with_delimiter @@ -2552,6 +2977,42 @@ class WineHelperGUI(QMainWindow): 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): """Обрабатывает вывод процесса, корректно отображая однострочный прогресс.""" new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore') @@ -2604,6 +3065,27 @@ class WineHelperGUI(QMainWindow): # Кнопка прервать 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: From 70f2976a70ea8e0b41e71b5c56e9d5d9f30546a8 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 11:13:24 +0600 Subject: [PATCH 07/20] fixed the display in the wine version selection bar --- winehelper_gui.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 1169e88..2385c9a 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1021,6 +1021,8 @@ class WineVersionSelectionDialog(QDialog): self.user_work_path = user_work_path self.selected_version = None self.wine_versions_data = {} + self.system_wine_display_name = "Системная версия" + self.selected_display_text = None self.setWindowTitle(f"Выбор версии Wine/Proton для {architecture} префикса") self.setMinimumSize(900, 500) @@ -1110,19 +1112,19 @@ class WineVersionSelectionDialog(QDialog): re_64bit = re.compile(r'amd64|x86_64|wow64') # --- System Tab --- - system_wine_display_name = "system" if shutil.which('wine'): + self.system_wine_display_name = "Системная версия" 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})" + self.system_wine_display_name = version_line except (FileNotFoundError, subprocess.CalledProcessError) as e: print(f"Не удалось получить версию системного wine: {e}") - # Если wine возвращает ошибку, просто используем "system" + # Если wine возвращает ошибку, используем имя по умолчанию "Системная версия" - self._create_version_tab("Системный", [(system_wine_display_name, "system")]) + self._create_version_tab("Системный", [(self.system_wine_display_name, "system")]) # --- Other versions from JSON --- group_keys = sorted(self.wine_versions_data.keys()) @@ -1208,6 +1210,10 @@ class WineVersionSelectionDialog(QDialog): def on_version_selected(self, version_name): """Обрабатывает выбор версии.""" self.selected_version = version_name + if version_name == 'system': + self.selected_display_text = self.system_wine_display_name + else: + self.selected_display_text = version_name self.accept() class WineHelperGUI(QMainWindow): @@ -1261,6 +1267,7 @@ class WineHelperGUI(QMainWindow): self.current_selected_app = None self.icon_animators = {} self.previous_tab_index = 0 + self.selected_wine_version_value = None # State for command dialog log processing (specifically for prefix creation) self.command_output_buffer = "" @@ -1773,7 +1780,8 @@ class WineHelperGUI(QMainWindow): 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) + self.wine_version_edit.setText(dialog.selected_display_text) + self.selected_wine_version_value = dialog.selected_version def clear_wine_version_selection(self): """ @@ -1781,6 +1789,7 @@ class WineHelperGUI(QMainWindow): чтобы заставить пользователя выбрать заново. """ self.wine_version_edit.clear() + self.selected_wine_version_value = None def create_prefix_tab(self): """Создает вкладку для создания нового префикса""" @@ -1991,7 +2000,8 @@ class WineHelperGUI(QMainWindow): 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() + wine_use = self.selected_wine_version_value + wine_use_display = self.wine_version_edit.text() self.command_dialog = QDialog(self) self.command_dialog.setWindowTitle(f"Создание префикса: {prefix_name}") @@ -2031,8 +2041,9 @@ class WineHelperGUI(QMainWindow): 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_log_output.append(f"=== Параметры создания префикса ===\nИмя: {prefix_name}\nПуть: {prefix_path}\nАрхитектура: {wine_arch}\nВерсия Wine: {wine_use_display}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n") + self.command_log_output.textCursor().insertText(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}\n") + QApplication.processEvents() self.command_process.start(self.winehelper_path, args) self.command_dialog.exec_() From a4f01e7340642084f90c0eeab0b285c644464102 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 11:33:45 +0600 Subject: [PATCH 08/20] refactoring the install_current_script() method --- winehelper_gui.py | 75 ++++++----------------------------------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 2385c9a..5eafbc0 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -2773,71 +2773,24 @@ class WineHelperGUI(QMainWindow): QMessageBox.warning(self, "Ошибка", "Укажите путь к установочному файлу") return + if not self._show_license_agreement_dialog(): + return # Пользователь отклонил лицензию + # Создаем диалоговое окно установки self.install_dialog = QDialog(self) 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) - - self.stacked_widget = QStackedWidget() - layout = QVBoxLayout() - layout.addWidget(self.stacked_widget) - self.install_dialog.setLayout(layout) - - # Первая страница - лицензионное соглашение - license_page = QWidget() - license_layout = QVBoxLayout(license_page) - - license_found = False - - 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""" -
{escaped_license_content}
- """) - license_found = True - except (FileNotFoundError, TypeError): - license_text.setHtml(f'

Лицензионные соглашения

Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}

') - except Exception as e: - print(f"Ошибка чтения файла лицензии: {str(e)}") - license_text.setHtml(f""" -

Лицензионные соглашения

-

Произошла ошибка при чтении файла лицензии:
{str(e)}

- """) - - license_layout.addWidget(license_text) - - self.license_checkbox = QCheckBox("Я принимаю условия лицензионного соглашения") - license_layout.addWidget(self.license_checkbox) - - self.btn_continue = QPushButton("Продолжить установку") - self.btn_continue.setEnabled(False) - self.btn_continue.clicked.connect(self._prepare_installation) - license_layout.addWidget(self.btn_continue) - - # Вторая страница - лог установки - log_page = QWidget() - log_layout = QVBoxLayout(log_page) + log_layout = QVBoxLayout(self.install_dialog) self.log_output = QTextEdit() self.log_output.setReadOnly(True) self.log_output.setFont(QFont('DejaVu Sans Mono', 10)) log_layout.addWidget(self.log_output) - self.control_buttons = QWidget() - btn_layout = QHBoxLayout(self.control_buttons) + control_buttons = QWidget() + btn_layout = QHBoxLayout(control_buttons) self.btn_abort = QPushButton("Прервать") self.btn_abort.clicked.connect(self.abort_installation) btn_layout.addWidget(self.btn_abort) @@ -2846,25 +2799,16 @@ class WineHelperGUI(QMainWindow): self.btn_close.setEnabled(False) self.btn_close.clicked.connect(self.install_dialog.close) btn_layout.addWidget(self.btn_close) - - log_layout.addWidget(self.control_buttons) - - self.stacked_widget.addWidget(license_page) - self.stacked_widget.addWidget(log_page) + log_layout.addWidget(control_buttons) # Назначение кастомного обработчика закрытия окна def dialog_close_handler(event): self.handle_install_dialog_close(event) self.install_dialog.closeEvent = dialog_close_handler - self.license_checkbox.stateChanged.connect( - lambda state: self.btn_continue.setEnabled(state == Qt.Checked) - ) - - if not license_found: - self.license_checkbox.setEnabled(False) - self.install_dialog.show() + # Сразу же готовим и запускаем установку + self._prepare_installation() def _reset_log_state(self): """Сбрасывает состояние буфера и флага прогресса для лога установки.""" @@ -2873,7 +2817,6 @@ class WineHelperGUI(QMainWindow): def _prepare_installation(self): """Подготавливает и запускает процесс установки""" - self.stacked_widget.setCurrentIndex(1) self._reset_log_state() # Сбрасываем состояние для обработки лога winehelper_path = self.winehelper_path From cefb3c8d5a79038e17c483d416d7ef6d203ae531 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 13:03:36 +0600 Subject: [PATCH 09/20] changing the tab display in the main window --- winehelper_gui.py | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 5eafbc0..9df665b 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -10,7 +10,7 @@ import time import json import hashlib 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, QTabBar, QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser) from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve @@ -1275,19 +1275,26 @@ class WineHelperGUI(QMainWindow): # Создаем главный виджет и layout self.main_widget = QWidget() self.setCentralWidget(self.main_widget) - self.main_layout = QHBoxLayout() + self.main_layout = QVBoxLayout() self.main_widget.setLayout(self.main_layout) - # Создаем табы - self.tabs = QTabWidget() - self.main_layout.addWidget(self.tabs, stretch=1) + # Создаем кастомную панель вкладок и виджет со страницами + self.tab_bar = QTabBar() + self.stacked_widget = QStackedWidget() + self.main_layout.addWidget(self.tab_bar) + + # Горизонтальный layout для страниц и инфо-панели + content_layout = QHBoxLayout() + content_layout.addWidget(self.stacked_widget, stretch=1) # Создаем панель информации о скрипте self.create_info_panel() - self.main_layout.addWidget(self.info_panel, stretch=1) + content_layout.addWidget(self.info_panel, stretch=1) + + self.main_layout.addLayout(content_layout) # Фиксируем минимальные размеры - self.tabs.setMinimumWidth(520) + self.stacked_widget.setMinimumWidth(520) self.info_panel.setMinimumWidth(415) # Вкладки @@ -1304,9 +1311,10 @@ class WineHelperGUI(QMainWindow): self.update_installed_apps() # Соединяем сигнал смены вкладок с функцией - self.tabs.currentChanged.connect(self.on_tab_changed) + self.tab_bar.currentChanged.connect(self.stacked_widget.setCurrentIndex) + self.tab_bar.currentChanged.connect(self.on_tab_changed) # Устанавливаем начальное состояние видимости панели - self.on_tab_changed(self.tabs.currentIndex()) + self.on_tab_changed(self.tab_bar.currentIndex()) def activate(self): """ @@ -1319,6 +1327,11 @@ class WineHelperGUI(QMainWindow): self.raise_() self.activateWindow() + def add_tab(self, widget, title): + """Добавляет вкладку в кастомный TabBar и страницу в StackedWidget.""" + self.tab_bar.addTab(title) + self.stacked_widget.addWidget(widget) + def _reset_info_panel_to_default(self, tab_name): """Сбрасывает правую панель в состояние по умолчанию для указанной вкладки.""" if tab_name == "Автоматическая установка": @@ -1360,7 +1373,7 @@ class WineHelperGUI(QMainWindow): def on_tab_changed(self, index): """Скрывает или показывает панель информации в зависимости от активной вкладки.""" # Очищаем поле поиска на вкладке, которую покинули - previous_widget = self.tabs.widget(self.previous_tab_index) + previous_widget = self.stacked_widget.widget(self.previous_tab_index) if previous_widget: # Ищем QLineEdit в дочерних элементах search_edit = previous_widget.findChild(QLineEdit) @@ -1370,7 +1383,7 @@ class WineHelperGUI(QMainWindow): # Обновляем индекс предыдущей вкладки для следующего переключения self.previous_tab_index = index - current_tab_text = self.tabs.tabText(index) + current_tab_text = self.tab_bar.tabText(index) # Сбрасываем растяжение к состоянию по умолчанию: # растягивается виджет с описанием (индекс 1), а не виджет с действиями (индекс 4) @@ -1738,7 +1751,7 @@ class WineHelperGUI(QMainWindow): buttons_list = [] self._populate_install_grid(grid_layout, scripts, script_folder, buttons_list) - self.tabs.addTab(tab_widget, tab_title) + self.add_tab(tab_widget, tab_title) return scripts, buttons_list, grid_layout, search_edit, scroll_area @@ -1773,7 +1786,7 @@ class WineHelperGUI(QMainWindow): installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab( "Поиск установленной программы...", self.filter_installed_buttons ) - self.tabs.addTab(installed_tab, "Установленные") + self.add_tab(installed_tab, "Установленные") def open_wine_version_dialog(self): """Открывает диалог выбора версии Wine.""" @@ -1846,7 +1859,7 @@ class WineHelperGUI(QMainWindow): self.create_prefix_button.clicked.connect(self.start_prefix_creation) layout.addWidget(self.create_prefix_button) - self.tabs.addTab(self.prefix_tab, "Создать префикс") + self.add_tab(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) @@ -1970,7 +1983,7 @@ class WineHelperGUI(QMainWindow): changelog_layout.addWidget(changelog_text) help_subtabs.addTab(changelog_tab, "История изменений") - self.tabs.addTab(help_tab, "Справка") + self.add_tab(help_tab, "Справка") def update_create_prefix_button_state(self): """Включает или выключает кнопку 'Создать префикс' в зависимости от заполнения полей.""" From f73f717d0e0ec043f5673e969ecae81d718f0aa0 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 14:19:45 +0600 Subject: [PATCH 10/20] the prefix control unit has been added to the Create prefix tab --- winehelper_gui.py | 133 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 11 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 9df665b..f1f769b 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1268,6 +1268,8 @@ class WineHelperGUI(QMainWindow): self.icon_animators = {} self.previous_tab_index = 0 self.selected_wine_version_value = None + self.last_created_prefix_name = None + self.last_created_prefix_info = None # State for command dialog log processing (specifically for prefix creation) self.command_output_buffer = "" @@ -1814,7 +1816,7 @@ class WineHelperGUI(QMainWindow): form_layout.setSpacing(10) self.prefix_name_edit = QLineEdit() - self.prefix_name_edit.setPlaceholderText("Например: 'm_prefix'") + self.prefix_name_edit.setPlaceholderText("Например: my_prefix") form_layout.addRow("Имя нового префикса:", self.prefix_name_edit) self.arch_groupbox = QGroupBox("Архитектура") @@ -1849,22 +1851,104 @@ class WineHelperGUI(QMainWindow): version_layout.addWidget(select_version_button) form_layout.addRow("Версия Wine/Proton:", version_layout) - layout.addLayout(form_layout) - layout.addStretch() + # --- GroupBox for managing the created prefix --- + self.prefix_management_groupbox = QGroupBox("Управление префиксом") + self.prefix_management_groupbox.setEnabled(False) # Disabled by default + management_layout = QGridLayout(self.prefix_management_groupbox) + management_layout.setSpacing(5) + + # --- Left side: Buttons --- + self.prefix_winetricks_button = QPushButton("Менеджер компонентов") + self.prefix_winetricks_button.clicked.connect( + lambda: self.open_winetricks_manager(prefix_name=self.last_created_prefix_name)) + self.prefix_winetricks_button.setToolTip( + "Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.") + management_layout.addWidget(self.prefix_winetricks_button, 0, 0) + + self.prefix_winecfg_button = QPushButton("Редактор настроек") + self.prefix_winecfg_button.clicked.connect( + lambda: self._run_wine_util('winecfg', prefix_name=self.last_created_prefix_name)) + self.prefix_winecfg_button.setToolTip( + "Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).") + management_layout.addWidget(self.prefix_winecfg_button, 0, 1) + + self.prefix_regedit_button = QPushButton("Редактор реестра") + self.prefix_regedit_button.clicked.connect( + lambda: self._run_wine_util('regedit', prefix_name=self.last_created_prefix_name)) + self.prefix_regedit_button.setToolTip( + "Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.") + management_layout.addWidget(self.prefix_regedit_button, 1, 0) + + self.prefix_uninstaller_button = QPushButton("Удаление программ") + self.prefix_uninstaller_button.clicked.connect( + lambda: self._run_wine_util('uninstaller', prefix_name=self.last_created_prefix_name)) + self.prefix_uninstaller_button.setToolTip( + "Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.") + management_layout.addWidget(self.prefix_uninstaller_button, 1, 1) + + self.prefix_cmd_button = QPushButton("Командная строка") + self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.last_created_prefix_name)) + self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") + management_layout.addWidget(self.prefix_cmd_button, 2, 0) + + self.prefix_winefile_button = QPushButton("Файловый менеджер") + self.prefix_winefile_button.clicked.connect( + lambda: self._run_wine_util('winefile', prefix_name=self.last_created_prefix_name)) + self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") + management_layout.addWidget(self.prefix_winefile_button, 2, 1) + + # --- Right side: Info display --- + self.prefix_info_display = QTextBrowser() + self.prefix_info_display.setReadOnly(True) + self.prefix_info_display.setFrameStyle(QFrame.StyledPanel) + management_layout.addWidget(self.prefix_info_display, 0, 2, 3, 1) + + # Устанавливаем растяжение колонок, чтобы область кнопок и область информации были примерно равны + management_layout.setColumnStretch(0, 1) + management_layout.setColumnStretch(1, 1) + management_layout.setColumnStretch(2, 2) 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) + layout.addLayout(form_layout) + layout.addWidget(self.create_prefix_button) + layout.addWidget(self.prefix_management_groupbox) + layout.addStretch() self.add_tab(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.prefix_name_edit.textChanged.connect(self.on_prefix_name_edited) self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) + def on_prefix_name_edited(self, text): + """Сбрасывает состояние управления префиксом, когда пользователь вводит новое имя.""" + if text and self.prefix_management_groupbox.isEnabled(): + self.prefix_management_groupbox.setEnabled(False) + self.prefix_management_groupbox.setTitle("Управление префиксом") + self.last_created_prefix_name = None + self.last_created_prefix_info = None + self.prefix_info_display.clear() + + def update_prefix_info_display(self): + """Обновляет информационный блок для созданного префикса.""" + if not self.last_created_prefix_info: + self.prefix_info_display.clear() + return + + info = self.last_created_prefix_info + html_content = f""" +

+ Имя: {html.escape(info['name'])}
+ Архитектура: {html.escape(info['arch'])}
+ Версия Wine: {html.escape(info['wine_version'])}
+ Путь: {html.escape(info['path'])}

""" + self.prefix_info_display.setHtml(html_content) + def create_help_tab(self): """Создает вкладку 'Справка' с подвкладками""" help_tab = QWidget() @@ -1996,6 +2080,13 @@ class WineHelperGUI(QMainWindow): if not self._show_license_agreement_dialog(): return + # Сбрасываем состояние управления предыдущим созданным префиксом + self.last_created_prefix_name = None + self.last_created_prefix_info = None + self.prefix_info_display.clear() + self.prefix_management_groupbox.setEnabled(False) + self.prefix_management_groupbox.setTitle("Управление префиксом") + prefix_name = self.prefix_name_edit.text().strip() if not prefix_name: @@ -2016,6 +2107,15 @@ class WineHelperGUI(QMainWindow): wine_use = self.selected_wine_version_value wine_use_display = self.wine_version_edit.text() + # Сохраняем информацию для отображения после создания + self.last_created_prefix_info = { + 'name': prefix_name, + 'path': prefix_path, + 'arch': "32-bit" if wine_arch == "win32" else "64-bit", + 'type': 'Чистый' if base_pfx else 'С рекомендуемыми библиотеками', + 'wine_version': wine_use_display + } + self.command_dialog = QDialog(self) self.command_dialog.setWindowTitle(f"Создание префикса: {prefix_name}") self.command_dialog.setMinimumSize(750, 400) @@ -2078,9 +2178,14 @@ class WineHelperGUI(QMainWindow): self._handle_command_finished(exit_code, exit_status) if exit_code == 0: + self.last_created_prefix_name = prefix_name + self.prefix_management_groupbox.setTitle(f"Управление префиксом «{prefix_name}»") + self.prefix_management_groupbox.setEnabled(True) + self.update_prefix_info_display() self.prefix_name_edit.clear() self.wine_version_edit.clear() - QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' успешно создан.") + QMessageBox.information(self, "Успех", + f"Префикс '{prefix_name}' успешно создан.\nТеперь вы можете управлять им с помощью кнопок ниже.") def update_installed_apps(self): """Обновляет список установленных приложений в виде кнопок""" @@ -2364,11 +2469,14 @@ class WineHelperGUI(QMainWindow): if msg_box.clickedButton() == yes_button: self._run_app_launcher(debug=True) - def open_winetricks_manager(self): + def open_winetricks_manager(self, prefix_name=None): """Открывает новый диалог для управления компонентами Winetricks.""" - prefix_name = self._get_prefix_name_for_selected_app() if not prefix_name: - QMessageBox.warning(self, "Менеджер Winetricks", "Сначала выберите установленное приложение, чтобы открыть менеджер для его префикса.") + prefix_name = self._get_prefix_name_for_selected_app() + + if not prefix_name: + QMessageBox.warning(self, "Менеджер Winetricks", + "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") return prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) @@ -2395,11 +2503,14 @@ class WineHelperGUI(QMainWindow): dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) dialog.exec_() - def _run_wine_util(self, util_name): + def _run_wine_util(self, util_name, prefix_name=None): """Запускает стандартную утилиту Wine для выбранного префикса.""" - prefix_name = self._get_prefix_name_for_selected_app() if not prefix_name: - QMessageBox.warning(self, "Ошибка", "Сначала выберите установленное приложение.") + prefix_name = self._get_prefix_name_for_selected_app() + + if not prefix_name: + QMessageBox.warning(self, "Ошибка", + "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") return prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) From dd5d8bb657edc8f5091a6a5dc223a7e785f1ed57 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 14:41:40 +0600 Subject: [PATCH 11/20] changes to the default fill button --- winehelper_gui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index f1f769b..9ade5cb 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1832,8 +1832,11 @@ class WineHelperGUI(QMainWindow): self.type_groupbox = QGroupBox("Тип префикса") type_layout = QHBoxLayout() self.type_clean_radio = QRadioButton("Чистый") + self.type_clean_radio.setToolTip("Создает пустой префикс Wine без каких-либо дополнительных компонентов.") self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками") - self.type_recommended_radio.setChecked(True) + tooltip_text = "Устанавливает базовый набор компонентов, необходимый для большинства приложений" + self.type_recommended_radio.setToolTip(tooltip_text) + self.type_clean_radio.setChecked(True) type_layout.addWidget(self.type_clean_radio) type_layout.addWidget(self.type_recommended_radio) self.type_groupbox.setLayout(type_layout) From d499147bdc6f148b78992104d750d4a0dbc71e99 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 15:20:57 +0600 Subject: [PATCH 12/20] added the choice of a prefix to be created for management --- winehelper_gui.py | 272 +++++++++++++++++++++++++++++++--------------- 1 file changed, 185 insertions(+), 87 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 9ade5cb..08f4c47 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1268,8 +1268,14 @@ class WineHelperGUI(QMainWindow): self.icon_animators = {} self.previous_tab_index = 0 self.selected_wine_version_value = None - self.last_created_prefix_name = None - self.last_created_prefix_info = None + self.created_prefixes_info = {} # Хранит информацию о префиксах, созданных на вкладке + self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке + self.pending_prefix_info = None # Временное хранилище информации о создаваемом префиксе + + # Добавим путь к файлу состояния + self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper") + os.makedirs(self.config_dir, exist_ok=True) + self.state_file = os.path.join(self.config_dir, "gui_state.json") # State for command dialog log processing (specifically for prefix creation) self.command_output_buffer = "" @@ -1306,6 +1312,9 @@ class WineHelperGUI(QMainWindow): self.create_prefix_tab() self.create_help_tab() + # Загружаем состояние после создания всех виджетов + self._load_state() + # Инициализируем состояние, которое будет использоваться для логов self._reset_log_state() @@ -1819,18 +1828,19 @@ class WineHelperGUI(QMainWindow): self.prefix_name_edit.setPlaceholderText("Например: my_prefix") form_layout.addRow("Имя нового префикса:", self.prefix_name_edit) - self.arch_groupbox = QGroupBox("Архитектура") - arch_layout = QHBoxLayout() + arch_widget = QWidget() + arch_layout = QHBoxLayout(arch_widget) + arch_layout.setContentsMargins(0, 0, 0, 0) 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("Разрядность:", self.arch_groupbox) + form_layout.addRow("Разрядность:", arch_widget) - self.type_groupbox = QGroupBox("Тип префикса") - type_layout = QHBoxLayout() + type_widget = QWidget() + type_layout = QHBoxLayout(type_widget) + type_layout.setContentsMargins(0, 0, 0, 0) self.type_clean_radio = QRadioButton("Чистый") self.type_clean_radio.setToolTip("Создает пустой префикс Wine без каких-либо дополнительных компонентов.") self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками") @@ -1839,8 +1849,7 @@ class WineHelperGUI(QMainWindow): self.type_clean_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("Наполнение:", self.type_groupbox) + form_layout.addRow("Наполнение:", type_widget) self.wine_version_edit = QLineEdit() self.wine_version_edit.setReadOnly(True) @@ -1854,63 +1863,6 @@ class WineHelperGUI(QMainWindow): version_layout.addWidget(select_version_button) form_layout.addRow("Версия Wine/Proton:", version_layout) - # --- GroupBox for managing the created prefix --- - self.prefix_management_groupbox = QGroupBox("Управление префиксом") - self.prefix_management_groupbox.setEnabled(False) # Disabled by default - management_layout = QGridLayout(self.prefix_management_groupbox) - management_layout.setSpacing(5) - - # --- Left side: Buttons --- - self.prefix_winetricks_button = QPushButton("Менеджер компонентов") - self.prefix_winetricks_button.clicked.connect( - lambda: self.open_winetricks_manager(prefix_name=self.last_created_prefix_name)) - self.prefix_winetricks_button.setToolTip( - "Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.") - management_layout.addWidget(self.prefix_winetricks_button, 0, 0) - - self.prefix_winecfg_button = QPushButton("Редактор настроек") - self.prefix_winecfg_button.clicked.connect( - lambda: self._run_wine_util('winecfg', prefix_name=self.last_created_prefix_name)) - self.prefix_winecfg_button.setToolTip( - "Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).") - management_layout.addWidget(self.prefix_winecfg_button, 0, 1) - - self.prefix_regedit_button = QPushButton("Редактор реестра") - self.prefix_regedit_button.clicked.connect( - lambda: self._run_wine_util('regedit', prefix_name=self.last_created_prefix_name)) - self.prefix_regedit_button.setToolTip( - "Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.") - management_layout.addWidget(self.prefix_regedit_button, 1, 0) - - self.prefix_uninstaller_button = QPushButton("Удаление программ") - self.prefix_uninstaller_button.clicked.connect( - lambda: self._run_wine_util('uninstaller', prefix_name=self.last_created_prefix_name)) - self.prefix_uninstaller_button.setToolTip( - "Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.") - management_layout.addWidget(self.prefix_uninstaller_button, 1, 1) - - self.prefix_cmd_button = QPushButton("Командная строка") - self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.last_created_prefix_name)) - self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") - management_layout.addWidget(self.prefix_cmd_button, 2, 0) - - self.prefix_winefile_button = QPushButton("Файловый менеджер") - self.prefix_winefile_button.clicked.connect( - lambda: self._run_wine_util('winefile', prefix_name=self.last_created_prefix_name)) - self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") - management_layout.addWidget(self.prefix_winefile_button, 2, 1) - - # --- Right side: Info display --- - self.prefix_info_display = QTextBrowser() - self.prefix_info_display.setReadOnly(True) - self.prefix_info_display.setFrameStyle(QFrame.StyledPanel) - management_layout.addWidget(self.prefix_info_display, 0, 2, 3, 1) - - # Устанавливаем растяжение колонок, чтобы область кнопок и область информации были примерно равны - management_layout.setColumnStretch(0, 1) - management_layout.setColumnStretch(1, 1) - management_layout.setColumnStretch(2, 2) - 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;") @@ -1919,7 +1871,75 @@ class WineHelperGUI(QMainWindow): layout.addLayout(form_layout) layout.addWidget(self.create_prefix_button) - layout.addWidget(self.prefix_management_groupbox) + + # --- Контейнер для выбора и управления созданными префиксами --- + self.management_container_groupbox = QGroupBox("Управление созданными префиксами") + self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов + container_layout = QVBoxLayout(self.management_container_groupbox) + + self.created_prefix_selector = QComboBox() + self.created_prefix_selector.setPlaceholderText("Выберите префикс для управления") + self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected) + container_layout.addWidget(self.created_prefix_selector) + + # --- GroupBox для управления выбранным префиксом --- + self.prefix_management_groupbox = QGroupBox("Управление") # Заголовок будет меняться + self.prefix_management_groupbox.setEnabled(False) + management_layout = QGridLayout(self.prefix_management_groupbox) + management_layout.setSpacing(5) + + # --- Левая сторона: Кнопки --- + self.prefix_winetricks_button = QPushButton("Менеджер компонентов") + self.prefix_winetricks_button.clicked.connect( + lambda: self.open_winetricks_manager(prefix_name=self.current_managed_prefix_name)) + self.prefix_winetricks_button.setToolTip( + "Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.") + management_layout.addWidget(self.prefix_winetricks_button, 0, 0) + + self.prefix_winecfg_button = QPushButton("Редактор настроек") + self.prefix_winecfg_button.clicked.connect( + lambda: self._run_wine_util('winecfg', prefix_name=self.current_managed_prefix_name)) + self.prefix_winecfg_button.setToolTip( + "Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).") + management_layout.addWidget(self.prefix_winecfg_button, 0, 1) + + self.prefix_regedit_button = QPushButton("Редактор реестра") + self.prefix_regedit_button.clicked.connect( + lambda: self._run_wine_util('regedit', prefix_name=self.current_managed_prefix_name)) + self.prefix_regedit_button.setToolTip( + "Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.") + management_layout.addWidget(self.prefix_regedit_button, 1, 0) + + self.prefix_uninstaller_button = QPushButton("Удаление программ") + self.prefix_uninstaller_button.clicked.connect( + lambda: self._run_wine_util('uninstaller', prefix_name=self.current_managed_prefix_name)) + self.prefix_uninstaller_button.setToolTip( + "Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.") + management_layout.addWidget(self.prefix_uninstaller_button, 1, 1) + + self.prefix_cmd_button = QPushButton("Командная строка") + self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.current_managed_prefix_name)) + self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") + management_layout.addWidget(self.prefix_cmd_button, 2, 0) + + self.prefix_winefile_button = QPushButton("Файловый менеджер") + self.prefix_winefile_button.clicked.connect( + lambda: self._run_wine_util('winefile', prefix_name=self.current_managed_prefix_name)) + self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") + management_layout.addWidget(self.prefix_winefile_button, 2, 1) + + # --- Правая сторона: Информационный блок --- + self.prefix_info_display = QTextBrowser() + self.prefix_info_display.setReadOnly(True) + self.prefix_info_display.setFrameStyle(QFrame.StyledPanel) + management_layout.addWidget(self.prefix_info_display, 0, 2, 3, 1) + + management_layout.setColumnStretch(0, 1) + management_layout.setColumnStretch(1, 1) + management_layout.setColumnStretch(2, 2) + + container_layout.addWidget(self.prefix_management_groupbox) + layout.addWidget(self.management_container_groupbox) layout.addStretch() self.add_tab(self.prefix_tab, "Создать префикс") @@ -1928,22 +1948,92 @@ class WineHelperGUI(QMainWindow): self.prefix_name_edit.textChanged.connect(self.on_prefix_name_edited) self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) + def _load_state(self): + """Загружает последнее состояние GUI из файла.""" + if not os.path.exists(self.state_file): + return + try: + with open(self.state_file, 'r', encoding='utf-8') as f: + state = json.load(f) + + loaded_prefixes = state.get('created_prefixes_info', {}) + if not loaded_prefixes: + return + + # Блокируем сигналы, чтобы избежать преждевременного срабатывания + self.created_prefix_selector.blockSignals(True) + self.created_prefix_selector.clear() + self.created_prefixes_info.clear() + + for prefix_name, prefix_info in loaded_prefixes.items(): + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if os.path.isdir(prefix_path): + self.created_prefixes_info[prefix_name] = prefix_info + self.created_prefix_selector.addItem(prefix_name) + + self.created_prefix_selector.blockSignals(False) + + if self.created_prefix_selector.count() > 0: + self.management_container_groupbox.setVisible(True) + last_managed = state.get('current_managed_prefix_name') + index = self.created_prefix_selector.findText(last_managed) + if index != -1: + self.created_prefix_selector.setCurrentIndex(index) + else: + self.created_prefix_selector.setCurrentIndex(0) # Выбираем первый, если предыдущий не найден + else: + self.management_container_groupbox.setVisible(False) + + except (IOError, json.JSONDecodeError, TypeError) as e: + print(f"Предупреждение: не удалось загрузить состояние GUI: {e}") + + def _save_state(self): + """Сохраняет текущее состояние GUI в файл.""" + state = { + 'created_prefixes_info': self.created_prefixes_info, + 'current_managed_prefix_name': self.current_managed_prefix_name + } + try: + with open(self.state_file, 'w', encoding='utf-8') as f: + json.dump(state, f, indent=2, ensure_ascii=False) + except IOError as e: + print(f"Предупреждение: не удалось сохранить состояние GUI: {e}") + + def on_created_prefix_selected(self, index): + """Обрабатывает выбор префикса из выпадающего списка.""" + if index == -1: + self.current_managed_prefix_name = None + self._setup_prefix_management_panel(None) + else: + prefix_name = self.created_prefix_selector.itemText(index) + self.current_managed_prefix_name = prefix_name + self._setup_prefix_management_panel(prefix_name) + self._save_state() + def on_prefix_name_edited(self, text): """Сбрасывает состояние управления префиксом, когда пользователь вводит новое имя.""" - if text and self.prefix_management_groupbox.isEnabled(): + if text: + if self.created_prefix_selector.currentIndex() != -1: + self.created_prefix_selector.setCurrentIndex(-1) + + def _setup_prefix_management_panel(self, prefix_name): + """Настраивает панель управления префиксом на основе текущего состояния.""" + if prefix_name and prefix_name in self.created_prefixes_info: + self.prefix_management_groupbox.setTitle(f"Управление «{prefix_name}»") + self.prefix_management_groupbox.setEnabled(True) + self.update_prefix_info_display(prefix_name) + else: + self.prefix_management_groupbox.setTitle("Управление") self.prefix_management_groupbox.setEnabled(False) - self.prefix_management_groupbox.setTitle("Управление префиксом") - self.last_created_prefix_name = None - self.last_created_prefix_info = None self.prefix_info_display.clear() - def update_prefix_info_display(self): + def update_prefix_info_display(self, prefix_name): """Обновляет информационный блок для созданного префикса.""" - if not self.last_created_prefix_info: + info = self.created_prefixes_info.get(prefix_name) + if not info: self.prefix_info_display.clear() return - info = self.last_created_prefix_info html_content = f"""

Имя: {html.escape(info['name'])}
@@ -2083,12 +2173,9 @@ class WineHelperGUI(QMainWindow): if not self._show_license_agreement_dialog(): return - # Сбрасываем состояние управления предыдущим созданным префиксом - self.last_created_prefix_name = None - self.last_created_prefix_info = None - self.prefix_info_display.clear() - self.prefix_management_groupbox.setEnabled(False) - self.prefix_management_groupbox.setTitle("Управление префиксом") + # Сбрасываем выбор в выпадающем списке, чтобы панель управления скрылась на время создания + if self.created_prefix_selector.count() > 0: + self.created_prefix_selector.setCurrentIndex(-1) prefix_name = self.prefix_name_edit.text().strip() @@ -2111,7 +2198,7 @@ class WineHelperGUI(QMainWindow): wine_use_display = self.wine_version_edit.text() # Сохраняем информацию для отображения после создания - self.last_created_prefix_info = { + self.pending_prefix_info = { 'name': prefix_name, 'path': prefix_path, 'arch': "32-bit" if wine_arch == "win32" else "64-bit", @@ -2181,14 +2268,25 @@ class WineHelperGUI(QMainWindow): self._handle_command_finished(exit_code, exit_status) if exit_code == 0: - self.last_created_prefix_name = prefix_name - self.prefix_management_groupbox.setTitle(f"Управление префиксом «{prefix_name}»") - self.prefix_management_groupbox.setEnabled(True) - self.update_prefix_info_display() + # Добавляем новый префикс в список и выбираем его + if self.pending_prefix_info: + self.created_prefixes_info[prefix_name] = self.pending_prefix_info + self.pending_prefix_info = None + + if self.created_prefix_selector.findText(prefix_name) == -1: + self.created_prefix_selector.addItem(prefix_name) + + self.created_prefix_selector.setCurrentText(prefix_name) + + if not self.management_container_groupbox.isVisible(): + self.management_container_groupbox.setVisible(True) + + self._save_state() self.prefix_name_edit.clear() self.wine_version_edit.clear() QMessageBox.information(self, "Успех", - f"Префикс '{prefix_name}' успешно создан.\nТеперь вы можете управлять им с помощью кнопок ниже.") + f"Префикс '{prefix_name}' успешно создан.\n" + "Теперь вы можете управлять им, выбрав его из выпадающего списка.") def update_installed_apps(self): """Обновляет список установленных приложений в виде кнопок""" From 274a21941dcd3172e5a6d75f2a8e45ebe5943ffb Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 15:43:34 +0600 Subject: [PATCH 13/20] added deletion of the created prefix --- winehelper_gui.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 08f4c47..ecc1c73 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1877,10 +1877,20 @@ class WineHelperGUI(QMainWindow): self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов container_layout = QVBoxLayout(self.management_container_groupbox) + selector_layout = QHBoxLayout() self.created_prefix_selector = QComboBox() self.created_prefix_selector.setPlaceholderText("Выберите префикс для управления") self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected) - container_layout.addWidget(self.created_prefix_selector) + selector_layout.addWidget(self.created_prefix_selector, 1) + + self.delete_prefix_button = QPushButton() + self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash")) + self.delete_prefix_button.setToolTip("Удалить выбранный префикс") + self.delete_prefix_button.setEnabled(False) + self.delete_prefix_button.clicked.connect(self.delete_selected_prefix) + selector_layout.addWidget(self.delete_prefix_button) + + container_layout.addLayout(selector_layout) # --- GroupBox для управления выбранным префиксом --- self.prefix_management_groupbox = QGroupBox("Управление") # Заголовок будет меняться @@ -2004,12 +2014,50 @@ class WineHelperGUI(QMainWindow): if index == -1: self.current_managed_prefix_name = None self._setup_prefix_management_panel(None) + self.delete_prefix_button.setEnabled(False) else: prefix_name = self.created_prefix_selector.itemText(index) self.current_managed_prefix_name = prefix_name self._setup_prefix_management_panel(prefix_name) + self.delete_prefix_button.setEnabled(True) self._save_state() + def delete_selected_prefix(self): + """Удаляет префикс, выбранный в выпадающем списке на вкладке 'Создать префикс'.""" + prefix_name = self.current_managed_prefix_name + if not prefix_name: + return + + msg_box = QMessageBox(self) + msg_box.setIcon(QMessageBox.Question) + msg_box.setWindowTitle('Подтверждение удаления') + msg_box.setText(f'Вы уверены, что хотите удалить префикс "{prefix_name}"?\n\n' + 'Это действие необратимо и удалит все данные внутри префикса.') + + 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) + + 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}") + def on_prefix_name_edited(self, text): """Сбрасывает состояние управления префиксом, когда пользователь вводит новое имя.""" if text: From 0608a3f250019222e3799ad803f06be12c713c2f Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 15:56:47 +0600 Subject: [PATCH 14/20] the Management header has been deleted --- winehelper_gui.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index ecc1c73..63dc9da 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1892,8 +1892,8 @@ class WineHelperGUI(QMainWindow): container_layout.addLayout(selector_layout) - # --- GroupBox для управления выбранным префиксом --- - self.prefix_management_groupbox = QGroupBox("Управление") # Заголовок будет меняться + # --- Виджет для управления выбранным префиксом --- + self.prefix_management_groupbox = QWidget() self.prefix_management_groupbox.setEnabled(False) management_layout = QGridLayout(self.prefix_management_groupbox) management_layout.setSpacing(5) @@ -2067,11 +2067,9 @@ class WineHelperGUI(QMainWindow): def _setup_prefix_management_panel(self, prefix_name): """Настраивает панель управления префиксом на основе текущего состояния.""" if prefix_name and prefix_name in self.created_prefixes_info: - self.prefix_management_groupbox.setTitle(f"Управление «{prefix_name}»") self.prefix_management_groupbox.setEnabled(True) self.update_prefix_info_display(prefix_name) else: - self.prefix_management_groupbox.setTitle("Управление") self.prefix_management_groupbox.setEnabled(False) self.prefix_info_display.clear() From aadd579cdc849800792edd41f0814f647a370144 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Mon, 25 Aug 2025 16:58:42 +0600 Subject: [PATCH 15/20] added the ability to install the application in the created prefix --- winehelper_gui.py | 172 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 144 insertions(+), 28 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 63dc9da..81056b5 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1220,7 +1220,7 @@ class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("WineHelper") - self.setMinimumSize(950, 500) + self.setMinimumSize(950, 550) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH): self.setWindowIcon(QIcon(Var.WH_ICON_PATH)) @@ -1900,6 +1900,7 @@ class WineHelperGUI(QMainWindow): # --- Левая сторона: Кнопки --- self.prefix_winetricks_button = QPushButton("Менеджер компонентов") + self.prefix_winetricks_button.setMinimumHeight(32) self.prefix_winetricks_button.clicked.connect( lambda: self.open_winetricks_manager(prefix_name=self.current_managed_prefix_name)) self.prefix_winetricks_button.setToolTip( @@ -1907,6 +1908,7 @@ class WineHelperGUI(QMainWindow): management_layout.addWidget(self.prefix_winetricks_button, 0, 0) self.prefix_winecfg_button = QPushButton("Редактор настроек") + self.prefix_winecfg_button.setMinimumHeight(32) self.prefix_winecfg_button.clicked.connect( lambda: self._run_wine_util('winecfg', prefix_name=self.current_managed_prefix_name)) self.prefix_winecfg_button.setToolTip( @@ -1914,6 +1916,7 @@ class WineHelperGUI(QMainWindow): management_layout.addWidget(self.prefix_winecfg_button, 0, 1) self.prefix_regedit_button = QPushButton("Редактор реестра") + self.prefix_regedit_button.setMinimumHeight(32) self.prefix_regedit_button.clicked.connect( lambda: self._run_wine_util('regedit', prefix_name=self.current_managed_prefix_name)) self.prefix_regedit_button.setToolTip( @@ -1921,6 +1924,7 @@ class WineHelperGUI(QMainWindow): management_layout.addWidget(self.prefix_regedit_button, 1, 0) self.prefix_uninstaller_button = QPushButton("Удаление программ") + self.prefix_uninstaller_button.setMinimumHeight(32) self.prefix_uninstaller_button.clicked.connect( lambda: self._run_wine_util('uninstaller', prefix_name=self.current_managed_prefix_name)) self.prefix_uninstaller_button.setToolTip( @@ -1928,11 +1932,13 @@ class WineHelperGUI(QMainWindow): management_layout.addWidget(self.prefix_uninstaller_button, 1, 1) self.prefix_cmd_button = QPushButton("Командная строка") + self.prefix_cmd_button.setMinimumHeight(32) self.prefix_cmd_button.clicked.connect(lambda: self._run_wine_util('cmd', prefix_name=self.current_managed_prefix_name)) self.prefix_cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") management_layout.addWidget(self.prefix_cmd_button, 2, 0) self.prefix_winefile_button = QPushButton("Файловый менеджер") + self.prefix_winefile_button.setMinimumHeight(32) self.prefix_winefile_button.clicked.connect( lambda: self._run_wine_util('winefile', prefix_name=self.current_managed_prefix_name)) self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") @@ -1948,6 +1954,34 @@ class WineHelperGUI(QMainWindow): 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, 3, 0, 1, 3) + + install_group = QWidget() + install_layout = QVBoxLayout(install_group) + install_layout.setContentsMargins(0, 5, 0, 0) + install_layout.setSpacing(5) + + install_path_layout = QHBoxLayout() + self.prefix_install_path_edit = QLineEdit() + self.prefix_install_path_edit.setPlaceholderText("Путь к .exe или .msi файлу...") + install_path_layout.addWidget(self.prefix_install_path_edit) + + self.prefix_browse_button = QPushButton("Обзор...") + self.prefix_browse_button.clicked.connect(self.browse_for_prefix_installer) + install_path_layout.addWidget(self.prefix_browse_button) + install_layout.addLayout(install_path_layout) + + 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) + + management_layout.addWidget(install_group, 4, 0, 1, 3) + container_layout.addWidget(self.prefix_management_groupbox) layout.addWidget(self.management_container_groupbox) layout.addStretch() @@ -1957,6 +1991,7 @@ class WineHelperGUI(QMainWindow): self.prefix_name_edit.textChanged.connect(self.update_create_prefix_button_state) self.prefix_name_edit.textChanged.connect(self.on_prefix_name_edited) 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 _load_state(self): """Загружает последнее состояние GUI из файла.""" @@ -2072,6 +2107,8 @@ class WineHelperGUI(QMainWindow): else: self.prefix_management_groupbox.setEnabled(False) self.prefix_info_display.clear() + self.prefix_install_path_edit.clear() + self.update_prefix_install_button_state() def update_prefix_info_display(self, prefix_name): """Обновляет информационный блок для созданного префикса.""" @@ -2088,6 +2125,70 @@ class WineHelperGUI(QMainWindow): Путь: {html.escape(info['path'])}

""" self.prefix_info_display.setHtml(html_content) + def browse_for_prefix_installer(self): + """Открывает диалог выбора файла для установки в созданный префикс.""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "Выберите установочный файл", + os.path.expanduser("~"), + "Исполняемые файлы (*.exe *.msi);;Все файлы (*)" + ) + if file_path: + self.prefix_install_path_edit.setText(file_path) + + def update_prefix_install_button_state(self): + """Обновляет состояние кнопки установки в префикс.""" + path_ok = bool(self.prefix_install_path_edit.text().strip()) + prefix_selected = self.current_managed_prefix_name is not None + self.prefix_install_button.setEnabled(path_ok and prefix_selected) + + def run_prefix_installer(self): + """Запускает установку файла в выбранный префикс.""" + prefix_name = self.current_managed_prefix_name + installer_path = self.prefix_install_path_edit.text().strip() + + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Не выбран префикс для установки.") + return + if not installer_path or not os.path.isfile(installer_path): + QMessageBox.warning(self, "Ошибка", "Указан неверный путь к установочному файлу.") + return + + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + wine_executable = self._get_wine_executable_for_prefix(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) + + 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_prefix_install_finished) + + env = QProcessEnvironment.systemEnvironment() + env.insert("WINEPREFIX", prefix_path) + self.command_process.setProcessEnvironment(env) + + args = [installer_path] + self.command_log_output.append(f"Запуск установки: {shlex.quote(wine_executable)} {shlex.quote(installer_path)}") + self.command_process.start(wine_executable, args) + self.command_dialog.exec_() + def create_help_tab(self): """Создает вкладку 'Справка' с подвкладками""" help_tab = QWidget() @@ -2390,6 +2491,20 @@ class WineHelperGUI(QMainWindow): self.installed_scroll_layout.addWidget(frame, row, column) self.installed_buttons.append(btn) + def _handle_prefix_install_finished(self, exit_code, exit_status): + """Обрабатывает завершение установки в префикс.""" + if exit_code == 0: + self.command_log_output.append("\n=== Установка успешно завершена ===") + else: + self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===") + + if self.command_process: + self.command_process.deleteLater() + self.command_process = None + self.command_close_button.setEnabled(True) + self.prefix_install_path_edit.clear() + self.update_installed_apps() + def _set_active_button(self, button_widget): """Устанавливает активную кнопку и обновляет стили ее обертки (QFrame).""" # Сброс стиля предыдущей активной кнопки @@ -2626,6 +2741,33 @@ class WineHelperGUI(QMainWindow): "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") return + def _get_wine_executable_for_prefix(self, prefix_name): + """Определяет и возвращает путь к исполняемому файлу wine для указанного префикса.""" + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + last_conf_path = os.path.join(prefix_path, "last.conf") + wh_wine_use = None + + if os.path.exists(last_conf_path): + try: + with open(last_conf_path, 'r', encoding='utf-8') as f: + for line in f: + if 'WH_WINE_USE=' in line: + value = line.split('=', 1)[1].strip().strip('"\'') + if value: + wh_wine_use = value + break + except Exception as e: + print(f"Предупреждение: не удалось прочитать или обработать {last_conf_path}: {e}") + + if wh_wine_use and not wh_wine_use.startswith('system'): + local_wine_path = os.path.join(Var.USER_WORK_PATH, "dist", wh_wine_use, "bin", "wine") + if os.path.exists(local_wine_path): + return local_wine_path + QMessageBox.warning(self, "Предупреждение", + f"Локальная версия Wine '{wh_wine_use}' не найдена по пути:\n{local_wine_path}\n\n" + "Будет использована системная версия Wine.") + return 'wine' # По умолчанию системный wine + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) if not os.path.isdir(prefix_path): QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") @@ -2665,33 +2807,7 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") return - # --- Определение используемой версии Wine --- - wine_executable = 'wine' # По умолчанию системный wine - last_conf_path = os.path.join(prefix_path, "last.conf") - wh_wine_use = None - - if os.path.exists(last_conf_path): - try: - with open(last_conf_path, 'r', encoding='utf-8') as f: - for line in f: - if 'WH_WINE_USE=' in line: - # Извлекаем значение из строки вида: export WH_WINE_USE="wine_ver" - value = line.split('=', 1)[1].strip().strip('"\'') - if value: - wh_wine_use = value - break - except Exception as e: - print(f"Предупреждение: не удалось прочитать или обработать {last_conf_path}: {e}") - - if wh_wine_use and not wh_wine_use.startswith('system'): - local_wine_path = os.path.join(Var.USER_WORK_PATH, "dist", wh_wine_use, "bin", "wine") - if os.path.exists(local_wine_path): - wine_executable = local_wine_path - else: - QMessageBox.warning(self, "Предупреждение", - f"Локальная версия Wine '{wh_wine_use}' не найдена по пути:\n{local_wine_path}\n\n" - "Будет использована системная версия Wine.") - # --- Конец определения версии Wine --- + wine_executable = self._get_wine_executable_for_prefix(prefix_name) env = os.environ.copy() env["WINEPREFIX"] = prefix_path From d73c0a47ab93bef8aaf6f8aed0f5dcbfa3234edc Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Tue, 26 Aug 2025 10:43:11 +0600 Subject: [PATCH 16/20] code block removed from _handle_prefix_creation_output method --- winehelper_gui.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 81056b5..7fcdbc8 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -3169,6 +3169,7 @@ class WineHelperGUI(QMainWindow): self.install_dialog.setWindowTitle(f"Установка «{title_name}»") self.install_dialog.setMinimumSize(750, 400) self.install_dialog.setWindowModality(Qt.WindowModal) + self.install_dialog.setAttribute(Qt.WA_DeleteOnClose) # Удалять диалог при закрытии log_layout = QVBoxLayout(self.install_dialog) self.log_output = QTextEdit() @@ -3405,6 +3406,7 @@ class WineHelperGUI(QMainWindow): self.btn_close.setEnabled(True) # Кнопка прервать self.btn_abort.setEnabled(False) + self.install_process = None def _handle_prefix_creation_output(self): """Обрабатывает вывод процесса создания префикса, корректно отображая прогресс.""" @@ -3427,12 +3429,6 @@ class WineHelperGUI(QMainWindow): self.command_output_buffer = self.command_output_buffer[split_idx + 1:] self._process_command_log_line(line) - # Процесс завершен, можно запланировать его удаление и очистить ссылку, - # чтобы избежать утечек и висячих ссылок. - if self.install_process: - self.install_process.deleteLater() - self.install_process = None - def handle_install_dialog_close(self, event): """Обрабатывает событие закрытия диалога установки.""" # Проверяем, запущен ли еще процесс установки From c5db176ca4f60234d1b751d86c87a9ecfcf8607d Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Tue, 26 Aug 2025 11:16:50 +0600 Subject: [PATCH 17/20] the component manager has been restored --- winehelper_gui.py | 81 +++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 7fcdbc8..1d3dda4 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -837,13 +837,8 @@ class WinetricksManagerDialog(QDialog): # 1. Проверяем, была ли отмена пользователем if self.user_cancelled: self._log("\n=== Установка прервана пользователем. ===") - self._show_message_box("Отмена", "Установки компонентов прервана пользователем.", - QMessageBox.Warning, {"buttons": {"Да": QMessageBox.AcceptRole}}) - - # Сбрасываем флаг и восстанавливаем UI - self.user_cancelled = False - self.apply_button.setEnabled(True) - self.close_button.setEnabled(True) + # Процесс завершен, теперь можно безопасно закрыть диалог. + self.close() return # 2. Обрабатываем реальную ошибку @@ -889,8 +884,10 @@ class WinetricksManagerDialog(QDialog): {"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}) if reply == "Да": self.user_cancelled = True + self.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===") self.apply_process.terminate() # Попытка мягкого завершения - event.accept() # Разрешаем закрытие + # Запрещаем закрытие. on_apply_finished обработает его после завершения процесса. + event.ignore() else: event.ignore() # Запрещаем закрытие else: @@ -2741,6 +2738,30 @@ class WineHelperGUI(QMainWindow): "Не удалось определить префикс. Выберите установленное приложение или создайте новый префикс.") return + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + if not os.path.isdir(prefix_path): + QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") + return + + winehelper_dir = os.path.dirname(self.winehelper_path) + winetricks_path = None + try: + # Ищем файл, который начинается с 'winetricks_' + for filename in os.listdir(winehelper_dir): + if filename.startswith("winetricks_"): + winetricks_path = os.path.join(winehelper_dir, filename) + break # Нашли, выходим из цикла + except OSError as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось прочитать директорию {winehelper_dir}: {e}") + return + + if not winetricks_path: + QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}") + return + + dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) + dialog.exec_() + def _get_wine_executable_for_prefix(self, prefix_name): """Определяет и возвращает путь к исполняемому файлу wine для указанного префикса.""" prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) @@ -2768,30 +2789,6 @@ class WineHelperGUI(QMainWindow): "Будет использована системная версия Wine.") return 'wine' # По умолчанию системный wine - prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) - if not os.path.isdir(prefix_path): - QMessageBox.critical(self, "Ошибка", f"Каталог префикса не найден:\n{prefix_path}") - return - - winehelper_dir = os.path.dirname(self.winehelper_path) - winetricks_path = None - try: - # Ищем файл, который начинается с 'winetricks_' - for filename in os.listdir(winehelper_dir): - if filename.startswith("winetricks_"): - winetricks_path = os.path.join(winehelper_dir, filename) - break # Нашли, выходим из цикла - except OSError as e: - QMessageBox.critical(self, "Ошибка", f"Не удалось прочитать директорию {winehelper_dir}: {e}") - return - - if not winetricks_path: - QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}") - return - - dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) - dialog.exec_() - def _run_wine_util(self, util_name, prefix_name=None): """Запускает стандартную утилиту Wine для выбранного префикса.""" if not prefix_name: @@ -3163,6 +3160,8 @@ class WineHelperGUI(QMainWindow): if not self._show_license_agreement_dialog(): return # Пользователь отклонил лицензию + self.installation_cancelled = False + # Создаем диалоговое окно установки self.install_dialog = QDialog(self) title_name = self._get_current_app_title() @@ -3389,6 +3388,14 @@ class WineHelperGUI(QMainWindow): cursor.insertText("\n") self._reset_log_state() + + if hasattr(self, 'installation_cancelled') and self.installation_cancelled: + self.append_log("\n=== Установка была прервана. ===") + self.cleanup_process() + if self.install_dialog: + self.install_dialog.close() + return + if exit_code == 0 and exit_status == QProcess.NormalExit: self.append_log("\n=== Установка успешно завершена ===") # Создаем кастомный диалог, чтобы кнопка была на русском @@ -3447,11 +3454,10 @@ class WineHelperGUI(QMainWindow): msg_box.exec_() if msg_box.clickedButton() == yes_button: - self.append_log("\n=== Пользователь прервал установку через закрытие окна ===", is_error=True) - # Завершаем процесс. Сигнал finished вызовет handle_process_finished, - # который обновит состояние кнопок. + self.append_log("\n=== Пользователь прервал установку через закрытие окна. Ожидание... ===", is_error=True) + self.installation_cancelled = True self.install_process.terminate() - event.accept() # Разрешаем закрытие окна + event.ignore() # Запрещаем закрытие, handle_process_finished обработает его else: # Пользователь нажал "Нет", поэтому игнорируем событие закрытия event.ignore() @@ -3474,7 +3480,8 @@ class WineHelperGUI(QMainWindow): msg_box.exec_() if msg_box.clickedButton() == yes_button: self.append_log("\n=== Пользователь прервал установку ===", is_error=True) - self.cleanup_process() + self.installation_cancelled = True + self.install_process.terminate() def _handle_command_output(self): """Обрабатывает вывод для диалога команды""" From b4d663f2a7f608cabf08cc781c23c1fee966cf5d Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Tue, 26 Aug 2025 11:42:10 +0600 Subject: [PATCH 18/20] added a more precise definition of the wine version for the prefix being created --- winehelper_gui.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/winehelper_gui.py b/winehelper_gui.py index 1d3dda4..c9b5af6 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -424,10 +424,11 @@ class WinetricksManagerDialog(QDialog): "Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»." ) - def __init__(self, prefix_path, winetricks_path, parent=None): + def __init__(self, prefix_path, winetricks_path, parent=None, wine_executable=None): super().__init__(parent) self.prefix_path = prefix_path self.winetricks_path = winetricks_path + self.wine_executable = wine_executable or 'wine' self.initial_states = {} self.apply_process = None self.installation_finished = False @@ -587,6 +588,7 @@ class WinetricksManagerDialog(QDialog): env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) + env.insert("WINE", self.wine_executable) # Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'. # Это известная проблема при запуске winetricks из ГУИ. process.setProcessEnvironment(env) @@ -827,6 +829,7 @@ class WinetricksManagerDialog(QDialog): self.apply_process.setProcessChannelMode(QProcess.MergedChannels) env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) + env.insert("WINE", self.wine_executable) self.apply_process.setProcessEnvironment(env) self.apply_process.readyReadStandardOutput.connect(lambda: self.log_output.append(self.apply_process.readAllStandardOutput().data().decode('utf-8', 'ignore').strip())) self.apply_process.finished.connect(self.on_apply_finished) @@ -2759,7 +2762,8 @@ class WineHelperGUI(QMainWindow): QMessageBox.critical(self, "Ошибка", f"Скрипт winetricks не найден в директории:\n{winehelper_dir}") return - dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self) + wine_executable = self._get_wine_executable_for_prefix(prefix_name) + dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self, wine_executable=wine_executable) dialog.exec_() def _get_wine_executable_for_prefix(self, prefix_name): From 662a62267c38dbaed47ef49df3307e4e0dd5e008 Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Thu, 28 Aug 2025 21:15:39 +0600 Subject: [PATCH 19/20] changed the download and selection of wine/proton versions on the cloud --- sha256sum.list | 137 +++++++++++++++++++++++- winehelper | 260 +++++++++++----------------------------------- winehelper_gui.py | 100 ++++++++++-------- 3 files changed, 252 insertions(+), 245 deletions(-) diff --git a/sha256sum.list b/sha256sum.list index 31c3b67..2d44485 100755 --- a/sha256sum.list +++ b/sha256sum.list @@ -1,5 +1,4 @@ ##### WINE ##### - 009c95bfe2df3f9264c9c5092f3e30ea7a168dd7869046058a718a70739602d4 wine_wh_tflex_10-9_amd64.tar.xz 3571c40a787f0386e0160b5d471b1bf11fa2a235b497156cd38198b90bcb4a9f wine_x_tkg_10-0_i586.tar.xz 71d6fdfd23d7988471d345c68e81699b8af931352b03dd216424b398f63c7b12 wine_x_tkg_10-0_amd64.tar.xz @@ -9,13 +8,144 @@ e0a84bb4908c3927954d7eef6b8ac7212e442b8c107d000c6890fec340f96183 wine-9.0.14-al 61bec1230b37b8fcc69fd45f848b44fd88cc41fcdd5dc3080336d7da63660f40 wine-7.16.1-alt1-amd64.tar.xz 6fea17fd131f57c2ebf7ca4c60d3c5a9e819afe16e5d0b77ecb750da99ae0e38 wine-7.16.1-alt1-i586.tar.xz -##### DXVK ##### +##### WINE_LG ##### +765e52484f06169909b179f4884d04ae4b440ff868e4b5722e64fc7d7a036ad9 WINE_LG_10-10-1.tar.xz +7a5128fcebbeed652ba36c5135821fd2c82493e87d715e8bc63e3210ba7ee849 WINE_LG_10-11.tar.xz +373beeb85c1c8163e752a32a453dcf575dd7ecf971726cecb3195e330d094925 WINE_LG_10-12.tar.xz +92f3c5a47a27644fde3ec1da810afcd5e6c9c5c2bc9c1f5f76a82b55894e745b WINE_LG_10-1.tar.xz +ef8a71c269f5a2a7012aec9c16f307ab49ed09fa85a91b42d6c6b954d40401a6 WINE_LG_10-5.tar.xz +7aca95bd921a102e0a4464f4a8945095e05587bffbb924fe1603d2deb8283ba5 WINE_LG_10-7.tar.xz +503a86e8b9446a4805da524ba5ec2bde591787b354df098135d65f2ed9d66dca WINE_LG_8-18-1.tar.xz +2714260a7f1f7d6427736aad2093dc17c1c10df9641b552e5ac4ac3fc73308ee WINE_LG_9-10.tar.xz +3c2dbb33da9eaa7f3afcbe366293f6abf601257fb782a06040710ec6f1def1ec WINE_LG_9-11.tar.xz +da79f89b1fc3175f113bf5ec35fc2755550159034736cff086f2165480dd6e0f WINE_LG_9-12-2.tar.xz +34a31802e50d6d70e1f4d23520c2d0047ae70d0cb4beb3e04046ee781e136a68 WINE_LG_9-12.tar.xz +6d6fd31ff71fd43f17182b466b6d499b61652775b7dbf25a18c3eaab7eacea2d WINE_LG_9-2.tar.xz +3f0cda10a1019c6a18d832dc547709e612d88297ce332f0835c2bffeae9215c3 WINE_LG_9-5.tar.xz +51ff56e435a90eeff13ffbbac543a820d1df14d65f2e3b760ad100bc946e4bfc WINE_LG_9-7.tar.xz +09901b17a7aaa13e5c1a1f99ab82c9f0d223aa2f5c9ac94938e5a9a0088bb244 WINE_LG_9-9.tar.xz +##### PROTON_LG ##### +d0f5b42096bc5ed379a26e415d99963f849129d5cec1ba9134b1ee50100b049b PROTON_LG_10-10.tar.xz +d87eb914ce7e5cab47e6b480aa400f8c7840f39a22d585bf35092a033ac09201 PROTON_LG_10-13.tar.xz +05ca56607af7cee30b4797ad6c6a111d38a52c9d5c2184d30eec2650f1585b58 PROTON_LG_10-15.tar.xz +f8bfac3029bd79e211a6dc592c0959db239fb7807c410ed2a537c34467875c74 PROTON_LG_10-4.tar.xz +83d3cf60bbae6ca9d0508c2fdeaf3887bb72830eb52afc858d4afa009d0faa78 PROTON_LG_10-8.tar.xz +f70039931d644898bb0a44daa7e3d44fcafa9dca0881e2277b1062ecd2a5357a PROTON_LG_8-25-2.tar.xz +3e9202caf11d517406f82209e9e5385a4b0b1e4e16afa8b6f4fd517401537224 PROTON_LG_9-11.tar.xz +0481da38de07c71074c245624404994af164c9a6a584942c6d1f12966db2311a PROTON_LG_9-12.tar.xz +786d6415bb884d0ec80617e02004558634ac6bd99474cd63c25e2e5fd339b112 PROTON_LG_9-15.tar.xz +8f05ee518e1861a79b1830301c57a8d4422b6a9c24dfeec07fde474d283d4152 PROTON_LG_9-16.tar.xz +8bd409e4c56472429042555c1b2102bcd8313a4d40a80fe17a2caac8f4c8d4ed PROTON_LG_9-18.tar.xz +e88e7b076ad1d85d625407468515d6f41dcb20b721a6bd699bcd8818647d136c PROTON_LG_9-19.tar.xz +bd5dfe3a5187eb3583e5bc2fbc7debe40760802539c1317211415edeeeae851e PROTON_LG_9-27.tar.xz +63fde3b768a60cf736dcf3f9922cc2ab606cb7007e4cbe59f10cd038d3bdbee8 PROTON_LG_9-2.tar.xz +e09c5da9ddffc5d390de7689df1778f279cc84a946665cc76ce77861010b2604 PROTON_LG_9-4.tar.xz +03ccc15aa4adcaa49f3eeb52bf2a9127bacd8d578478f716b390e812dab74e21 PROTON_LG_9-5-1.tar.xz +56eae794a48aa7322ce5a636c490974d7f2516528d3153991fb033b0fa5fe3cf PROTON_LG_9-5.tar.xz +82263e1d0ab16130f15d419f2661fc5a7baf29193b9eeeb7eec3f01a3e54a1b3 PROTON_LG_9-7.tar.xz + +##### PROTON_STEAM ##### +09e6e386a87710996d634e08650bbc733365df65d3491a4372f06cf4e815a8b2 PROTON_STEAM_6.3-8.tar.xz +2f8bdfeb75e5427886aac7d2a599a1ee8900e50d5873a0e620d5703707bf4271 PROTON_STEAM_7.0-5.tar.xz +b33aca95664067c9eff06a2b4380992d43a5d31880ff970fb340e851671be625 PROTON_STEAM_8.0-4.tar.xz +9a20af28f4213e4f1c532b79a20e6a95ef872130784fc306510637342c6f58a1 PROTON_STEAM_8.0-5.tar.xz + +##### DXVK ##### +d1a86ca53ab7e156e1252d3cf7eed3c66fe0651699cfc9c4ba152024261d8258 dxvk-1.10.1-967.tar.xz 98411effbc1b3611b11c4ca3af2ad29400b08e1c8c380791d6351600a06fb3c7 dxvk-1.10.3-28.tar.xz +061568b51ce99d285ef2d0f1f31aab65b3e6fd34747073859c5a719bec2a7afd dxvk-1.6.1.tar.xz +37ed4a8cd4c2bff3abbf830e6d602c2e0a971eda7c69b3f45834539bc84b1431 dxvk-2.0-116.tar.xz +50269fc2e9a27ec15865ec370e48934d5036774a8da180deb599f481ef6d11c7 dxvk-2.0-26.tar.xz +a218072e6e750406b19602407201eb0e3cee80015d3b5b569373a830ac428c9e dxvk-2.0-34.tar.xz +3ea6e464339be82932d665331d6219021ea7e6f8a1b94972b89feeac0d79aa03 dxvk-2.0-36.tar.xz +526f92560c5a4b1aec81d243641315a8bd8d9d5de8e6b69cd92f4fe5f47cbd79 dxvk-2.1-48.tar.xz +ef7ef69e8c6e3036216cb1df4af33024995650bf956609a512d0e9644fb7c911 dxvk-2.1-51.tar.xz +7ebfc554ac0adeff2aea9c261eca029a921cce1e63bbc8989515f3f132c19130 dxvk-2.1-79.tar.xz +89ddc7f18c5b3066532a7a1b7fceaf0733406c0c03b0cd1d6b37287037670f0b dxvk-2.1-85.tar.xz +bb711363456acf7a5f4cad88e939ad0a9dd92b030ad4568d24a1c083cd9f31b1 dxvk-2.1.tar.xz +7b5d32da81897499b16fd0d630fe6e8faaa764f9671d992af77a380786bb78ff dxvk-2.2-116.tar.xz +e242eff8b1856d357ae8c9233bfd6e13a3a85785dc648bd8d68967732593c4aa dxvk-2.2-137.tar.xz +ff66e31df8d62a5b666c33548ecb98322366df1b9a080e9d3463e08861f08458 dxvk-2.2-164.tar.xz +f1bbfff9aeb4dd0ee55343cdafabf4bfc1d920da3a4c514a99cc1759055185cb dxvk-2.2-1.tar.xz +b71105c70f64664c772a44565b525ae2e3c7a74765b8ef9852db300d6de3b548 dxvk-2.2-34.tar.xz +f0ea1469d8e1e10642e2b12830ade0050d7976930db6dd6a38b83a989caab992 dxvk-2.2-7.tar.xz +50feeffdef071414c82b6ead88e70403685a9c0f482e58c702e97fa1f7bf79ed dxvk-2.3.1-120.tar.xz +74c3e14b89d28d768197f29f1893c08e36336749eeca7f34efdafd38f3f3b0f3 dxvk-2.3.1-37.tar.xz +6c0485f0bd11159cda59f6ec3074918d9b86053833fc3e76af151ad7d56cc216 dxvk-2.3-13.tar.xz +85729d1dfa06c7b569b5e3f0dd834f20902395cdb065419f40de1f3c990f17a9 dxvk-2.3.1-72.tar.xz +f0f999189431c227db08234be41823004cb86e3c85bb4134af62be56ebf764f5 dxvk-2.3.1-9.tar.xz +a9cb76f8889afc133838b00c2cec153f7028df24cf56e2e15f995f14b074af9b dxvk-2.3.1.tar.xz +37ad50ffce2e886a26e5c6557d4acc025971e6c8d04e45c8a39718a6a446984f dxvk-2.3-26.tar.xz +38fde7df801be0c66c454cd1b4001c914d008d7e29886e1526f00dccde4ffad4 dxvk-2.3-57.tar.xz +2054dc956ea98a54c42bc9659270bebc0a8410293819fb862ede3481c41b2864 dxvk-2.3-5.tar.xz +e0dfc268e5c3eeff830c71774070a079572ebb57d709ef11a7bc8a35baad06fd dxvk-2.3-90.tar.xz +76f1b50ae6defc421f593826f192c8f4c8298ffc7b95abbe19f19ac86acebedc dxvk-2.4.1-194.tar.xz +6c04b694e58b9da5c0a15acab3da1a1c3627674bf6612d009d730af8ffc4fb6d dxvk-2.4.1-393.tar.xz +622e40cb20da30cf2771b4a1f30d898e231e39e4d8968f4a8052ae51086f59e7 dxvk-2.4.1-446.tar.xz +47a153abbeb38f9c679c171eca80558bd8744311e642c7f2469fe10fcd9a5db6 dxvk-2.4.1-74.tar.xz +89f2d4a26a7337cc697345e96023d612401717f8a71fa7c9c59835ef2d5c5a40 dxvk-2.4-37.tar.xz +49a29d069a9acf73967c69a4a873cb9d023b657717d3377405e23f4730314be0 dxvk-2.4-41.tar.xz +9b52cfa2b1b27b7fc2e4142c7bba1d93309aca548c7a3e4fac362b6375a587ae dxvk-2.4-94.tar.xz +969b9a720fecf9c53b266c6e289e896e6ed7c49fee260424ab7bc00abbab2d41 dxvk-2.5.1-35.tar.xz +b499be012ffa0b5e3079c9f475c79a749e8f8066924f68919aa898d06a8ba33d dxvk-2.5.2-1.tar.xz +69f8b86ed92cab495f187840aa144691859631d304d2b1880e27de2e49c54244 dxvk-2.5.3-299.tar.xz +00f016b9bd8b2bb1165952edec3ad0da404b525b1b9eec5f4aae490944d47f82 dxvk-2.5.3-31.tar.xz +94c569b205d22e742dd165741a88f77a7a9c6feebd924097fbfd6f169aaa303b dxvk-2.6.1-255.tar.xz +a8a65fe53e054e9f9da89dc848dba689a67be7daba7e59dc82b3be7190925354 dxvk-2.6.1-368.tar.xz +b4c7cbb6c09fdc2c42104ac9e4377167881d3f9d97b2ff125f36f1c351255ea5 dxvk-2.6.2.tar.xz +eec7c6ed63789ebafeda16895678feb8d561ed8117b6ecdb4ab76c0beb3ae17c dxvk-2.6-65.tar.xz +4ac30b8ed56a790c51c6c136c14cd61b8fec9c40fe5fc6ef6d8f042ee155c850 dxvk-2.7-44.tar.xz +8475410048889b9bbef2cf082fc15c67a6610f9dcfef6948b56bd956999bfb88 dxvk-2.7-46.tar.xz +c02b565d2fca1dc4066fb58acfcdd3919386fdbb01b30cfe181d4dd02ac5f1bb dxvk-2.7-5.tar.xz +5f97deb1eeb97ed41d0539264c0ca98e8841f79ec59684f32e7f1ca5c29a109b dxvk-sarek-1.11.0.tar.xz ##### VKD3D ##### - df1a940d2e072a884524f66a16e0f0ef74048bc6a6642b96eb257528a522109f vkd3d-proton-1.1-2602.tar.xz +bc86b06af83054e25bad21fe4bec4c10538837221fe847470571df7d556d355b vkd3d-proton-1.1-2967.tar.xz +41bd2465015f069ef2d378e42a0f906fd40f1ce70e1602b36e445209b16e0d50 vkd3d-proton-1.1-3088.tar.xz +6d9eb9053fff0f8ed1760246f889d827dca6fbc48d508997a9b1a5b77a399991 vkd3d-proton-1.1-3090.tar.xz +e92444e85cf59a65b12f14135a5a3ffd51d204bd84b0dfe4bd079fc3d202ec0c vkd3d-proton-1.1-3094.tar.xz +3a88b84fae6c1425a17a9b0335f04a1e2ae414a31a8225fd0512c2bab62191fd vkd3d-proton-1.1-3115.tar.xz +231b501433612598c5b85f16914a7415765b7cf4ec1debc921484efafd68e66b vkd3d-proton-1.1-3132.tar.xz +8786fbde85181036932efa00051f73f6c66f620f97ef0ae3cc795239a817f667 vkd3d-proton-1.1-3325.tar.xz +3724c066273773ea3913a2a34c239465a17b1d04ab08ca8583c8abe97baa2e45 vkd3d-proton-1.1-3339.tar.xz +5b25fb04db003ebb6dbcb4566b9c4471e9b6e36d7271a8bd0b35835debc81fb8 vkd3d-proton-1.1-3410.tar.xz +7af94939575b83dabd9440af940e6bb0b639dbba86d96d5cf53c9a656a4ab28a vkd3d-proton-1.1-3413.tar.xz +854ea12d5441ae7bec27f74e91851eaeab980cf3cc3a58341ccaa3956ee61851 vkd3d-proton-1.1-3414.tar.xz +919a533ee67fc869832a3dad71ef7afde4461d166f251fe1aff879bab61e50ec vkd3d-proton-1.1-3424.tar.xz +cd1a0b923a8d5a41d0f7e875c1706985c5add63b10ec17c73b15a180563cd4c6 vkd3d-proton-1.1-3445.tar.xz +573ff59ed58d9f304332441deb62c10b789512ef818f39e86954a8d7a21ded98 vkd3d-proton-1.1-3488.tar.xz +6a3edc710f9a5ab3a23bc7138a2fdde80a7ce8cc92cb4ecb4ea4e194f5753507 vkd3d-proton-1.1-3516.tar.xz +ab90f2fd8272cb8b4a026291cccd2e336184c149c12cfe54b8a4eaed24841ce0 vkd3d-proton-1.1-3556.tar.xz +8ac31cb696e98579b95e7a6ad62f332d3faa8d72db62edb8bb6ba6110bd9def9 vkd3d-proton-1.1-3622.tar.xz +de5b1bd60505118b440c85a674cdf25a56cdea0de98b6bb41079de1bf1e8d5c0 vkd3d-proton-1.1-3727.tar.xz +e40617aa79940725d31256f39e095f96f49edd84a7a63b4f830a28d69b4f0db0 vkd3d-proton-1.1-3821.tar.xz +24aecefd3b5c4dc56794d39356baa7c0e1ad99fba4e32bb577b3716b8fb1a065 vkd3d-proton-1.1-3908.tar.xz +014aec87dfd14424f5bc3e8fc8d9704a677974e00b54b4ce5cc74042b26de5a0 vkd3d-proton-1.1-3954.tar.xz +fde723756ab563ef11b77842886b3bc511fbfb639fcbee3da67883389a6e5384 vkd3d-proton-1.1-3973.tar.xz +f4b37156792dc14768d1d161db220782d89065eb995c881efbad3de454782123 vkd3d-proton-1.1-3980.tar.xz +c74d9d5958e829732833299a5b37a9d1b77d2fdf9d1991608be515cd46e5f1e8 vkd3d-proton-1.1-4022.tar.xz +a90b829d773c740e51b400aafd8f46ed98f9b43fa26080d1cf52d6abc1f3963a vkd3d-proton-1.1-4051.tar.xz +b780ce056c7fe3868e1c0fac9c5efd30bbbbe73515a313315f5e1593f9bcdcb4 vkd3d-proton-1.1-4110.tar.xz +82134ae8ab79d6b884eb4461db2efd747cfb013a80019e3223def454eec8d631 vkd3d-proton-1.1-4167.tar.xz +0ffd513ef29e048fdfc0527b0894fd74e640be07c520b435a1089bdbcf0d780c vkd3d-proton-1.1-4177.tar.xz +aa0a8cf76a47a5967d5d5c4803d81715c04b609776ebf8b84c1c627f994f26f6 vkd3d-proton-1.1-4191.tar.xz +fdb384c3c3b8fdf2bc55bf68895ff0c514bfef1cefa9d0047d32c9f691fe8ada vkd3d-proton-1.1-4202.tar.xz +510447a2ab92df48124f9b88c97e0713f858ade48273aabc8393cabac7b1b1a4 vkd3d-proton-1.1-4240.tar.xz +1117f775efe00a4d14cb09ee77afe220f88a1fb0ec406ef4f004c17f9049a617 vkd3d-proton-1.1-4252.tar.xz +04288493c9a7671688c19fd304dcb4bcb4e05b6ceb7bd9b3fa57c1d3cb196769 vkd3d-proton-1.1-4271.tar.xz +8b509e904f1e75507eab4ee5ac42c6f5a45593eea9cef5c920175910756cbc47 vkd3d-proton-1.1-4325.tar.xz +1cc20f8c375fda6a9388d01fb9454eb634daf171610e62173daa976d350be002 vkd3d-proton-1.1-4326.tar.xz +90fea8a749e8df3981c8ef506c0ddaeb7a9ce92d7c7a9860f71f02bd5bfe443d vkd3d-proton-1.1-4367.tar.xz +aa2f98f4de0f8a550f0c7d659704b75ec2a77434e93e9b6e75faad9ecc998614 vkd3d-proton-1.1-4478.tar.xz +5d3e9e3a209477bfc41165c628fab3098aa44eddc4410744ecc486672c639906 vkd3d-proton-1.1-4511.tar.xz +3749e0a77283ecd038312b760b49790fc5956215d61f2781572618ebbcedb55d vkd3d-proton-1.1-4591.tar.xz +b3f56f58b5b764aba564c8b8c69eb12425467d57bf2a6457edff08842af03573 vkd3d-proton-1.1-4655.tar.xz +02ed90a4b760fad633146703ce35ef587ce244f7a053853f179bfb0c03ca1b7c vkd3d-proton-1.1-4666.tar.xz +240207848855b08c3412fc5ab54c043e96baa6ea874499fb2e7fe11df114d8c1 vkd3d-proton-1.1-4707.tar.xz +8170e8bd34d47c5607603da2c93737acc19f4ebedc45e02115c282dfc0c36681 vkd3d-proton-2.14.1.tar.xz +edf16d2b37bc77d121d5d81b06b60d3f694e0060c6606e729ceab30de3d27466 vkd3d-proton-sarek-2.6.0.tar.xz ##### CPCSP_PROXY ##### 3153088ff28d6af415c3504a421e5382554dbb305f38d5a87dd84df90d680421 wine-cpcsp_proxy-0.6.1-alt1.tar.xz @@ -24,7 +154,6 @@ df1a940d2e072a884524f66a16e0f0ef74048bc6a6642b96eb257528a522109f vkd3d-proton-1 405bfe3b7c7f80034837c05656535053305727ee4bf1d993521b67b71d08ebc6 extra_fonts_v01.tar.xz ##### PREFIX ##### - 0e86cd9958d1bd1bfac99e23165a091a4ea2fa3e693c551f69aabeed5f681fea defpfx_x86_v01.tar.xz # create with wine_x_tkg_10-0_amd64 (universal user: xuser and isolate_home by default) # winetricks msxml3 msxml4 msxml6 andale arial comicsans courier georgia impact times trebuchet verdana webdings corefonts wsh57 vcrun6 mdac27 jet40 gdiplus lucida tahoma ucrtbase2019 vcrun2019 diff --git a/winehelper b/winehelper index 22b3bbd..1511801 100755 --- a/winehelper +++ b/winehelper @@ -811,39 +811,11 @@ init_wine_ver () { export WINEDIR="$WH_DIST_DIR/$WH_WINE_USE" if [[ ! -d "$WINEDIR" ]] ; then - local download_url wine_package_name wine_package check_sum_arg - WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" + local download_url wine_package + download_url="$CLOUD_URL/$WH_WINE_USE.tar.xz" + wine_package="$WH_TMP_DIR/$WH_WINE_USE.tar.xz" - # Сначала пытаемся сформировать URL по старой схеме, как основной для автоустановки - local old_schema_url="$CLOUD_URL/$WH_WINE_USE.tar.xz" - - # `curl -f -s -I` делает HEAD-запрос. `-f` заставляет его вернуть код ошибки при 404. - if curl -f -s -I "$old_schema_url" > /dev/null ; then - download_url="$old_schema_url" - elif [[ -f "$WINE_METADATA_FILE" ]]; then - # Если по старому URL ничего нет, ищем в метаданных - print_info "Версия '$WH_WINE_USE' не найдена на основном сервере, ищем в метаданных..." - download_url=$(jq -r --arg name "$WH_WINE_USE" '.[] | .[] | select(.name == $name) | .url' "$WINE_METADATA_FILE" | head -n 1) - else - # Если и метаданных нет, то считаем, что должен был быть старый URL - print_warning "Файл метаданных не найден. Предполагается, что версия '$WH_WINE_USE' находится на основном сервере." - download_url="$old_schema_url" - fi - - # Если URL так и не был найден, выводим ошибку. - if [[ -z "$download_url" ]]; then - fatal "Не удалось найти URL для скачивания версии '$WH_WINE_USE'." - fi - - wine_package_name="$(basename "$download_url")" - wine_package="$WH_TMP_DIR/$wine_package_name" - - # Проверяем хэш-сумму только для файлов с нашего сервера - if [[ "$download_url" == *"$CLOUD_URL"* ]]; then - check_sum_arg="check256sum" - fi - - try_download "$download_url" "$wine_package" "$check_sum_arg" + try_download "$download_url" "$wine_package" "check256sum" unpack "$wine_package" "$WH_DIST_DIR/" try_remove_file "$wine_package" @@ -1503,178 +1475,73 @@ remove_prefix() { fi } -generate_wine_metadata () { - WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" - - if [[ -f "$WINE_METADATA_FILE" ]]; then - if find "$WINE_METADATA_FILE" -mmin -1440 | grep -q . ; then - print_info "Файл метаданных $WINE_METADATA_FILE обновлялся менее 24 часов назад. Пропускаем генерацию." - return 0 - fi - print_info "Файл метаданных $WINE_METADATA_FILE устарел." - print_info "Начинаем обновление..." - else - print_info "Генерации метаданных..." - fi - - TMP_WINE_META="$WH_TMP_DIR/wine_metadata" - local FETCH_ERROR_FLAG="$TMP_WINE_META/fetch.error" - rm -f "$FETCH_ERROR_FLAG" - - mkdir -p "$TMP_WINE_META" - - cleanup() { - rm -rf "$TMP_WINE_META" - } - trap cleanup EXIT - - fetch_github_releases() { - local repo="$1" - local wine_metadata_file="$2" - - local url="https://api.github.com/repos/$repo/releases" - - if ! curl -s --fail -H "Accept: application/vnd.github.v3+json" "$url" > "$wine_metadata_file"; then - touch "$FETCH_ERROR_FLAG" - return 1 - fi - - if ! jq -e 'type == "array"' "$wine_metadata_file" >/dev/null 2>&1; then - try_remove_file "$wine_metadata_file" - touch "$FETCH_ERROR_FLAG" - return 1 - fi - - print_info "Получение данных для $repo" - } - - create_wine_entries() { - local input_file="$1" - local file_extension="$2" - local exclude_patterns="$3" - - jq -r --arg ext "$file_extension" ' - .[] | - .assets[] | - select(.browser_download_url | test($ext)) | - { - name: (.name | gsub($ext; "")), - url: .browser_download_url - } - ' "$input_file" | \ - if [[ -n "$exclude_patterns" ]]; then - jq -c --arg patterns "$exclude_patterns" ' - select(.name | test($patterns) | not) - ' - else - jq -c '.' - fi - } - - # Формат: "ключ_json;репозиторий;расширение_файла;шаблон_исключения" - local sources=( - "proton_ge;GloriousEggroll/proton-ge-custom;\\.tar\\.gz$;github-action" - "wine_kron4ek;Kron4ek/Wine-Builds;\\.tar\\.xz$;" - "proton_lg;Castro-Fidel/wine_builds;\\.tar\\.xz$;plugins" - "proton_cachyos;CachyOS/proton-cachyos;\\.tar\\.xz$;znver" - "proton_sarek;pythonlover02/Proton-Sarek;\\.tar\\.gz$;" - "proton_em;Etaash-mathamsetty/Proton;\\.tar\\.xz$;" - ) - - for source_data in "${sources[@]}"; do - ( - IFS=';' read -r key repo extension exclude_pattern <<< "$source_data" - - local releases_file="$TMP_WINE_META/${key}_releases.json" - local entries_file="$TMP_WINE_META/${key}.json" - - fetch_github_releases "$repo" "$releases_file" || exit 1 - create_wine_entries "$releases_file" "$extension" "$exclude_pattern" > "$entries_file" - ) & - done - - wait - - if [[ -f "$FETCH_ERROR_FLAG" ]]; then - fatal "Ошибка при получении релизов. Возможно, превышен лимит запросов к API GitHub или проблема с сетью." - fi - - print_ok "Все данные получены." - - print_info "Создание итогового JSON файла..." - - jq -n \ - --slurpfile proton_ge "$TMP_WINE_META/proton_ge.json" \ - --slurpfile wine_kron4ek "$TMP_WINE_META/wine_kron4ek.json" \ - --slurpfile proton_lg "$TMP_WINE_META/proton_lg.json" \ - --slurpfile proton_cachyos "$TMP_WINE_META/proton_cachyos.json" \ - --slurpfile proton_sarek "$TMP_WINE_META/proton_sarek.json" \ - --slurpfile proton_em "$TMP_WINE_META/proton_em.json" \ - '{ - proton_ge: $proton_ge, - wine_kron4ek: $wine_kron4ek, - proton_lg: $proton_lg, - proton_cachyos: $proton_cachyos, - proton_sarek: $proton_sarek, - proton_em: $proton_em - }' > "$WINE_METADATA_FILE" - - if jq empty "$WINE_METADATA_FILE" 2>/dev/null; then - print_ok "JSON файл создан успешно: $WINE_METADATA_FILE" - else - print_error "Ошибка создания JSON файла" - exit 1 - fi -} - select_wine_version() { - if ! command -v jq &> /dev/null; then - print_warning "Команда 'jq' не найдена. Невозможно отобразить список версий WINE/Proton." - print_warning "Будет использована версия по умолчанию: $WH_WINE_USE" - return - fi + local sha256_file="$DATA_PATH/sha256sum.list" + [[ ! -f "$sha256_file" ]] && fatal "Файл с версиями WINE/Proton не найден: $sha256_file" - generate_wine_metadata - WINE_METADATA_FILE="$WH_TMP_DIR/wine_metadata.json" - [[ ! -f "$WINE_METADATA_FILE" ]] && fatal "Файл метаданных WINE не найден." - - local arch_filter_jq if [[ "$WINEARCH" == "win64" ]]; then print_info "Фильтруем версии для 64-битного префикса..." - # Для 64-битных префиксов показываем сборки с 'amd64', 'x86_64', 'wow64' - # или те, у которых нет явного указания на 32-битную архитектуру. - arch_filter_jq='select((.name | test("amd64|x86_64|wow64")) or (.name | test("i[3-6]86|x86(?!_64)") | not))' else # win32 print_info "Фильтруем версии для 32-битного префикса..." - # Для 32-битных префиксов показываем только сборки с явным указанием 32-битной архитектуры. - arch_filter_jq='select(.name | test("i[3-6]86|x86(?!_64)"))' fi local options=() local total_versions_found=0 # --- System --- + local system_wine_display_name="system" + if command -v wine &>/dev/null; then + local system_wine_version + system_wine_version=$(wine --version 2>/dev/null) + [[ -n "$system_wine_version" ]] && system_wine_display_name="$system_wine_version" + fi options+=("--- System ---") - options+=("system") + options+=("$system_wine_display_name") - # --- Other versions from JSON --- - local group_keys - mapfile -t group_keys < <(jq -r 'keys_unsorted | .[]' "$WINE_METADATA_FILE") - - for key in "${group_keys[@]}"; do - local group_versions - mapfile -t group_versions < <(jq -r --arg key "$key" '.[$key] | .[] | '"$arch_filter_jq"' | .name' "$WINE_METADATA_FILE" | sort -V -r | uniq) + # --- Other versions from sha256sum.list --- + local current_group="" + local group_versions=() + flush_group() { if [[ ${#group_versions[@]} -gt 0 ]]; then - # Prettify the group name (e.g., "proton_ge" -> "Proton Ge") - local pretty_key - pretty_key=$(echo "$key" | tr '_' ' ' | sed -e "s/\b\(.\)/\u\1/g") - - options+=("--- $pretty_key ---") - options+=("${group_versions[@]}") + IFS=$'\n' sorted_versions=($(sort -Vr <<<"${group_versions[*]}")) + unset IFS + options+=("${sorted_versions[@]}") ((total_versions_found+=${#group_versions[@]})) + group_versions=() fi - done + } + + while IFS= read -r line; do + if [[ "$line" =~ ^#+[[:space:]]([^#[:space:]]+)[[:space:]]#* ]]; then + flush_group + current_group="${BASH_REMATCH[1]}" + # Отображаем только группы, которые являются сборками WINE или PROTON + case "$current_group" in + WINE|WINE_LG|PROTON_LG|PROTON_STEAM) + local pretty_key=$(echo "$current_group" | tr '_' ' ' | sed -e "s/\b\(.\)/\u\1/g") + options+=("--- $pretty_key ---") + ;; + *) + current_group="" + ;; + esac + elif [[ -n "$current_group" ]] && [[ "$line" =~ [a-f0-9]{64} ]]; then + local filename=$(echo "$line" | awk '{print $2}') + local version_name=${filename%.tar.xz} + + if [[ "$WINEARCH" == "win64" ]]; then + if [[ "$version_name" =~ (amd64|x86_64|wow64) ]] || ! [[ "$version_name" =~ i[3-6]86 ]]; then + group_versions+=("$version_name") + fi + else # win32 + if [[ "$version_name" =~ i[3-6]86 ]]; then + group_versions+=("$version_name") + fi + fi + fi + done < "$sha256_file" + flush_group if [[ $total_versions_found -eq 0 ]]; then print_warning "Не найдено подходящих версий WINE/Proton для архитектуры $WINEARCH." @@ -1682,13 +1549,11 @@ select_wine_version() { return fi - # --- Пользовательское меню с разделением на группы и пустыми строками --- local selectable_options=("Отмена") local display_groups=() local current_group_items=() local choice_idx=0 - # Помощник для переноса текущей группы элементов в основной массив display_groups flush_current_group() { if ((${#current_group_items[@]} > 0)); then # Объединяйте элементы с помощью уникального разделителя для последующего разделения @@ -1699,11 +1564,10 @@ select_wine_version() { current_group_items+=(" 0) Отмена создания префикса") - # Обработка массива основных параметров для создания групп for opt in "${options[@]}"; do if [[ "$opt" == "---"* ]]; then flush_current_group - display_groups+=("$opt") # Добавьте заголовок как отдельный элемент + display_groups+=("$opt") else ((choice_idx++)) current_group_items+=(" ${choice_idx}) $opt") @@ -1714,17 +1578,14 @@ select_wine_version() { print_info "Выберите версию WINE/Proton для $WINEARCH префикса:" - # Показывать группы одну за другой local first_block=true for group_data in "${display_groups[@]}"; do - if [[ "$group_data" == "---"* ]]; then if [[ "$first_block" = false ]]; then echo fi echo "$group_data" else - # Это блок элементов для печати в столбцах local items_to_print=() IFS='@@@' read -r -a items_to_print <<< "$group_data" @@ -1752,15 +1613,21 @@ select_wine_version() { done while true; do + echo local max_choice=$(( ${#selectable_options[@]} - 1 )) - read -p "Введите номер (0-$max_choice): " user_choice + read -p "Введите номер для выбора wine/proton (0-$max_choice): " user_choice if [[ "$user_choice" =~ ^[0-9]+$ ]] && (( user_choice >= 0 && user_choice <= max_choice )); then if [[ "$user_choice" == "0" ]]; then print_info "Создание префикса отменено." exit 0 fi - local selected_opt="${selectable_options[$user_choice]}" - export WH_WINE_USE="$selected_opt" + local selected_opt + selected_opt="${selectable_options[$user_choice]}" + if [[ "$selected_opt" == "$system_wine_display_name" ]]; then + export WH_WINE_USE="system" + else + export WH_WINE_USE="$selected_opt" + fi break else print_error "Неверный выбор. Введите число от 0 до $max_choice." @@ -2175,7 +2042,6 @@ case "$arg1" in create-prefix) create_prefix "$@" ;; remove-prefix) remove_prefix "$@" ;; create-base-pfx) create_base_pfx "$@" ;; - generate-db) generate_wine_metadata "$@" ;; init-prefix) prepair_wine ; wait_wineserver ;; *) if [[ -f "$arg1" ]] ; then diff --git a/winehelper_gui.py b/winehelper_gui.py index c9b5af6..81a3526 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1014,11 +1014,9 @@ class ScriptParser: class WineVersionSelectionDialog(QDialog): """Диалог для выбора версии Wine/Proton с группировкой.""" - def __init__(self, architecture, winehelper_path, user_work_path, parent=None): + def __init__(self, architecture, 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.system_wine_display_name = "Системная версия" @@ -1049,10 +1047,6 @@ class WineVersionSelectionDialog(QDialog): 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) @@ -1061,52 +1055,71 @@ class WineVersionSelectionDialog(QDialog): 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.""" + QApplication.processEvents() + + self._parse_sha256_list() + self.populate_ui() + 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, "Ошибка") + def _parse_sha256_list(self): + """Парсит sha256sum.list для получения списка версий.""" + sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list") + if not os.path.exists(sha256_path): + QMessageBox.warning(self, "Ошибка", f"Файл с версиями не найден:\n{sha256_path}") + self.wine_versions_data = {} return - self.populate_ui() + self.wine_versions_data = {} + current_group = None + + try: + with open(sha256_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line: + continue + + match = re.match(r'^#+\s+([A-Z_]+)\s+#*$', line) + if match: + group_name = match.group(1) + allowed_groups = {"WINE", "WINE_LG", "PROTON_LG", "PROTON_STEAM"} + # Отображаем только группы, которые являются сборками WINE или PROTON + if group_name in allowed_groups: + current_group = group_name + if current_group not in self.wine_versions_data: + self.wine_versions_data[current_group] = [] + else: + current_group = None + continue + + if current_group and re.match(r'^[a-f0-9]{64}', line): + parts = line.split(maxsplit=1) + if len(parts) == 2: + filename = parts[1] + if filename.endswith('.tar.xz'): + version_name = filename[:-7] + self.wine_versions_data[current_group].append(version_name) + except IOError as e: + QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать файл версий:\n{e}") + self.wine_versions_data = {} def populate_ui(self): """Заполняет UI отфильтрованными версиями.""" self.version_tabs.clear() + if not self.wine_versions_data: + error_widget = QWidget() + error_layout = QVBoxLayout(error_widget) + error_label = QLabel("Список версий пуст или не удалось его загрузить.") + error_label.setAlignment(Qt.AlignCenter) + error_layout.addWidget(error_label) + self.version_tabs.addTab(error_widget, "Ошибка") + return + 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') @@ -1131,10 +1144,9 @@ class WineVersionSelectionDialog(QDialog): 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): + for name in sorted(versions, reverse=True): if is_win64: if re_64bit.search(name) or not re_32bit.search(name): filtered_versions.append(name) @@ -1802,7 +1814,7 @@ class WineHelperGUI(QMainWindow): 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) + dialog = WineVersionSelectionDialog(architecture, self) if dialog.exec_() == QDialog.Accepted and dialog.selected_version: self.wine_version_edit.setText(dialog.selected_display_text) self.selected_wine_version_value = dialog.selected_version From 504be8ea5b9b431220ff3418dd605436121df69a Mon Sep 17 00:00:00 2001 From: Sergey Palcheh Date: Fri, 29 Aug 2025 10:21:26 +0600 Subject: [PATCH 20/20] fixed typos --- winehelper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winehelper b/winehelper index 1511801..fc24379 100755 --- a/winehelper +++ b/winehelper @@ -1687,7 +1687,7 @@ create_prefix() { print_info "Выберите тип создаваемого префикса:" echo " 0) Отмена создания префикса" echo " 1) Чистый префикс (без библиотек)" - echo " 2) С рекомендуемыми библиотеками (по умолчанию)" + echo " 2) С рекомендуемыми библиотеками" echo local pfx_type_choice read -p "Ваш выбор [0-2] (по умолчанию 1): " pfx_type_choice