Compare commits

..

2 Commits

Author SHA1 Message Date
Sergey Palcheh
61fb7d9847 a typo has been fixed 2025-09-17 13:38:19 +06:00
Sergey Palcheh
443c040edd the winemenubuilder is disabled so that it does not create unnecessary shortcuts in the Wine system menu 2025-09-17 13:35:47 +06:00
8 changed files with 238 additions and 587 deletions

View File

@@ -6,10 +6,10 @@ export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WINEPREFIX="scadoffice"
export PROG_NAME="SCAD Office"
export PROG_ICON="scadoffice"
export BASE_PFX="scadaoffice_pfx_x64_v04"
export BASE_PFX="scadaoffice_pfx_x64_v03"
export WH_WINDOWS_VER="10"
export WINEARCH="win64"
export INSTALL_DLL="dotnet20 dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6"
export INSTALL_DLL="dotnet20 dotnet472 dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6"
export WH_XDG_OPEN="rtf"
AUTOINSTALL_EXE="${WH_TMP_DIR}/SCADOffice_installer.exe"
SCADOFFICE_ADDONS_URL="https://cloud.linux-gaming.ru/portproton/scadoffice_addons_v02.tar.xz"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
# info_ru: Установщик программного комплекса NetTest (демо-версия)
########################################################################
export PROG_URL="https://www.kpolyakov.spb.ru/prog/nettest/nettget.htm"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WINEPREFIX="nettest"
export PROG_NAME="NetTest"
export BASE_PFX="none"
export WINEARCH="win64"
export INSTALL_DLL=""
export WH_WINDOWS_VER="10"
ZIP_FILE="$2"
if [[ -f "$ZIP_FILE" ]] \
&& [[ $ZIP_FILE =~ ".zip" ]]
then
prepair_wine
PROG_PATH="$DRIVE_C/nettest"
if [[ $ZIP_FILE =~ "tests" ]] ; then
unpack "$2" "$PROG_PATH/tests"
print_info "Тесты $(basename "$ZIP_FILE") установлены."
exit 0
else
unpack "$2" "$PROG_PATH"
fi
cp -fr "$PROG_PATH/fonts/"* "$DRIVE_C/windows/Fonts/"
create_desktop "$PROG_NAME (Сервер)" "$PROG_PATH/testser.exe" "nettest_server"
create_desktop "$PROG_NAME (Клиент)" "$PROG_PATH/testcli.exe" "nettest_client"
else
fatal "Не найден файл архива для $PROG_NAME. Перезапустите по примеру:
winehelper install $1 \"/путь/до/архива\""
fi

View File

@@ -5,7 +5,6 @@
fb7fdfde96de10a1b3b051bdf2727b6a7c1768b878483726454dd6726e9e0193 wine-9.0.14-alt1-i586-spravkibk.tar.xz
e0a84bb4908c3927954d7eef6b8ac7212e442b8c107d000c6890fec340f96183 wine-9.0.14-alt1-amd64.tar.xz
6f86d2220b65b709bf88c6f829a4998de3b929cc2091cd1333a51c32e1491b79 wine-9.0.9-alt1-i586.tar.xz
f1bf1261550ca2928cefacdb724926d3d6d103433d0ff6882ee9783a50d8f4e4 wine-8.8-staging-amd64.tar.xz
61bec1230b37b8fcc69fd45f848b44fd88cc41fcdd5dc3080336d7da63660f40 wine-7.16.1-alt1-amd64.tar.xz
6fea17fd131f57c2ebf7ca4c60d3c5a9e819afe16e5d0b77ecb750da99ae0e38 wine-7.16.1-alt1-i586.tar.xz
@@ -212,8 +211,8 @@ dfb44ce5e5af7dba1686932c63d6b05e5dd6919a21c78130a7d1d0271b93958e audiorecstatio
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
# winetricks arial dotnet7 dotnetdesktop7 renderer=gdi
4fa93434c5c15440014357323257ddcee7d28b94ad6a56bd6f5a08b33ae4c3cb scadaoffice_pfx_x64_v04.tar.xz
# create with wine-8.8-staging-amd64
25e277c7afa4a9afc5f013cb05f872c12a7f381c4f0503a423dcacccca9a14c6 scadaoffice_pfx_x64_v03.tar.xz
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
# winetricks dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6 dotnet20
# + addons with ODBC, SSH, *.reg
0f4ef434df07bc338ae308af44330590eaa1d9c94b64850514e55b960642d0eb scadoffice_addons_v02.tar.xz

View File

@@ -126,12 +126,6 @@ WH_TESTINSTALL_DIR="$DATA_PATH/testinstall"
WH_WINETRICKS="$DATA_PATH/winetricks_$WINETRICKS_VERSION"
WH_MENU_DIR="$HOME/.local/share/applications/WineHelper"
# TODO: system menu directory
# /usr/share/desktop-directories/WineHelper.directory
# /etc/xdg/menus/applications-merged/WineHelper.menu
# user menu directory
WH_MENU_CATEGORY="$HOME/.local/share/desktop-directories/WineHelper.directory"
WH_MENU_CONFIG="$HOME/.config/menus/applications-merged/WineHelper.menu"
@@ -168,10 +162,12 @@ check_variables WINE_WIN_START "start /wait /high /unix"
check_variables WINE_CPU_TOPOLOGY "8"
check_variables DXVK_VER "none"
check_variables USE_RENDERER "opengl" # opengl, damavand, proton
check_variables DXVK_VER "1.10.3-28"
# check_variables DXVK_CONFIG_FILE "path/to/dxvk.conf"
check_variables VKD3D_VER "none"
check_variables VKD3D_VER "1.1-2602"
# check_variables VKD3D_LIMIT_TESS_FACTORS 64
# check_variables VKD3D_FEATURE_LEVEL "12_0"
@@ -399,14 +395,10 @@ print_license_agreement () {
}
try_download () {
if [[ $1 != "cloud" ]] ; then
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
else
shift
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
local download_file_url output_file output_file_name
download_file_url="${1// /%20}"
@@ -625,7 +617,6 @@ create_desktop () {
echo "StartupNotify=true"
echo "Path=$DATA_PATH"
echo "Icon=$icon_file"
echo "StartupWMClass=$(basename "$exe_file")"
} > "$USER_WORK_PATH/$desktop_filename.desktop"
chmod +x "$USER_WORK_PATH/$desktop_filename.desktop"
@@ -661,7 +652,7 @@ create_desktop () {
[Desktop Entry]
Type=Directory
Name=WineHelper
Icon=winehelper
Icon=wine
EOF
fi
@@ -702,11 +693,9 @@ EOF
echo '#!/usr/bin/env bash'
echo "# cmd_name: $INSTALL_SCRIPT_NAME"
} > "$exe_file".whdb
grep -e "info_" -e "#####" -e "PROG_URL=" -e "WINEPREFIX=" -e "INSTALL_DLL=" \
-e "PROG_NAME=" -e "PROG_ICON=" -e "var_" "$INSTALL_SCRIPT" \
grep -e "info_" -e "#####" -e "export" -e "var_" "$INSTALL_SCRIPT" \
| grep -vE "LAUNCH_PARAMETERS|AUTOINSTALL|WIN_FILE_EXEC|echo" \
| awk '{$1=$1;print}' >> "$exe_file".whdb
print_info "Создан файл настроек для $exe_file"
fi
}
@@ -770,25 +759,31 @@ run_installed_programs () {
fi
}
copy_wined3d () {
for wined3dfiles in $1 ; do
try_copy_wine_dll_to_pfx_64 "$wined3dfiles.dll"
try_copy_wine_dll_to_pfx_32 "$wined3dfiles.dll"
done
init_wined3d () {
if [[ "$USE_RENDERER" != "proton" ]] ; then
WINED3D_FILES="d3d8 d3d9 d3d10_1 d3d10 d3d10core d3d11 dxgi d3d12 d3d12core"
for wined3dfiles in $WINED3D_FILES ; do
try_copy_wine_dll_to_pfx_64 "$wined3dfiles.dll"
try_copy_wine_dll_to_pfx_32 "$wined3dfiles.dll"
done
# if [[ "$USE_RENDERER" == "damavand" ]]
# then export WINE_D3D_CONFIG="renderer=vulkan"
# else export WINE_D3D_CONFIG="renderer=gl"
# fi
return 0
else
return 1
fi
}
init_dxvk () {
DXVK_VER="$1"
if [[ $DXVK_VER == "none" ]] ; then
copy_wined3d "d3d8 d3d9 d3d10_1 d3d10 d3d10core d3d11 dxgi"
return 0
fi
check_variables USE_DXVK_VER "$1"
get_dxvk() {
local DXVK_URL="$1"
local DXVK_VAR_VER="$2"
local DXVK_PACKAGE="${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}.tar.$(echo "${DXVK_URL#*.tar.}")"
if try_download cloud "$DXVK_URL" "$DXVK_PACKAGE" check256sum \
if try_download "$DXVK_URL" "$DXVK_PACKAGE" check256sum \
&& unpack "$DXVK_PACKAGE" "$WH_VULKAN_LIBDIR"
then
try_remove_file "$DXVK_PACKAGE"
@@ -797,37 +792,36 @@ init_dxvk () {
return 1
}
if [[ ! -d "${WH_VULKAN_LIBDIR}/${DXVK_VER}" ]] ; then
get_dxvk "$CLOUD_URL/${DXVK_VER}.tar.xz" "$DXVK_VER"
fi
for DXVK_VAR_VER in "$USE_DXVK_VER" $@ ; do
if [[ ! -d "${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}" ]] ; then
get_dxvk "$CLOUD_URL/${DXVK_VAR_VER}.tar.xz" "$DXVK_VAR_VER"
fi
done
if [[ $WH_USE_WINE_DXGI == "1" ]] ; then
if [[ "${WH_USE_WINE_DXGI}" == 1 ]] ; then
DXVK_FILES="d3d9 d3d10_1 d3d10 d3d11" # dxvk_config openvr_api_dxvk"
copy_wined3d "dxgi"
try_copy_wine_dll_to_pfx_64 "dxgi.dll"
try_copy_wine_dll_to_pfx_32 "dxgi.dll"
else
DXVK_FILES="d3d9 d3d10_1 d3d10 d3d11 dxgi" # dxvk_config openvr_api_dxvk"
fi
for dxvkfiles in $DXVK_FILES ; do
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${DXVK_VER}/x64/$dxvkfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${DXVK_VER}/x32/$dxvkfiles.dll"
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x64/$dxvkfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x32/$dxvkfiles.dll"
then var_winedlloverride_update "$dxvkfiles=n"
fi
done
}
init_vkd3d () {
VKD3D_VER="$1"
if [[ $VKD3D_VER == "none" ]] ; then
copy_wined3d "d3d12 d3d12core"
return 0
fi
check_variables USE_VKD3D_VER "$1"
get_vkd3d() {
local VKD3D_URL="$1"
local VKD3D_VAR_VER="$2"
local VKD3D_PACKAGE="${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}.tar.$(echo "${VKD3D_URL#*.tar.}")"
if try_download cloud "$VKD3D_URL" "$VKD3D_PACKAGE" check256sum \
if try_download "$VKD3D_URL" "$VKD3D_PACKAGE" check256sum \
&& unpack "$VKD3D_PACKAGE" "$WH_VULKAN_LIBDIR"
then
try_remove_file "$VKD3D_PACKAGE"
@@ -836,14 +830,16 @@ init_vkd3d () {
return 1
}
if [[ ! -d "${WH_VULKAN_LIBDIR}/${VKD3D_VER}" ]] ; then
get_vkd3d "$CLOUD_URL/${VKD3D_VER}.tar.xz" "$VKD3D_VER"
fi
for VKD3D_VAR_VER in "$USE_VKD3D_VER" $@ ; do
if [[ ! -d "${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}" ]] ; then
get_vkd3d "$CLOUD_URL/${VKD3D_VAR_VER}.tar.xz" "$VKD3D_VAR_VER"
fi
done
VKD3D_FILES="d3d12 d3d12core libvkd3d-shader-1 libvkd3d-1" # libvkd3d-proton-utils-3
for vkd3dfiles in $VKD3D_FILES ; do
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${VKD3D_VER}/x64/$vkd3dfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${VKD3D_VER}/x86/$vkd3dfiles.dll"
try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x64/$vkd3dfiles.dll"
if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x86/$vkd3dfiles.dll"
then var_winedlloverride_update "$vkd3dfiles=n"
fi
done
@@ -858,7 +854,7 @@ init_wine_ver () {
download_url="$CLOUD_URL/$WH_WINE_USE.tar.xz"
wine_package="$WH_TMP_DIR/$WH_WINE_USE.tar.xz"
try_download cloud "$download_url" "$wine_package" "check256sum"
try_download "$download_url" "$wine_package" "check256sum"
unpack "$wine_package" "$WH_DIST_DIR/"
try_remove_file "$wine_package"
@@ -911,7 +907,7 @@ init_wine_ver () {
CPCSP_PROXY_NAME="wine-cpcsp_proxy-$CPCSP_PROXY_VER"
CPCSP_PROXY_URL="$CLOUD_URL/$CPCSP_PROXY_NAME.tar.xz"
try_download cloud "$CPCSP_PROXY_URL" "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" check256sum
try_download "$CPCSP_PROXY_URL" "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" check256sum
unpack "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" "$WH_TMP_DIR"
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"i386-* "$WINEDIR/lib/wine/"
@@ -1114,11 +1110,6 @@ init_wineprefix () {
export DRIVE_C="$WINEPREFIX/drive_c"
export XUSER_PATH="$DRIVE_C/users/xuser"
if [[ -d "$XUSER_PATH" ]] \
&& [[ ! -d "$DRIVE_C/users/$USER" ]]
then try_force_link_dir "$XUSER_PATH" "$DRIVE_C/users/$USER"
fi
if [[ ! -f "$WINEPREFIX/.firstboot" ]] ; then
create_new_dir "$WINEPREFIX"
if [[ "$CLEAR_PREFIX" == "1" ]]
@@ -1187,7 +1178,6 @@ init_wineprefix () {
# добавление ассоциаций файлов для запуска нативного приложения из wine
# пример переменной: WH_XDG_OPEN="txt doc pdf"
check_variables WH_XDG_OPEN "0"
local WRAPPER="${WH_TMP_DIR}/wh-xdg-open.sh"
local XDG_OPEN_REG="Software\Classes\xdg-open\shell\open\command"
if [[ $WH_XDG_OPEN != "0" ]] ; then
@@ -1210,19 +1200,13 @@ init_wineprefix () {
# добавляем новую команду xdg-open в реестр
get_and_set_reg_file --add "$XDG_OPEN_REG" '@=' 'REG_SZ' "$WRAPPER %1" "system"
# удаляем старые ассоциации, которых нет в новом списке
sed -i '/@="xdg-open"/d' "$WINEPREFIX/system.reg"
# добавляем ассоциации файлов для запуска с помощью xdg-open
for ext in $WH_XDG_OPEN ; do
get_and_set_reg_file --add "Software\Classes\.$ext" '@=' 'REG_SZ' "xdg-open" "system"
done
print_info "Используются ассоциации с нативными приложениями для файлов: \"$WH_XDG_OPEN\""
else
# удаление всех ассоциаций
for old_ext in $old_xdg_open; do
get_and_set_reg_file --delete "Software\Classes\.$old_ext" '@='
done
# удаление команды xdg-open из реестра
get_and_set_reg_file --delete "$XDG_OPEN_REG" '@='
# удаяем скрипт-обёртку
try_remove_file "$WRAPPER"
@@ -1284,7 +1268,7 @@ init_wineprefix () {
echo "# переменные последнего использования префикса:" > "$WINEPREFIX/last.conf"
for var in WH_WINE_USE BASE_PFX WINEARCH WH_WINDOWS_VER WINEESYNC WINEFSYNC \
STAGING_SHARED_MEMORY WINE_LARGE_ADDRESS_AWARE WH_USE_SHADER_CACHE WH_USE_WINE_DXGI \
WINE_CPU_TOPOLOGY DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_MESA_GL_OVERRIDE
WINE_CPU_TOPOLOGY USE_RENDERER DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_MESA_GL_OVERRIDE
do
echo "export $var=\"${!var}\"" >> "$WINEPREFIX/last.conf"
done
@@ -1334,8 +1318,7 @@ use_winetricks () {
}
kill_wine () {
wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' \
| grep "$USER_WORK_PATH" | awk -F/ '{print $3}')
wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' | awk -F/ '{print $3}')
for pw_kill_pids in ${wine_pids}; do
if ps cax | grep "${pw_kill_pids}" ; then
@@ -1362,27 +1345,32 @@ init_database () {
if [[ "$WHDB_FILE" != "0" ]] ; then
print_info "Используется файл настроек: $WHDB_FILE"
. "$WHDB_FILE"
fi
if check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then
elif check_prefix_var && [[ -f "$WINEPREFIX/last.conf" ]] ; then
print_info "Найдены настройки из предыдущего использования префикса: $WINEPREFIX"
cat "$WINEPREFIX/last.conf"
. "$WINEPREFIX/last.conf"
else
print_warning "Файл настроек не найден. Пропускаем."
fi
}
prepair_wine () {
var_winedlloverride_update "winemenubuilder.exe=d"
if [[ -n "$INSTALL_SCRIPT_NAME" ]]
then print_info "Используются настройки из скрипта установки: $INSTALL_SCRIPT_NAME"
else init_database
fi
init_wine_ver
init_wineprefix
use_winetricks
init_dxvk "$DXVK_VER"
init_vkd3d "$VKD3D_VER"
if init_wined3d ; then
:
else
init_dxvk "$DXVK_VER"
init_vkd3d "$VKD3D_VER"
fi
[[ "$MANGOHUD" == 1 ]] && MANGOHUD_RUN="mangohud"
}
@@ -1423,12 +1411,6 @@ wine_run_install () {
}
run_autoinstall () {
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
if [[ $1 == "--clear-pfx" ]] ; then
export CLEAR_PREFIX="1"
shift
@@ -1832,9 +1814,9 @@ create_base_pfx () {
&& [[ ! -L "$users_dir/$USER" ]]
then
if [[ -L "$users_dir/xuser" ]]
then try_remove_dir "$users_dir/xuser"
then try_remove_dir "$users_dir/xuser/"
fi
create_new_dir "$users_dir/xuser"
create_new_dir "$users_dir/xuser/"
cp -fr "$users_dir/$USER"/* "$users_dir/xuser/"
fi
@@ -2145,19 +2127,6 @@ select_component_version() {
done
}
run_install_to_prefix() {
export WINEPREFIX="$1"
local WIN_FILE_EXEC="$2"
if [[ -z "$WINEPREFIX" ]] || [[ -z "$WIN_FILE_EXEC" ]]; then
fatal "Использование: $SCRIPT_NAME install-to-prefix <имя_префикса> <путь_к_установщику>"
fi
check_prefix_var
prepair_wine
wine_run_install "$WIN_FILE_EXEC"
}
run_install_dxvk() {
local version="$1"
if [[ -z "$version" ]] ; then
@@ -2169,14 +2138,16 @@ run_install_dxvk() {
fi
check_prefix_var
init_database
export DXVK_VER="$version"
init_wine_ver
init_wineprefix
if [[ "$DXVK_VER" == "none" ]]
then print_info "Удаление DXVK..."
else print_info "Установка DXVK: $DXVK_VER"
if [[ "$version" == "none" ]] ; then
print_info "Удаление DXVK..."
init_wined3d
update_last_conf_var "DXVK_VER" ""
else
init_dxvk "$version"
update_last_conf_var "DXVK_VER" "$USE_DXVK_VER"
fi
init_dxvk "$DXVK_VER"
wait_wineserver
}
@@ -2191,14 +2162,16 @@ run_install_vkd3d() {
fi
check_prefix_var
init_database
export VKD3D_VER="$version"
init_wine_ver
init_wineprefix
if [[ "$VKD3D_VER" == "none" ]]
then print_info "Удаление VKD3D..."
else print_info "Установка VKD3D: $VKD3D_VER"
if [[ "$version" == "none" ]] ; then
print_info "Удаление VKD3D..."
init_wined3d
update_last_conf_var "VKD3D_VER" ""
else
init_vkd3d "$version"
update_last_conf_var "VKD3D_VER" "$USE_VKD3D_VER"
fi
init_vkd3d "$VKD3D_VER"
wait_wineserver
}
@@ -2270,10 +2243,6 @@ else
arg1="--help"
fi
# отключаем создание .desktop файлов средствами wine
# и отключаем winebth, так как может сломать winedevice.exe
var_winedlloverride_update "winemenubuilder.exe,winebth.sys=d"
case "$arg1" in
--version|version) rpm -qi "$SCRIPT_NAME" ; exit 0 ;;
--help|help) wh_info ; exit 0 ;;
@@ -2286,7 +2255,6 @@ case "$arg1" in
winetricks) prepair_wine ; "$WH_WINETRICKS" -q "$@" ;;
desktop) create_desktop "$@" ; exit 0 ;;
install|-i) run_autoinstall "$@" ;;
install-to-prefix) run_install_to_prefix "$@" ;;
install-dxvk) run_install_dxvk "$@" ;;
install-vkd3d) run_install_vkd3d "$@" ;;
change-wine) run_change_wine_version "$@" ;;

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env xdg-open
[Desktop Entry]
Name=WineHelper
Exec=winehelper gui %F
@@ -8,4 +7,3 @@ Type=Application
Categories=WineHelper;Utility;Emulator;
StartupNotify=true
Icon=winehelper
StartupWMClass=winehelper

View File

@@ -10,11 +10,11 @@ import time
import json
import hashlib
from functools import partial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar,
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, QInputDialog, QDialogButtonBox, QSystemTrayIcon, QMenu)
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
@@ -474,9 +474,10 @@ class WinetricksManagerDialog(QDialog):
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
# Кнопки управления, выровненные по правому краю
# Кнопки управления
button_layout = QHBoxLayout()
button_layout.addStretch(1)
self.status_label = QLabel("Загрузка компонентов...")
button_layout.addWidget(self.status_label, 1)
self.apply_button = QPushButton("Применить")
self.apply_button.setEnabled(False)
@@ -547,6 +548,7 @@ class WinetricksManagerDialog(QDialog):
def load_all_categories(self):
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
self.category_statuses = {name: "загрузка..." for name in self.categories.keys()}
for internal_name in self.categories.values():
self._start_load_process(internal_name)
@@ -592,6 +594,7 @@ class WinetricksManagerDialog(QDialog):
env.insert("WINEPREFIX", self.prefix_path)
env.insert("WINE", self.wine_executable)
# Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'.
env.insert("WINEDLLOVERRIDES", "winemenubuilder.exe=d")
# Это известная проблема при запуске winetricks из ГУИ.
process.setProcessEnvironment(env)
@@ -600,6 +603,13 @@ class WinetricksManagerDialog(QDialog):
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
def _update_status_label(self):
"""Обновляет текстовую метку состояния загрузки."""
status_parts = []
for name, status in self.category_statuses.items():
status_parts.append(f"{name}: {status}")
self.status_label.setText(" | ".join(status_parts))
def _parse_winetricks_log(self):
"""Читает winetricks.log и возвращает множество установленных компонентов."""
installed_verbs = set()
@@ -672,15 +682,22 @@ class WinetricksManagerDialog(QDialog):
if exit_code != 0 or exit_status != QProcess.NormalExit:
error_string = process.errorString() if process else "N/A"
self._log(f"--- Ошибка загрузки категории '{category_display_name}' (код: {exit_code}) ---", "red")
self._log(f"--- Ошибка загрузки категории '{category}' (код: {exit_code}) ---", "red")
self.category_statuses[category_display_name] = "ошибка"
self._update_status_label() # Показываем ошибку в статусе
if exit_status == QProcess.CrashExit:
self._log("--- Процесс winetricks завершился аварийно. ---", "red")
# По умолчанию используется "Неизвестная ошибка", которая не очень полезна.
if error_string != "Неизвестная ошибка":
self._log(f"--- Системная ошибка: {error_string} ---", "red")
self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.")
self._log("--------------------------------------------------", "red")
else:
self.category_statuses[category_display_name] = "готово"
installed_verbs = self._parse_winetricks_log()
# Обновляем статус только если это была сетевая загрузка
if from_cache is None:
self._update_status_label()
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget)
if from_cache is None: # Только если мы не читали из кэша
@@ -705,6 +722,7 @@ class WinetricksManagerDialog(QDialog):
self.loading_count -= 1
if self.loading_count == 0:
self.status_label.setText("Готово.")
self._update_ui_state()
def _on_item_changed(self, item):
@@ -817,6 +835,8 @@ class WinetricksManagerDialog(QDialog):
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
env.insert("WINE", self.wine_executable)
# Отключаем winemenubuilder, чтобы он не создавал лишние ярлыки в системном меню Wine.
env.insert("WINEDLLOVERRIDES", "winemenubuilder.exe=d")
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)
@@ -845,6 +865,11 @@ class WinetricksManagerDialog(QDialog):
# 3. Обрабатываем успех
self._log("\n=== Все операции успешно завершены ===")
self._show_message_box("Успех",
"Операции с компонентами были успешно выполнены.",
QMessageBox.Information,
{"buttons": {"Да": QMessageBox.AcceptRole}})
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние
self.close_button.setEnabled(True)
@@ -854,6 +879,7 @@ class WinetricksManagerDialog(QDialog):
search_edit.clear()
# Перезагружаем данные, чтобы обновить состояние
self.status_label.setText("Обновление данных...")
self.initial_states.clear()
self.load_all_categories()
self.installation_finished = True
@@ -1210,9 +1236,9 @@ class CreatePrefixDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.parent_gui = parent # Сохранить ссылку на главное окно
self.parent_gui = parent # Store reference to main window
self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(680, 250)
self.setMinimumSize(500, 250)
self.setModal(True)
# Attributes to store results
@@ -1226,22 +1252,9 @@ class CreatePrefixDialog(QDialog):
form_layout = QFormLayout()
form_layout.setSpacing(10)
# Создаем виджет для поля ввода и предупреждения
name_input_widget = QWidget()
name_input_layout = QVBoxLayout(name_input_widget)
name_input_layout.setContentsMargins(0, 0, 0, 0)
name_input_layout.setSpacing(2)
self.prefix_name_edit = QLineEdit()
self.prefix_name_edit.setPlaceholderText("Например: my_prefix")
name_input_layout.addWidget(self.prefix_name_edit)
self.name_warning_label = QLabel("Имя может содержать только латинские буквы, цифры, тире и знаки подчеркивания.")
self.name_warning_label.setStyleSheet("color: red;")
self.name_warning_label.setVisible(False)
name_input_layout.addWidget(self.name_warning_label)
form_layout.addRow("<b>Имя нового префикса:</b>", name_input_widget)
form_layout.addRow("<b>Имя нового префикса:</b>", self.prefix_name_edit)
arch_widget = QWidget()
arch_layout = QHBoxLayout(arch_widget)
@@ -1298,7 +1311,7 @@ class CreatePrefixDialog(QDialog):
# Connect signals
self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection)
self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
self.prefix_name_edit.textChanged.connect(self.update_create_button_state)
self.wine_version_edit.textChanged.connect(self.update_create_button_state)
def open_wine_version_dialog(self):
@@ -1314,28 +1327,11 @@ class CreatePrefixDialog(QDialog):
self.wine_version_edit.clear()
self.selected_wine_version_value = None
def validate_prefix_name(self, text):
"""Проверяет имя префикса в реальном времени и показывает/скрывает предупреждение."""
valid_pattern = r'^[a-zA-Z0-9_-]*$'
if re.match(valid_pattern, text):
self.name_warning_label.setVisible(False)
else:
# Удаляем недопустимые символы
cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
# Блокируем сигналы, чтобы избежать рекурсии при изменении текста
self.prefix_name_edit.blockSignals(True)
self.prefix_name_edit.setText(cleaned_text)
self.prefix_name_edit.blockSignals(False)
self.name_warning_label.setVisible(True)
self.update_create_button_state()
def update_create_button_state(self):
"""Включает или выключает кнопку 'Создать'."""
name_ok = bool(self.prefix_name_edit.text().strip())
version_ok = bool(self.wine_version_edit.text().strip())
# Кнопка активна, только если имя валидно и версия выбрана
self.create_button.setEnabled(name_ok and version_ok and not self.name_warning_label.isVisible())
self.create_button.setEnabled(name_ok and version_ok)
def accept_creation(self):
"""Валидирует данные, сохраняет их и закрывает диалог с успехом."""
@@ -1345,8 +1341,8 @@ class CreatePrefixDialog(QDialog):
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
return
if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания.")
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)
@@ -1362,75 +1358,6 @@ class CreatePrefixDialog(QDialog):
self.accept()
class FileAssociationsDialog(QDialog):
"""Диалог для управления ассоциациями файлов (WH_XDG_OPEN)."""
def __init__(self, current_associations, parent=None):
super().__init__(parent)
self.setWindowTitle("Настройка ассоциаций файлов")
self.setMinimumWidth(450)
self.setModal(True)
self.new_associations = current_associations
layout = QVBoxLayout(self)
layout.setSpacing(10) # Добавляем вертикальный отступ между виджетами
info_label = QLabel(
"Укажите расширения файлов, которые должны открываться нативными<br>"
"приложениями Linux. Чтобы удалить все ассоциации, очистите поле.<br><br>"
"<b>Пример:</b> <code>pdf docx txt</code>"
)
info_label.setWordWrap(True)
info_label.setTextFormat(Qt.RichText)
layout.addWidget(info_label)
self.associations_edit = QLineEdit()
# Если ассоциации не заданы (значение "0"), поле будет пустым, чтобы показать подсказку
if current_associations != "0":
self.associations_edit.setText(current_associations)
self.associations_edit.setPlaceholderText("Введите расширения через пробел...")
layout.addWidget(self.associations_edit)
# Запрещенные расширения
forbidden_label = QLabel(
"<small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small>"
)
forbidden_label.setTextFormat(Qt.RichText) # Включаем обработку HTML
layout.addWidget(forbidden_label)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.validate_and_accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def validate_and_accept(self):
"""Проверяет введенные данные перед закрытием."""
forbidden_extensions = {"cpl", "dll", "exe", "lnk", "msi"}
# Получаем введенные расширения, очищаем от лишних пробелов
input_text = self.associations_edit.text().lower().strip()
entered_extensions = {ext.strip() for ext in input_text.split() if ext.strip()}
found_forbidden = entered_extensions.intersection(forbidden_extensions)
if found_forbidden:
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Warning)
msg_box.setWindowTitle("Недопустимые расширения")
msg_box.setTextFormat(Qt.RichText)
msg_box.setText(
"Следующие расширения запрещены и не могут быть использованы:<br><br>"
f"<b>{', '.join(sorted(list(found_forbidden)))}</b>"
)
msg_box.exec_()
return
# Сохраняем результат в виде отсортированной строки
self.new_associations = " ".join(sorted(list(entered_extensions)))
self.accept()
class ComponentVersionSelectionDialog(QDialog):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
@@ -1611,7 +1538,6 @@ class WineHelperGUI(QMainWindow):
self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке
self.prefixes_before_install = set()
self.is_quitting = False # Флаг для корректного выхода из приложения
self.command_output_buffer = ""
self.command_last_line_was_progress = False
# Создаем главный виджет и layout
@@ -1678,50 +1604,6 @@ class WineHelperGUI(QMainWindow):
self.raise_()
self.activateWindow()
def create_tray_icon(self):
"""Создает и настраивает иконку в системном трее."""
if not QSystemTrayIcon.isSystemTrayAvailable():
print("Системный трей не доступен.")
return
self.tray_icon = QSystemTrayIcon(self)
icon_path = Var.WH_ICON_PATH
if icon_path and os.path.exists(icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
self.tray_icon.setIcon(QIcon(pixmap))
# Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось
self.tray_menu = QMenu(self)
toggle_visibility_action = self.tray_menu.addAction("Показать/Скрыть")
toggle_visibility_action.triggered.connect(self.toggle_visibility)
self.tray_menu.addSeparator()
quit_action = self.tray_menu.addAction("Выход")
quit_action.triggered.connect(self.quit_application)
self.tray_icon.activated.connect(self.on_tray_icon_activated)
self.tray_icon.show()
def on_tray_icon_activated(self, reason):
"""Обрабатывает клики по иконке в трее."""
# Показываем меню при левом клике
if reason == QSystemTrayIcon.Trigger:
# Получаем позицию курсора и показываем меню
self.tray_menu.popup(QCursor.pos())
def toggle_visibility(self):
"""Переключает видимость главного окна."""
if self.isVisible() and self.isActiveWindow():
self.hide()
else:
# Сначала скрываем, чтобы "сбросить" состояние, затем активируем.
# Это помогает обойти проблемы с фокусом и переключением рабочих столов.
self.hide()
self.activate()
def add_tab(self, widget, title):
"""Добавляет вкладку в кастомный TabBar и страницу в StackedWidget."""
self.tab_bar.addTab(title)
@@ -2163,7 +2045,7 @@ class WineHelperGUI(QMainWindow):
# --- Контейнер для выбора и управления созданными префиксами ---
self.management_container_groupbox = QGroupBox()
self.management_container_groupbox.setVisible(True) # Всегда виден
self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов
container_layout = QVBoxLayout(self.management_container_groupbox)
selector_layout = QHBoxLayout()
@@ -2172,13 +2054,6 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.currentIndexChanged.connect(self.on_created_prefix_selected)
selector_layout.addWidget(self.created_prefix_selector, 1)
self.create_base_pfx_button = QPushButton()
self.create_base_pfx_button.setIcon(QIcon.fromTheme("document-export"))
self.create_base_pfx_button.setToolTip("Создать шаблон из выбранного префикса (для опытных пользователей)")
self.create_base_pfx_button.setEnabled(False)
self.create_base_pfx_button.clicked.connect(self.create_base_prefix_from_selected)
selector_layout.addWidget(self.create_base_pfx_button)
self.delete_prefix_button = QPushButton()
self.delete_prefix_button.setIcon(QIcon.fromTheme("user-trash"))
self.delete_prefix_button.setToolTip("Удалить выбранный префикс")
@@ -2270,56 +2145,61 @@ class WineHelperGUI(QMainWindow):
self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.")
management_layout.addWidget(self.vkd3d_manage_button, 5, 1)
self.file_associations_button = QPushButton("Ассоциации файлов")
self.file_associations_button.setMinimumHeight(32)
self.file_associations_button.clicked.connect(self.open_file_associations_manager)
self.file_associations_button.setToolTip(
"Настройка открытия определенных типов файлов с помощью нативных приложений Linux.")
management_layout.addWidget(self.file_associations_button, 6, 0, 1, 2)
# --- Правая сторона: Информационный блок и кнопки установки ---
right_column_widget = QWidget()
right_column_layout = QVBoxLayout(right_column_widget)
right_column_layout.setContentsMargins(0, 0, 0, 0)
right_column_layout.setSpacing(10)
# --- Правая сторона: Информационный блок ---
self.prefix_info_display = QTextBrowser()
self.prefix_info_display.setReadOnly(True)
self.prefix_info_display.setFrameStyle(QFrame.StyledPanel)
right_column_layout.addWidget(self.prefix_info_display)
# Увеличиваем rowspan, чтобы охватить все строки с кнопками
management_layout.addWidget(self.prefix_info_display, 0, 2, 6, 1)
management_layout.setColumnStretch(0, 1)
management_layout.setColumnStretch(1, 1)
management_layout.setColumnStretch(2, 2)
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
management_layout.addWidget(separator, 6, 0, 1, 3)
install_group = QWidget()
install_layout = QVBoxLayout(install_group)
install_layout.setContentsMargins(0, 0, 0, 0)
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)
# Layout для кнопок установки и создания ярлыка
action_buttons_layout = QHBoxLayout()
self.prefix_install_button = QPushButton("Установить приложение в префикс")
self.prefix_install_button.setEnabled(False)
self.prefix_install_button.clicked.connect(self.browse_and_run_prefix_installer)
install_layout.addWidget(self.prefix_install_button)
self.prefix_install_button.clicked.connect(self.run_prefix_installer)
action_buttons_layout.addWidget(self.prefix_install_button)
self.create_launcher_button = QPushButton("Создать ярлык для приложения в префиксе")
self.create_launcher_button.setToolTip(
"Создает ярлык в меню и на вкладке 'Установленные' для .exe файла внутри префикса.")
self.create_launcher_button.clicked.connect(self.create_launcher_for_prefix)
self.create_launcher_button.setEnabled(False) # Изначально неактивна
install_layout.addWidget(self.create_launcher_button)
right_column_layout.addWidget(install_group)
right_column_layout.setStretch(0, 1) # Информационное окно растягивается
right_column_layout.setStretch(1, 0) # Группа кнопок не растягивается
management_layout.addWidget(right_column_widget, 0, 2, 7, 1)
management_layout.setColumnStretch(0, 1)
management_layout.setColumnStretch(1, 1)
management_layout.setColumnStretch(2, 2)
action_buttons_layout.addWidget(self.create_launcher_button)
install_layout.addLayout(action_buttons_layout)
management_layout.addWidget(install_group, 7, 0, 1, 3)
container_layout.addWidget(self.prefix_management_groupbox)
layout.addWidget(self.management_container_groupbox)
layout.addStretch()
self.add_tab(self.prefix_tab, "Менеджер префиксов")
self.prefix_install_path_edit.textChanged.connect(self.update_prefix_install_button_state)
def _get_current_prefixes(self):
"""Возвращает множество имен существующих префиксов."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
@@ -2343,8 +2223,8 @@ class WineHelperGUI(QMainWindow):
def _load_created_prefixes(self):
"""Загружает и обновляет список созданных префиксов в выпадающем списке."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
has_prefixes_dir = os.path.isdir(prefixes_root_path)
if not has_prefixes_dir:
if not os.path.isdir(prefixes_root_path):
self.management_container_groupbox.setVisible(False)
return
try:
@@ -2363,16 +2243,18 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.blockSignals(False)
if not prefix_names:
self.management_container_groupbox.setVisible(False)
self.on_created_prefix_selected(-1) # Убедимся, что панель управления сброшена
return
self.management_container_groupbox.setVisible(True)
def on_created_prefix_selected(self, index):
"""Обрабатывает выбор префикса из выпадающего списка."""
if index == -1:
self.current_managed_prefix_name = None
self._setup_prefix_management_panel(None)
self.delete_prefix_button.setEnabled(False)
self.create_base_pfx_button.setEnabled(False)
else:
# Прокручиваем к выбранному элементу, чтобы он был виден в списке
self.created_prefix_selector.view().scrollTo(
@@ -2382,7 +2264,6 @@ class WineHelperGUI(QMainWindow):
self.current_managed_prefix_name = prefix_name
self._setup_prefix_management_panel(prefix_name)
self.delete_prefix_button.setEnabled(True)
self.create_base_pfx_button.setEnabled(True)
def delete_selected_prefix(self):
"""Удаляет префикс, выбранный в выпадающем списке на вкладке 'Менеджер префиксов'."""
@@ -2444,68 +2325,27 @@ class WineHelperGUI(QMainWindow):
# Успешное удаление, обновляем GUI
self._remove_prefix_from_gui_state(prefix_name)
self.update_installed_apps()
QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' и все связанные с ним данные были успешно удалены.")
else:
QMessageBox.critical(self, "Ошибка удаления", f"Не удалось удалить префикс '{prefix_name}'.\nПодробности смотрите в логе.")
def create_base_prefix_from_selected(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\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
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)
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._run_simple_command("create-base-pfx", [prefix_name])
self.command_dialog.exec_()
def _setup_prefix_management_panel(self, prefix_name):
"""Настраивает панель управления префиксом на основе текущего состояния."""
is_prefix_selected = bool(prefix_name)
self.prefix_management_groupbox.setEnabled(is_prefix_selected)
self.create_launcher_button.setEnabled(is_prefix_selected)
self.prefix_install_button.setEnabled(is_prefix_selected)
if is_prefix_selected:
self.update_prefix_info_display(prefix_name)
else:
self.prefix_info_display.clear()
self.prefix_install_path_edit.clear()
# Сбрасываем состояние кнопок, когда префикс не выбран
self.esync_button.setChecked(False)
self.fsync_button.setChecked(False)
self.update_prefix_install_button_state()
def update_prefix_info_display(self, prefix_name):
"""Обновляет информационный блок для созданного префикса, читая данные из last.conf."""
if not prefix_name:
@@ -2559,9 +2399,8 @@ class WineHelperGUI(QMainWindow):
"VKD3D_VER": ("Версия VKD3D", lambda v: v if v else "Не установлено"),
"WINEESYNC": ("ESync", lambda v: "Включен" if v == "1" else "Выключен"),
"WINEFSYNC": ("FSync", lambda v: "Включен" if v == "1" else "Выключен"),
"WH_XDG_OPEN": ("Ассоциации файлов", lambda v: v if v and v != "0" else "Не заданы"),
}
display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC", "WH_XDG_OPEN"]
display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER", "WINEESYNC", "WINEFSYNC"]
html_content = f'<p style="line-height: 1.3; font-size: 9pt;">'
html_content += f"<b>Имя:</b> {html.escape(prefix_name)}<br>"
@@ -2586,13 +2425,8 @@ class WineHelperGUI(QMainWindow):
html_content += "</p>"
self.prefix_info_display.setHtml(html_content)
def browse_and_run_prefix_installer(self):
"""Открывает диалог выбора файла и запускает установку в созданный префикс."""
prefix_name = self.current_managed_prefix_name
if not prefix_name:
QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс для установки.")
return
def browse_for_prefix_installer(self):
"""Открывает диалог выбора файла для установки в созданный префикс."""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Выберите установочный файл",
@@ -2600,11 +2434,18 @@ class WineHelperGUI(QMainWindow):
"Исполняемые файлы (*.exe *.msi);;Все файлы (*)"
)
if file_path:
self.run_prefix_installer(file_path)
self.prefix_install_path_edit.setText(file_path)
def run_prefix_installer(self, installer_path):
"""Запускает установку файла в выбранный префикс через скрипт winehelper."""
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, "Ошибка", "Не выбран префикс для установки.")
@@ -2613,6 +2454,9 @@ class WineHelperGUI(QMainWindow):
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)
@@ -2636,12 +2480,15 @@ class WineHelperGUI(QMainWindow):
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
self.command_process.finished.connect(self._handle_prefix_install_finished)
# Окружение полностью настраивается скриптом winehelper
self.command_process.setProcessEnvironment(QProcessEnvironment.systemEnvironment())
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", prefix_path)
# Отключаем winemenubuilder, чтобы установщик не создавал ярлыки в обход WineHelper.
env.insert("WINEDLLOVERRIDES", "winemenubuilder.exe=d")
self.command_process.setProcessEnvironment(env)
args = ["install-to-prefix", prefix_name, installer_path]
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)
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 _get_prefix_component_version(self, prefix_name, component_key):
@@ -2710,12 +2557,11 @@ class WineHelperGUI(QMainWindow):
# Для удаления лицензия не нужна, запускаем сразу.
self.run_component_install_command(prefix_name, command, version)
else:
# Установка: для DXVK и VKD3D лицензию не показываем.
if component not in ['dxvk', 'vkd3d-proton']:
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
# Установка: сначала показываем лицензионное соглашение.
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
# Запускаем установку.
# Если лицензия принята, запускаем установку.
self.run_component_install_command(prefix_name, command, version)
def open_wine_version_manager(self):
@@ -2746,6 +2592,9 @@ class WineHelperGUI(QMainWindow):
new_version = dialog.selected_version
new_version_display = dialog.selected_display_text
if not self._show_license_agreement_dialog():
return # Пользователь отклонил лицензию
self.run_change_wine_version_command(prefix_name, new_version, new_version_display)
def run_change_wine_version_command(self, prefix_name, new_version, new_version_display):
@@ -2859,88 +2708,6 @@ class WineHelperGUI(QMainWindow):
if exit_code == 0:
self.update_prefix_info_display(prefix_name)
def open_file_associations_manager(self):
"""Открывает диалог для управления ассоциациями файлов."""
prefix_name = self.current_managed_prefix_name
if not prefix_name:
QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
return
current_associations = self._get_prefix_component_version(prefix_name, "WH_XDG_OPEN") or "0"
dialog = FileAssociationsDialog(current_associations if current_associations != "0" else "", self)
if dialog.exec_() == QDialog.Accepted:
new_associations = dialog.new_associations
# Запускаем обновление, только если значение изменилось
if new_associations != (current_associations if current_associations != "0" else "0"):
self.run_update_associations_command(prefix_name, new_associations)
def run_update_associations_command(self, prefix_name, new_associations):
"""Выполняет команду обновления ассоциаций файлов."""
# --- Прямое редактирование last.conf, чтобы обойти перезапись переменных в winehelper ---
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
QMessageBox.critical(self, "Ошибка", f"Файл конфигурации last.conf не найден для префикса '{prefix_name}'.")
return
try:
with open(last_conf_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
updated = False
for i, line in enumerate(lines):
if line.strip().startswith("export WH_XDG_OPEN="):
lines[i] = f'export WH_XDG_OPEN="{new_associations}"\n'
updated = True
break
if not updated:
lines.append(f'export WH_XDG_OPEN="{new_associations}"\n')
with open(last_conf_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
except IOError as e:
QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf: {e}")
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
self.command_dialog = QDialog(self)
self.command_dialog.setWindowTitle("Обновление ассоциаций файлов")
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(
lambda exit_code, exit_status: self._handle_component_install_finished(
prefix_name, exit_code, exit_status))
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", prefix_path)
# Переменная WH_XDG_OPEN теперь читается из измененного last.conf
self.command_process.setProcessEnvironment(env)
# Вызываем init-prefix, который теперь прочитает правильное значение из last.conf
args = ["init-prefix"]
self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
self.command_process.start(self.winehelper_path, args)
self.command_dialog.exec_()
def create_launcher_for_prefix(self):
"""
Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.
@@ -3041,15 +2808,14 @@ class WineHelperGUI(QMainWindow):
authors_text = QTextEdit()
authors_text.setReadOnly(True)
authors_text.setHtml("""
<div style="text-align: center; font-size: 10pt;">
<p><span style="font-size: 11pt;"><b>Разработчики</b></span><br>
<div style="text-align: center;">
<h2>Разработчики</h2>
Михаил Тергоев (fidel)<br>
Сергей Пальчех (minergenon)</p>
<p><span style="font-size: 11pt;"><b>Помощники</b></span><br>
<p><b>Помощники</b><br>
Иван Мажукин (vanomj)</p>
<p><span style="font-size: 11pt;"><b>Идея и поддержка</b></span><br>
ООО "Базальт СПО"<br>
ALT Linux Team</p>
<p><b>Идея и поддержка:</b><br>
сообщество ALT Linux</p>
<br>
<p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
тестирует и сообщает об ошибках!</p>
@@ -3194,6 +2960,8 @@ class WineHelperGUI(QMainWindow):
env.insert("WH_WINE_USE", wine_use)
if base_pfx:
env.insert("BASE_PFX", base_pfx)
# Отключаем winemenubuilder, чтобы при инициализации префикса не создавались стандартные ярлыки Wine.
env.insert("WINEDLLOVERRIDES", "winemenubuilder.exe=d")
self.command_process.setProcessEnvironment(env)
args = ["init-prefix"]
@@ -3227,6 +2995,13 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.setCurrentText(prefix_name)
if not self.management_container_groupbox.isVisible():
self.management_container_groupbox.setVisible(True)
QMessageBox.information(self, "Успех",
f"Префикс '{prefix_name}' успешно создан.\n"
"Теперь вы можете управлять им, выбрав его из выпадающего списка.")
def update_installed_apps(self):
"""Обновляет список установленных приложений в виде кнопок"""
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
@@ -3295,6 +3070,7 @@ class WineHelperGUI(QMainWindow):
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):
@@ -3315,13 +3091,11 @@ class WineHelperGUI(QMainWindow):
def show_installed_app_info(self, desktop_path, button_widget):
"""Показывает информацию об установленном приложении в правой панели."""
self._set_active_button(button_widget)
# Если в поиске был текст, очищаем его и перерисовываем список.
# Это предотвращает "прыжок", если список не был отфильтрован.
if self.installed_search_edit.text():
self.installed_search_edit.blockSignals(True)
self.installed_search_edit.clear()
self.installed_search_edit.blockSignals(False)
self.filter_installed_buttons()
# Очищаем поле поиска и принудительно обновляем список, чтобы показать все приложения
self.installed_search_edit.blockSignals(True)
self.installed_search_edit.clear()
self.installed_search_edit.blockSignals(False)
self.filter_installed_buttons()
# Прокручиваем к выбранному элементу
frame = button_widget.parent()
@@ -3771,7 +3545,7 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
return
process = QProcess()
process = QProcess(self)
env = QProcessEnvironment.systemEnvironment()
cmd_start_index = 0
@@ -3789,10 +3563,7 @@ class WineHelperGUI(QMainWindow):
arguments = clean_command[cmd_start_index + 1:]
process.setProcessEnvironment(env)
# Используем functools.partial для надежной передачи аргументов
# и избегания проблем с замыканием в lambda.
process.finished.connect(partial(self._on_app_process_finished, desktop_path))
process.finished.connect(lambda: self._on_app_process_finished(desktop_path))
try:
process.start(program, arguments)
@@ -3811,51 +3582,6 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка",
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
def quit_application(self):
"""Инициирует процесс выхода из приложения."""
self.is_quitting = True
self.close() # Инициируем событие закрытия, которое будет обработано в closeEvent
def closeEvent(self, event):
"""Обрабатывает событие закрытия главного окна."""
# Теперь любое закрытие окна (крестик или выход из меню) инициирует выход
if self.running_apps:
msg_box = QMessageBox(self)
msg_box.setWindowTitle('Подтверждение выхода')
msg_box.setTextFormat(Qt.RichText)
msg_box.setText('<font color="red">Все запущенные приложения будут закрыты вместе с WineHelper.</font><br><br>'
"Вы уверены, что хотите выйти?")
msg_box.setIcon(QMessageBox.Question)
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:
# Отключаем обработчики сигналов от всех запущенных процессов,
# так как мы собираемся их принудительно завершить и выйти.
# Это предотвращает ошибку RuntimeError при закрытии.
for process in self.running_apps.values():
process.finished.disconnect()
# Используем встроенную команду killall для надежного завершения всех процессов wine
print("Завершение всех запущенных приложений через 'winehelper killall'...")
# Используем subprocess.run, который дождется завершения команды
subprocess.run([self.winehelper_path, "killall"], check=False, capture_output=True)
# Принудительно дожидаемся завершения всех дочерних процессов
for process in self.running_apps.values():
process.waitForFinished(5000) # Ждем до 5 секунд
QApplication.instance().quit()
event.accept()
else:
event.ignore()
else:
QApplication.instance().quit() # Если нет запущенных приложений, просто выходим
def uninstall_app(self):
"""Удаляет выбранное установленное приложение и его префикс"""
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
@@ -4039,14 +3765,11 @@ class WineHelperGUI(QMainWindow):
search_edit = tab_data['search_edit']
scroll_area = tab_data['scroll_area']
# Если в поиске был текст, очищаем его и перерисовываем список.
# Это предотвращает "прыжок", если список не был отфильтрован.
if search_edit.text():
search_edit.blockSignals(True)
search_edit.clear()
search_edit.blockSignals(False)
self.filter_buttons(tab_type)
# Общая логика: очищаем поиск, обновляем список и прокручиваем к элементу
search_edit.blockSignals(True)
search_edit.clear()
search_edit.blockSignals(False)
self.filter_buttons(tab_type)
frame = button_widget.parent()
if isinstance(frame, QFrame):
QTimer.singleShot(0, lambda: scroll_area.ensureWidgetVisible(frame))
@@ -4408,6 +4131,18 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.setCurrentIndex(0)
# --- Конец обновления списка префиксов ---
# Создаем кастомный диалог, чтобы кнопка была на русском
success_box = QMessageBox(self.install_dialog)
success_box.setWindowTitle("Успех")
title_name = self._get_current_app_title()
success_text = f"Программа «{title_name}» установлена успешно!"
if new_prefix_name:
success_text += f"\n\nНовый префикс '{new_prefix_name}' был автоматически выбран в списке управления на вкладке 'Менеджер префиксов'."
success_box.setText(success_text)
success_box.setIcon(QMessageBox.Information)
success_box.addButton("Готово", QMessageBox.AcceptRole)
success_box.exec_()
self.update_installed_apps()
# Кнопка закрыть
@@ -4485,22 +4220,13 @@ class WineHelperGUI(QMainWindow):
self.install_process.terminate()
def _handle_command_output(self):
"""Обрабатывает вывод для общих команд в модальном диалоге."""
"""Обрабатывает вывод для диалога команды"""
if hasattr(self, 'command_process') and self.command_process:
# Используем readAll, чтобы получить и stdout, и stderr
output_bytes = self.command_process.readAll()
output = output_bytes.data().decode('utf-8', errors='ignore').strip()
output = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
if output and hasattr(self, 'command_log_output'):
self.command_log_output.append(output)
QApplication.processEvents()
def _run_simple_command(self, command, args=None):
"""Запускает простую команду winehelper и выводит лог."""
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_command_finished)
self.command_process.start(self.winehelper_path, [command] + (args or []))
def _handle_command_finished(self, exit_code, exit_status):
"""Обрабатывает завершение для диалога команды"""
if exit_code == 0:
@@ -4516,6 +4242,7 @@ class WineHelperGUI(QMainWindow):
"""Обрабатывает завершение создания ярлыка."""
self._handle_command_finished(exit_code, exit_status)
if exit_code == 0:
QMessageBox.information(self, "Успех", "Ярлык успешно создан.")
self.update_installed_apps()
# Переключаемся на вкладку "Установленные"
for i in range(self.tab_bar.count()):
@@ -4601,8 +4328,6 @@ def main():
# Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора
window.server = server
window.show()
# Создаем иконку в системном трее после создания окна
window.create_tray_icon()
return app.exec_()
return 1