Compare commits

..
53 Commits
Author SHA1 Message Date
Mikhail Tergoev 3dbfea89e3 add launch of third-party scripts 2026-04-24 14:28:13 +03:00
Mikhail Tergoev daa13f2e4c update print_error for cpcsp_setup 2026-04-23 16:19:24 +03:00
Mikhail Tergoev 2aca05928a fix restore pfx and use su_run example.sh with noexec home 2026-04-23 15:03:32 +03:00
Mikhail Tergoev 8b2af7015b add AGENTS.md 2026-04-23 11:40:54 +03:00
Mikhail Tergoev 4315cd0e3f Merge branch 'minergenon-readme' 2026-04-23 11:35:23 +03:00
Mikhail Tergoev fa10a3cf0e Merge branch 'minergenon-adem' 2026-04-23 11:33:09 +03:00
Sergey Palcheh 9f932c4403 Readme and image/handbook files have been updated to current ones 2026-04-23 13:46:03 +06:00
Mikhail Tergoev b725be78d1 add and use wine-11.6.1-scadoffice-alt5-wow64 for scadoffice 2026-04-20 16:14:14 +03:00
Sergey Palcheh 7cbd3cffc7 fixed the RuntimeError error when closing a window during process execution 2026-04-17 09:52:30 +06:00
Mikhail Tergoev 190698a1b4 Merge branch 'minergenon-fixedgui' 2026-04-08 17:17:23 +03:00
Mikhail Tergoev c96033311c Merge branch 'minergenon-nalog' 2026-04-08 17:17:06 +03:00
Mikhail Tergoev 0a225d4c3d Merge branch 'minergenon-devel' 2026-04-08 17:15:34 +03:00
Sergey Palcheh 18b92b66b1 gui: fixed auto-scrolling of the list in the component manager when searching 2026-04-07 21:58:00 +06:00
Mikhail Tergoev 230315bc57 added winedbg for debug 2026-04-07 17:21:57 +03:00
Sergey Palcheh 3d981158b0 gui: Download tab in the Wine version selection window has been removed 2026-04-07 19:00:19 +06:00
Sergey Palcheh 4ad1e38141 added to INSTALL_DLL vb6run testinstall/npul 2026-04-07 17:42:39 +06:00
Mikhail Tergoev ae24b2386e Merge branch 'minergenon-gui' 2026-04-07 13:50:24 +03:00
Sergey Palcheh 8b5b199ff1 update testinstall/npul 2026-04-07 15:24:19 +06:00
Sergey Palcheh af291e9c7a fixed the grep error when deleting all WH data 2026-04-07 11:30:27 +06:00
Sergey Palcheh c8cb60e42c refactor (gui): extract services and utils, replace Var with EnvironmentConfig
- replace global Var class with EnvironmentConfig for environment variables
- extract ScriptManager, InstallationManager, AppLauncher, SettingsManager services
- add CacheService, FileCacheService, ProcessUtils utilities
- add IconManager widget for centralized icon management
- simplify WinetricksCacheService using FileCacheService
- move winetricks parsing logic to WinetricksOutputParser
- add missing directory creation in PrefixManager.save_config
- clean up whitespace and remove unused imports
2026-04-07 10:19:54 +06:00
Mikhail Tergoev 4d508afb44 updated changelog to 0.12.0 2026-04-06 21:22:25 +03:00
Mikhail Tergoev f36869bea5 skip copy desktop file if noexec home 2026-04-06 21:16:35 +03:00
Mikhail Tergoev 0e311b5d07 replaced wine_x_tkg_10-0 to wine-10.12.1-alt1-i586 for win32 2026-04-06 20:30:16 +03:00
Mikhail Tergoev cfbb6030b5 fixed work cpcsp-proxy with noexec home 2026-04-06 19:29:49 +03:00
Mikhail Tergoev 8661d0258d updated changelog to 0.11.0 2026-04-06 15:51:52 +03:00
Mikhail Tergoev 9bb7dc7db0 fixed work with noexec home 2026-04-06 15:39:58 +03:00
Mikhail Tergoev 1f08c75cb2 dropped bc from check log size 2026-04-06 11:03:09 +03:00
Mikhail Tergoev 17802d2bc5 Merge branch 'minergenon-devel' 2026-04-06 10:37:21 +03:00
Mikhail Tergoev 86694b3721 Merge branch 'minergenon-gui' 2026-04-06 10:25:27 +03:00
Sergey Palcheh ad37601b23 added the display of the program icon in the window title 2026-04-03 13:34:17 +06:00
Mikhail Tergoev a6d894ce99 updated wine and script for emias 2026-03-31 14:48:53 +03:00
Mikhail Tergoev 8877012ec4 updated scadoffice_pfx 2026-03-31 14:48:22 +03:00
Sergey Palcheh 5774e033d4 added archiving of logs exceeding a certain size 2026-03-24 17:28:33 +06:00
Mikhail Tergoev de8294b34d Merge branch 'minergenon-devel' 2026-03-19 11:56:29 +03:00
Mikhail Tergoev 273e86958d updated changelog to 0.10.0 2026-03-18 20:11:39 +03:00
Mikhail Tergoev d0f3d0f8d6 used xz for backup; simplified dll registration 2026-03-18 19:44:58 +03:00
Sergey Palcheh fc01de74b2 renaming modules: gui -> winehelperqt 2026-03-14 16:10:32 +06:00
Mikhail Tergoev eb5d6bb613 cleaning and sorting wine 2026-03-05 11:53:16 +03:00
Mikhail Tergoev e65cd2386c added wine-11.3.1-alt1-wow64 2026-03-05 11:10:34 +03:00
Mikhail Tergoev 6b094f4905 fixed change windows version 2026-03-04 21:53:06 +03:00
Mikhail Tergoev 7f44432388 added wine-cpcsp_proxy-0.7.7-alt1.1-em-wow64 2026-03-04 15:29:00 +03:00
Mikhail Tergoev fbfccd0417 updated CPCSP_PROXY_WOW64_VER for emias 2026-03-04 15:15:28 +03:00
Mikhail Tergoev 1942d49f4d added CPCSP_PROXY update in prefix when changing version 2026-03-04 15:14:15 +03:00
Mikhail Tergoev ab2933f8b0 Merge branch 'minergenon-ctm' 2026-03-03 17:33:19 +03:00
Mikhail Tergoev 4b985417c4 olimp: fixed cropped text 2026-03-03 17:32:06 +03:00
Mikhail Tergoev d52bec4798 added WINEDLLOVERRIDES to whdb file 2026-03-03 16:33:10 +03:00
Mikhail Tergoev f2c7ecf926 updated wine and pfx for olimp 2026-03-03 11:54:00 +03:00
Mikhail Tergoev a929411daf updated olimp_pfx_x86 to v02 2026-03-03 11:53:25 +03:00
Mikhail Tergoev 8114b3cb54 do not touch the registry for WH_XDG_OPEN by default 2026-03-03 11:24:31 +03:00
Mikhail Tergoev 04e974a0b7 added wine-10.18.1-alt2-i586 2026-03-03 11:07:26 +03:00
Sergey Palcheh efe8ef7267 updated autoinstall/ctm-service 2026-03-03 13:19:14 +06:00
Sergey Palcheh a4c6ef89bf gui: removed the --force flag for installing components using winetricks 2026-02-25 16:08:52 +06:00
Sergey Palcheh 2bfb574ba1 refactor: separating GUI components into separate modules
- gui package has been created/ with modules: parsers, services, dialogues
- dialog classes have been introduced (WinetricksManagerDialog, SettingsDialog, etc.)
- services have been introduced (PrefixManager, Desktops Filemanager)
2026-02-25 13:07:47 +06:00
86 changed files with 6611 additions and 2416 deletions

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
.project
.pydevproject
.settings/
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Temporary files
*.tmp
*.bak
+232
View File
@@ -0,0 +1,232 @@
# WineHelper - Руководство для AI агентов
## Обзор проекта
WineHelper — инструмент для упрощения установки Windows-приложений на Linux. Использует подготовленные скрипты, портативные версии Wine и изолированные префиксы, обеспечивая максимальную идентичность на различных операционных системах, основанных на ALT Linux (начиная с релиза p10 и выше).
**Лицензия:** GNU Lesser General Public License v2.1 (LGPL-2.1+)
**Автор:** Mikhail Tergoev (2025)
**Язык документации и комментариев:** Русский
**Статус проекта:** WIP (work in progress)
## Технологический стек
### Основные технологии
- **Bash** — основной язык для CLI-интерфейса (~2692 строк в главном скрипте)
- **Python 3** — графический интерфейс с использованием PyQt5
- **Wine** — запуск Windows-приложений (поддерживаются различные версии: vanilla, staging, Proton, tkg, crossover)
- **DXVK/VKD3D** — трансляция DirectX → Vulkan
### Зависимости Python
- PyQt5 (основной GUI фреймворк)
- Стандартная библиотека Python (os, subprocess, sys, re, shlex, shutil, html)
### Системные зависимости
- i586-версии библиотек (для 32-битных приложений, если не используется wow64)
- wget/curl для загрузки
- 7z, tar для распаковки
- wrestool, icotool для извлечения иконок из .exe
- winetricks для управления компонентами Wine
## Структура проекта
```
winehelper/
├── winehelper # Главный bash-скрипт (CLI)
├── winehelper_gui.py # GUI на PyQt5 (точка входа)
├── winehelperqt/ # Python-модуль GUI
│ ├── __init__.py # Экспорты основных классов
│ ├── dialogs/ # Диалоговые окна
│ │ ├── base_version_dialog.py
│ │ ├── component_version.py
│ │ ├── create_prefix.py # Создание префикса
│ │ ├── file_associations.py
│ │ ├── settings.py # Настройки
│ │ ├── wine_version.py # Выбор версии Wine
│ │ └── winetricks_manager.py
│ ├── parsers/ # Парсеры
│ │ ├── script_parser.py # Парсинг скриптов установки
│ │ ├── sha256_parser.py # Парсинг sha256sum.list
│ │ └── winetricks_parser.py
│ ├── services/ # Бизнес-логика
│ │ ├── application_installer.py
│ │ ├── desktop_file_manager.py
│ │ ├── prefix_manager.py # Управление префиксами
│ │ ├── process_handler.py
│ │ └── winetricks_service.py
│ ├── utils/ # Утилиты
│ │ └── log_handler.py # Обработка логов
│ └── widgets/ # Кастомные виджеты
├── autoinstall/ # Скрипты автоматической установки (43 приложения)
├── manualinstall/ # Скрипты ручной установки (9 приложений)
├── testinstall/ # Тестовые скрипты установки
├── database/ # Базы данных префиксов (.whdb файлы)
├── image/ # Иконки приложений и GUI ресурсы
│ ├── gui/ # Иконки приложения (SVG)
│ └── handbook/ # Скриншоты для документации
├── hiden_scripts/ # Скрытые/вспомогательные скрипты
├── auto_completion/ # Автодополнение для bash
├── dependencies.sh # Установка системных зависимостей
├── sha256sum.list # SHA256 хеши загружаемых файлов
└── winehelper.desktop # Desktop-файл для запуска GUI
```
## Архитектура кода
### CLI (winehelper)
Главный скрипт `winehelper` — монолитный bash-скрипт с функциональными блоками:
1. **Переменные и окружение** — определение путей, версий, флагов
2. **Функции сообщений** — цветной вывод в терминал
3. **Утилиты** — работа с файлами, архивами, скачивание
4. **Управление префиксами** — создание, удаление, бэкап/восстановление
5. **Wine management** — установка версий Wine, DXVK, VKD3D
6. **Desktop integration** — создание .desktop файлов, меню
7. **Основной цикл** — обработка аргументов командной строки
### GUI (winehelperqt)
Модульная архитектура PyQt5:
- **WineHelperGUI** — главное окно, управление вкладками
- **PrefixManager** — CRUD операции с префиксами
- **ApplicationInstaller** — установка приложений
- **ScriptParser** — парсинг информации из скриптов установки
- **Диалоги** — модальные окна для настроек и выбора
### Скрипты установки
Каждый скрипт установки — это bash-скрипт с переменными:
```bash
#!/usr/bin/env bash
# info_ru: Описание программы
export PROG_URL="https://example.com"
export WH_WINE_USE="wine-10.18.1-alt1-wow64" # Версия Wine
export WINEPREFIX="prefix_name" # Имя префикса
export PROG_NAME="Название программы"
export BASE_PFX="base_prefix_v01" # Базовый шаблон (опционально)
export WINEARCH="win64" # Архитектура (win32/win64)
export INSTALL_DLL="corefonts msxml3..." # DLL через winetricks
# Логика установки
prepair_wine
try_download "URL" "${FILE_PATH}"
unpack "${FILE_PATH}" "${DEST_DIR}"
wine_run_install "${SETUP_EXE}" /S
create_desktop "Имя ярлыка" "${EXE_PATH}" "icon_name"
```
## Режимы работы
### Системный режим
Когда скрипт установлен в `/usr/bin/winehelper`:
- Данные в `/usr/share/winehelper/`
- Рабочая директория пользователя: `~/.local/share/winehelper/`
- Иконки в `/usr/share/icons/...`
### Режим разработки
При запуске из репозитория:
- Рабочая директория: `~/test-winehelper/`
- Данные берутся из текущей директории
- Включается проверка синтаксиса скриптов
## Команды CLI
```bash
# Установка
winehelper install list # Список доступных программ
winehelper -i <script_name> # Установить программу
winehelper -i <script_name> /path # Установить из дистрибутива
# Управление префиксами
winehelper create-prefix # Создать префикс
winehelper remove-prefix [name] # Удалить префикс
winehelper backup-prefix [name] # Создать бэкап (.whpack)
winehelper restore-prefix <path> # Восстановить из бэкапа
# Wine tools
winehelper winecfg # Настройки Wine
winehelper winereg # Редактор реестра
winehelper winefile # Файловый менеджер
winehelper winetricks [component] # Winetricks
winehelper <file.exe> # Запустить exe в префиксе
# Управление компонентами
winehelper install-dxvk [version] # Установить DXVK
winehelper install-vkd3d [version] # Установить VKD3D
winehelper change-wine [version] # Сменить версию Wine
# Другое
winehelper --debug <command> # Режим отладки
winehelper --changelog # История изменений
winehelper gui # Запустить GUI
```
## Разработка
### Запуск в режиме разработки
```bash
# GUI режим
./winehelper gui
# CLI режим
./winehelper install list
./winehelper -i alta-expert
```
### Добавление нового скрипта установки
1. Создать файл в `autoinstall/` (для автоматической установки) или `manualinstall/` (требуется дистрибутив)
2. Добавить обязательные переменные: `PROG_NAME`, `WINEPREFIX`, `WH_WINE_USE`, `PROG_URL`
3. Добавить комментарий `# info_ru:` с описанием
4. Добавить иконку в `image/` (PNG, имя совпадает с переменной в create_desktop)
5. Обновить `sha256sum.list` если требуется загрузка архивов
### Требования к коду
**Bash:**
- Использовать `#!/usr/bin/env bash`
- Проверять наличие переменных перед использованием
- Использовать функции обработки ошибок (`fatal`, `print_error`)
- Экранировать все пути с помощью кавычек
**Python:**
- Импортировать из `winehelperqt` относительно
- Использовать type hints где возможно
- Документация на русском языке
- Классы должны наследоваться от соответствующих QWidget
## Безопасность
- Скрипт запрещает запуск от root
- Проверяет noexec для /home
- Проверяет SHA256 хеши загружаемых файлов
- Использует временные файлы с ограниченными правами (600)
- Поддержка лицензионного соглашения перед установкой
## Особенности ALT Linux
- Требуется репозиторий x86_64-i586 для 32-битных зависимостей
- Интеграция с RPM (версия определяется через `rpm -q winehelper`)
- Использование apt-get для установки зависимостей
- Desktop-файлы размещаются в `~/.local/share/applications/`
## Полезные ссылки
- Официальная документация: https://www.altlinux.org/Winehelper
- Совместимость ПО (P10): https://www.basealt.ru/fileadmin/user_upload/compatibility/P10-view2.html
- Совместимость ПО (P11): https://www.basealt.ru/fileadmin/user_upload/compatibility/P11-view2.html
## Сторонние компоненты
- Wine / Wine-Staging / Proton / wine-tkg
- Wine-Mono, Wine-Gecko
- DXVK, VKD3D
- Winetricks
- wine-cpcsp_proxy (для КриптоПро)
+24
View File
@@ -1,5 +1,29 @@
История изменений:
0.12.0
* исправлена работа cpcsp_proxy при монтировании home с флагом noexec
* обновлены все скрипты установки ВЭД и СТМ
0.11.0
* исправлена работа WineHelper при монтировании home с флагом noexec
* исправлена иконка графического режима Qt5
* добавлена автоматическая архивация лога, если он больше 10 Мб
* исправлена работа сертификатов для emias (спасибо lav@)
0.10.0
* добавлены скрипты установок:
- apropos-z
- olimp
* обновлены скрипты установок:
- ctm-service
- emias
* добавлены версии wine:
- wine-11.3.1-alt1-wow64
- wine-10.18.1-alt2-i586
- wine-9.0-cx-amd64
* добавлена поддержка wine-cx (форк wine от crossover)
* небольшие улучшения графического режим Qt5
0.9.0
* добавлена поддержка wine-wow64 (не требует установку 32-битных зависимостей в систему)
* добавлена поддержка wine-cpcsp_proxy для wine-wow64 (не требует установку 32-битного КриптоПро в систему)
+60 -16
View File
@@ -48,19 +48,19 @@
$ winehelper -i
```
### Запуск установки приложений
### Запуск установки приложений из списка скриптов
Скрипты установки разделены на два типа:
1. **Список программ с возможностью автоматической установки** — содержит полностью автоматизированные скрипты установок программ у которыx есть дистрибутивы (установщики) в свободном доступе.
1. **Список программ с возможностью автоматической установки** — содержит полностью автоматизированные скрипты установок программ у которых есть дистрибутивы (установщики) в свободном доступе.
Процесс полностью автоматизирован.
Пример:
```
$ winehelper install spravki-bk
$ winehelper install ksamu
```
или сокращенная команда:
```
$ winehelper -i spravki-bk
$ winehelper -i ksamu
```
2. **Список программ с возможностью установки из существующего дистрибутива** — содержит скрипты установок для программ, которых нет в свободном доступе.
Пример:
@@ -72,7 +72,7 @@
$ winehelper -i is-record-station "/путь/до/установочного_файла"
```
### Процесс установки приложений
### Процесс установки приложений из списка скриптов
При первом запуске **WineHelper** проверяются и при необходимости устанавливаются дополнительные зависимости (запрашиваются права root).
@@ -88,6 +88,27 @@
Если устанавливаемое приложение требует дополнительных действий от пользователя (ввод лицензии, настройка), это будет явно указано в терминале.
### Установка своего приложения
Для установки своего приложения (не из списка готовых скриптов) выполните следующие шаги:
**Этап 1: Создание префикса**
Создайте новый префикс с использованием wine-wow64 (рекомендуется для новых приложений):
```
$ winehelper create-prefix
```
Выберите wine-wow64 версию и разрядность 64-bit.
> [!NOTE]
> **Всегда сначала пробуйте wine-wow64**, это упрощает настройку и избавляет от 32-битных зависимостей.
**Этап 2: Запуск установщика**
Запустите установочный файл в созданном префиксе:
```
$ winehelper "/путь/до/установочного_файла.exe"
```
Выберите префикс из списка (введите номер и нажмите Enter).
### Удаление префикса с приложением
Для удаления префикса выполните команду:
@@ -101,14 +122,14 @@
2 - Префикс2
Выберите префикс (0-2):
```
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**.Далее потребуется подтвердить удаление префикса вводом **y**.
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**. Далее потребуется подтвердить удаление префикса вводом **y**.
В результате будут удалены:
- сам префикс;
- установленное ПО;
- desktop-файлы (ярлыки для установленного ПО в префиксе).
###
### Cоздание резервной копии префикса
Команда создания резервной копии префикса:
```
@@ -138,7 +159,7 @@
Восстановление префикса не требует подключение к интернету и происходит в автоматическом режиме:
- Распаковка префикса.
- Восстановление версии WINE.
- Восстановление ярлыков (.desktop-файdлов).
- Восстановление ярлыков (.desktop-файлов).
После завершения восстановления приложение будет доступно для запуска через меню или рабочий стол.
@@ -175,7 +196,7 @@ WineHelper предоставляет доступ к основным инст
```
Файл будет запущен в выбранном префиксе.
2. С помощью файлового менеджера WINE:'''
2. С помощью файлового менеджера WINE:
```
$ winehelper winefile
0 - Отмена
@@ -239,7 +260,7 @@ WineHelper предоставляет доступ к основным инст
<p><em>Вкладка "Ручная установка"</em></p>
</div>
Так же на вкладке **Автоматическая установка** есть чекбокс с тестовыми версиями скриптов установки которые еще не прошли полную проверку на зависимости и совместимость, но уже можно попробовать в работе.
Также на вкладке **Автоматическая установка** есть чекбокс с тестовыми версиями скриптов установки которые еще не прошли полную проверку на зависимости и совместимость, но уже можно попробовать в работе.
<div align="center">
<img src="image/handbook/auto_install_test.png">
@@ -324,9 +345,9 @@ WineHelper предоставляет доступ к основным инст
#### Создание нового префикса
Нажав кнопку **«Создать новый префикс»**, вы откроете диалог, где можно задать:
* **Имя префикса**.
* **Имя нового префикса**.
* **Разрядность** (32-bit или 64-bit).
* **Наполнение** (чистый префикс или с рекомендуемыми библиотеками).
* **Шаблон префикса** (чистый префикс или с рекомендуемыми библиотеками).
* **Версию Wine** из доступного списка.
<div align="center">
@@ -334,22 +355,45 @@ WineHelper предоставляет доступ к основным инст
<p><em>Диалог создания нового префикса</em></p>
</div>
#### Установка своего приложения
Для установки своего приложения (не из списка готовых скриптов) выполните следующие шаги:
**Этап 1: Создание префикса**
1. Вкладка **«Менеджер префиксов»**
2. Нажмите **«Создать новый префикс»**
3. Задайте имя префикса
4. Выберите **wine-wow64** версию (рекомендуется)
5. Выберите разрядность **64-bit**
> [!NOTE]
> **Всегда сначала пробуйте wine-wow64**, это упрощает настройку и избавляет от 32-битных зависимостей.
**Этап 2: Установка приложения**
1. Выберите созданный префикс из выпадающего списка
2. Нажмите кнопку **«Запустить файл в префиксе»**, выберите файл для установки
3. Нажмите кнопку **«Создать ярлык для приложения в префиксе»**, выберите установленный файл в префиксе
#### Управление существующим префиксом
Выбрав префикс из выпадающего списка, вы получаете доступ к панели управления, которая позволяет:
* **Управлять компонентами**:
* **Менеджер компонентов (Winetricks)**: Удобный интерфейс для установки и переустановки библиотек, шрифтов и настроек.
* **Запускать стандартные утилиты Wine**:
* `Редактор настроек (winecfg)`
* `Редактор реестра (regedit)`
* `Удаление программ (uninstaller)`
* `Командная строка (cmd)`
* `Файловый менеджер (winefile)`
* **Управлять компонентами**:
* **Менеджер компонентов (Winetricks)**: Удобный интерфейс для установки и переустановки библиотек, шрифтов и настроек.
* **Управление Wine**: Смена версии Wine для выбранного префикса. Диалог выбора позволяет как выбрать уже установленную версию, так и скачать новую из списков, сгруппированных по архитектуре.
* `Панель управления (control)`
* **Управлять версиями Wine**:
* **Управление версией Wine**: Смена версии Wine для выбранного префикса. Диалог выбора позволяет как выбрать уже установленную версию, так и скачать новую из списков, сгруппированных по архитектуре.
* **Управлять дополнительными настройками**:
* **Управление DXVK/VKD3D**: Установка или удаление конкретных версий DXVK и vkd3d-proton.
* **Ассоциации файлов**: Настройка открытия определенных типов файлов (например, `.pdf`, `.docx`) нативными приложениями Linux, чтобы избежать их открытия внутри Wine.
* **Включать/выключать ESync и FSync**.
* **Включать/выключать ESync и FSync**: Включение/выключение методов оптимизации синхронизации потоков в Windows-приложениях при запуске через Wine.
* **Включать/выключать CPCSP_PROXY**: Поддержка КриптоПро CSP (электронная подпись). Включайте только для приложений, требующих ЭП.
* **Устанавливать приложения**: Установить любой `.exe` или `.msi` файл напрямую в выбранный префикс.
* **Создавать ярлыки**: Создать ярлык для любого исполняемого файла внутри префикса.
* **Удалять префикс** или **создавать из него шаблон**.
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Пакет программ для таможенного оформления. Включает программы Такса и ТамДок в комплектациях Эксперт, а также программу Заполнитель.
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Альта-Эксперт"
export BASE_PFX="alta_pfx_x86_v01"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Таможенное оформление товаров
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Альта-ГТД"
export PROG_ICON="altagtd"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Пакет программ для таможенного оформления. Включает программы Альта-ГТД, Такса и ТамДок в комплектациях Максимум, а также программу Заполнитель.
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Альта-Максимум"
export BASE_PFX="alta_pfx_x86_v01"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Таможенное законодательство
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Таможенные документы"
export PROG_ICON="altatamdoc"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Расчет контракта
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Такса"
export PROG_ICON="altatax"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Автоматизация импорта данных
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="alta"
export PROG_NAME="Заполнитель"
export PROG_ICON="altazap"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.ctm.ru"
export PROG_NAME="СТМ-Финансы"
export PROG_ICON="ctmfinance"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.ctm.ru"
export PROG_NAME="СТМ-Журналы"
export PROG_ICON="ctmjournals"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.ctm.ru"
export PROG_NAME="Монитор ЭД"
export PROG_ICON="ctmmonitor"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.ctm.ru"
export PROG_NAME="СТМ-Отчет"
export PROG_ICON="ctmreport"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+2 -2
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.ctm.ru"
export PROG_NAME="СТМ-Сервис"
export PROG_ICON="ctmservice"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
@@ -18,7 +18,7 @@ get_and_set_reg_file --add 'Software\CTM\CTMSETUP' 'SkipKBCheck' 'REG_DWORD' "1"
AUTOINSTALL_EXE="${WH_TMP_DIR}/setup_cs.exe"
AUTOINSTALL_UNPACK="${WH_TMP_DIR}/setup_cs"
AUTOINSTALL_SETUP="${WH_TMP_DIR}/setup_cs/setup.exe"
AUTOINSTALL_SETUP="${WH_TMP_DIR}/setup_cs/Setup.exe"
if try_download "https://files.ctm.ru/CSERVICE/SFX/setup_cs.exe" "${AUTOINSTALL_EXE}" ; then
unpack "${AUTOINSTALL_EXE}" "${AUTOINSTALL_UNPACK}"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Сервис обновления ПО: "R-Атлас", "R-Инфо" и "R-Тариф".
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WINEPREFIX="daobit"
export PROG_NAME="Сервис обновления ПО"
export PROG_ICON="daobitcservice"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Построение маршрутов железнодорожных грузоперевозок
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WINEPREFIX="daobit"
export PROG_NAME="R-Атлас"
export PROG_ICON="daobitratlas"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Специализированная информация для организации железнодорожных перевозок
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WINEPREFIX="daobit"
export PROG_NAME="R-Инфо"
export PROG_ICON="daobitrinfo"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Расчёт размера провозной платы и дополнительных сборов за грузовые перевозки по железной дороге
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WINEPREFIX="daobit"
export PROG_NAME="R-Тариф"
export PROG_ICON="daobitrtariff"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://www.keepsoft.ru"
export PROG_NAME="Конструктор тестов"
export AUTOINSTALL_EXE="${WH_TMP_DIR}/simulator4.exe"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="keepsoft_similator4"
export BASE_PFX="defpfx_x86_v01"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Алфавит"
export PROG_ICON="vedalf"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Контроль"
export PROG_ICON="vedcontrol"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Декларант"
export PROG_ICON="veddcl"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Инфо"
export PROG_ICON="vedinfo"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Платежи"
export PROG_ICON="vedpayments"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -4,7 +4,7 @@
export PROG_URL="https://ctm.ru"
export PROG_NAME="ВЭД-Склад"
export PROG_ICON="vedsts"
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Автоматизация установки МИС Витакарта (AIS LPU Client).
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="ais-lpu-client"
export PROG_NAME="AIS LPU Client"
export PROG_ICON="ais_lpu_client"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 208 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Специализированное программное обеспечение «Автономная станция прослушивания» предназначено для загрузки и прослушивания электронных материалов итогового собеседования.
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="desktopaudiostation"
export PROG_NAME="АС Прослушивания"
export PROG_ICON="desktopaudiostation"
+3 -5
View File
@@ -2,16 +2,14 @@
# info_ru: Установщик "ЕМИАС" - Единая медицинская информационно-аналитическая система
########################################################################
export PROG_URL="https://emias.info/"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-11.4.1-emias-alt2-wow64"
export WINEPREFIX="emias"
export PROG_NAME="КИС ЕМИАС"
export BASE_PFX="emias_pfx_x64_v01"
export BASE_PFX="none"
export WINEARCH="win64"
export INSTALL_DLL="crypt32"
export WH_WINDOWS_VER="7"
export WH_USE_CPCSP_PROXY="1"
export STAGING_SHARED_MEMORY="1"
export WINE_LARGE_ADDRESS_AWARE="1"
export CPCSP_PROXY_WOW64_VER="0.7.7-alt1.1-em-wow64"
if [[ -f "$2" ]] \
&& [[ $(basename "$2") =~ (kis\.exe|kis_z\.exe) ]]
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Специализированное программное обеспечение «Автономная станция записи» предназначено для записи ответов участников итогового собеседования.
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="isrecordingstation"
export PROG_NAME="АС Записи"
export PROG_ICON="isrecordingstation"
+3 -2
View File
@@ -2,15 +2,16 @@
# info_ru: "КИС Олимп" - программно-аналитический комплекс, состоящий из самостоятельных подключаемых модулей.
########################################################################
export PROG_URL="https://olimp.best/"
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.18.1-alt2-i586"
export WINEPREFIX="olimp"
export PROG_NAME="КИС Олимп"
export BASE_PFX="olimp_pfx_x86_v01"
export BASE_PFX="olimp_pfx_x86_v02"
export WINEARCH="win32"
export WH_WINDOWS_VER="7"
export INSTALL_DLL="jet40 dotnet45 corefonts lucida opensymbol tahoma cjkfonts mdac28 msxml4 native_oleaut32 vcrun2005"
export WINEDLLOVERRIDES="d2d1=n"
export WH_MAIN_DECORATED="0"
export FREETYPE_PROPERTIES="truetype:interpreter-version=35"
if [[ -f "$2" ]] \
&& [[ $(basename "$2") =~ (Loader\.exe) ]]
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Это мощный, современный инструмент быстрой и качественной разработки АСУТП
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WINEPREFIX="scada-krug-2000"
export PROG_NAME="SCADA КРУГ 2000"
export PROG_ICON="krugtaskmanager"
+16 -12
View File
@@ -1,24 +1,28 @@
##### WINE WOW64 #####
e9a8b79dfe12cad1304dca573d73aefe109e3d69b6fff739c8dbb8b2d3c271c6 wine-10.12.1-alt1-wow64.tar.xz
a862761c432f8619caf8100589678a1cbb523787259120ccd4388089d81e3b17 wine-10.18.1-tflex-alt2-wow64.tar.xz
8b9a6548eb5df921514cd569607e7f845acd18a3a6cf56f1f724b38bc9a94e43 wine-11.6.1-scadoffice-alt5-wow64.tar.xz
a176e2ee3d60f691538b3c463047ff5eabd0ba2e37c0158783d79e9fa68caa2f wine-11.4.1-emias-alt2-wow64.tar.xz
3c7c394658ad993968532e20ae9b591c1a0ec5b7495c8450de09bd4f8947462e wine-11.3.1-alt1-wow64.tar.xz
349c707148a23e667970309248bbbf97b4e2a0db59c548e73d9fe9c3f585872c wine-10.18.1-alt1-wow64.tar.xz
a862761c432f8619caf8100589678a1cbb523787259120ccd4388089d81e3b17 wine-10.18.1-tflex-alt2-wow64.tar.xz
e9a8b79dfe12cad1304dca573d73aefe109e3d69b6fff739c8dbb8b2d3c271c6 wine-10.12.1-alt1-wow64.tar.xz
##### WINE AMD64 #####
009c95bfe2df3f9264c9c5092f3e30ea7a168dd7869046058a718a70739602d4 wine_wh_tflex_10-9_amd64.tar.xz
71d6fdfd23d7988471d345c68e81699b8af931352b03dd216424b398f63c7b12 wine_x_tkg_10-0_amd64.tar.xz
e0a84bb4908c3927954d7eef6b8ac7212e442b8c107d000c6890fec340f96183 wine-9.0.14-alt1-amd64.tar.xz
fd209c95a3aa507002374417365b3d1119dbe3a0a6d223034171221a2b187b66 wine-9.0-cx-amd64.tar.xz
f1bf1261550ca2928cefacdb724926d3d6d103433d0ff6882ee9783a50d8f4e4 wine-8.8-staging-amd64.tar.xz
61bec1230b37b8fcc69fd45f848b44fd88cc41fcdd5dc3080336d7da63660f40 wine-7.16.1-alt1-amd64.tar.xz
fd209c95a3aa507002374417365b3d1119dbe3a0a6d223034171221a2b187b66 wine-9.0-cx-amd64.tar.xz
##### WINE I586 #####
1385323e059c376299a83088ada85e625c170337ba07e5307df75961726d8eee wine-10.18.1-alt2-i586.tar.xz
49ce3e1152f01c1f24ca148092a66f6eccd68cf4ee35d0cb04900120f45dd864 wine-10.12.1-alt1-i586.tar.xz
3571c40a787f0386e0160b5d471b1bf11fa2a235b497156cd38198b90bcb4a9f wine_x_tkg_10-0_i586.tar.xz
fb7fdfde96de10a1b3b051bdf2727b6a7c1768b878483726454dd6726e9e0193 wine-9.0.14-alt1-i586-spravkibk.tar.xz
6f86d2220b65b709bf88c6f829a4998de3b929cc2091cd1333a51c32e1491b79 wine-9.0.9-alt1-i586.tar.xz
6fea17fd131f57c2ebf7ca4c60d3c5a9e819afe16e5d0b77ecb750da99ae0e38 wine-7.16.1-alt1-i586.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
@@ -114,9 +118,9 @@ b3f56f58b5b764aba564c8b8c69eb12425467d57bf2a6457edff08842af03573 vkd3d-proton-1
edf16d2b37bc77d121d5d81b06b60d3f694e0060c6606e729ceab30de3d27466 vkd3d-proton-sarek-2.6.0.tar.xz
##### CPCSP_PROXY #####
3153088ff28d6af415c3504a421e5382554dbb305f38d5a87dd84df90d680421 wine-cpcsp_proxy-0.6.1-alt1.tar.xz
fbe72bfe3c2c307363782ce2d37589e142b44111f269657b0e516a6b2d6ab8a9 wine-cpcsp_proxy-0.7.3-alt5-wow64.tar.xz
5a0c582f662ac501865f569124c153b43cded72eb98a001eaa7ebd5b2168f1a2 wine-cpcsp_proxy-0.7.7-alt1.1-em-wow64.tar.xz
6f43427148c6f424cd49a406328669e4ec4f21df7bc86f1a6aad2b5c16b50e39 wine-cpcsp_proxy-0.7.7-alt1-wow64.tar.xz
3153088ff28d6af415c3504a421e5382554dbb305f38d5a87dd84df90d680421 wine-cpcsp_proxy-0.6.1-alt1.tar.xz
##### FONTS #####
405bfe3b7c7f80034837c05656535053305727ee4bf1d993521b67b71d08ebc6 extra_fonts_v01.tar.xz
@@ -183,10 +187,6 @@ dfb44ce5e5af7dba1686932c63d6b05e5dd6919a21c78130a7d1d0271b93958e audiorecstatio
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
# winetricks arial dotnet7 dotnetdesktop7 renderer=gdi
8c6312f2e4e846a98ca4a87fc90ee1917eb28d4caaddde040fb4d2dd05f8c0fe scadaoffice_pfx_x64_v05.tar.xz
# create with wine_x_tkg_10-0_amd64 (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
ef7e8f1ba785d48e4ea287feed5b79bd630d423e59efadb43da9653adefef218 ais-lpu-client_pfx_x86_v01.tar.xz
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
# winetricks vcrun2005 vcrun2008 dotnet20sp2 dotnet40 mfc42 7zip
@@ -216,11 +216,15 @@ d16c300b5451e4fad6eeebeca0949367a7bdf1cb1878fc51f0f9dee33744d3a7 emias_pfx_x64_
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
# winetricks crypt32
66a31bf72aac7354956c0ce8ddb9a2858b291bb9c8b94229b0820b0119ced97f olimp_pfx_x86_v01.tar.xz
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
cf7f6284fc46d9a0be9756721d04e9f8842a481f805829711d4340fed5e67e69 olimp_pfx_x86_v02.tar.xz
# create with wine-10.18.1-alt2-i586
# winetricks jet40 dotnet45 corefonts lucida opensymbol tahoma cjkfonts mdac28 msxml4 native_oleaut32 vcrun2005
# + psqlodbc_x86.msi installed from script
4fc8ba95190d7d2cd05f60f29fe6af801f99bf046ab75bccd82bf48a600d5fc4 scadoffice_pfx_x64_v06.tar.xz
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
# winetricks msxml6 gdiplus vcrun6sp6 vcrun2005 ucrtbase2019 vcrun2019 d3dx9 d3dx11_43 d3dx11_42 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 dotnet40 dotnet48 riched20 riched30 msls31 richtx32
##### ADDONS #####
# addons with ODBC, SSH, *.reg
0f4ef434df07bc338ae308af44330590eaa1d9c94b64850514e55b960642d0eb scadoffice_addons_v02.tar.xz
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Программное обеспечение для 3D-графики, предназначенное для проектирования освещения. (Версия 4.4.0.2)
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="dialux"
export PROG_NAME="DIALux"
export PROG_ICON="dialux"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Удобная программа для создания двухмерных схем и чертежей. Основной акцент сделан на проектировании систем тепло-, водо- и газоснабжения, водоочистки, автоматизированных систем управления технологическими процессами (АСУТП), а также планов эвакуации при пожаре и авариях.
########################################################################
export PROG_URL="https://easymnemo.com"
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="easymnemo"
export PROG_NAME="Easymnemo"
export PROG_ICON="easymnemo"
+97 -25
View File
@@ -6,42 +6,114 @@ export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WINEPREFIX="nalog"
export PROG_NAME="Налогоплательщик ЮЛ"
export PROG_ICON="npul"
export BASE_PFX="nalog_pfx_x64_v01"
export BASE_PFX="none"
export WINEARCH="win64"
export WH_WINDOWS_VER="10"
export WH_XDG_OPEN="doc"
export INSTALL_DLL="msxml4 msxml6 corefonts wsh57 gdiplus"
export INSTALL_DLL="msxml4 msxml6 corefonts wsh57 gdiplus vb6run"
try_get_page "https://data.nalog.ru/rn77/program/5961229/"
VER_MSI=$(read_page | grep -oP 'NalogUL\d+\.msi' | tail -n 1 | grep -oP '\d+')
VER_MSI_URL="${VER_MSI:0:1}.${VER_MSI:1}" # 492 → 4.92
VER_EXE=$(read_page | grep -oP 'NalogUL\d+\.exe' | tail -n 1 | grep -oP '\d+')
VER_EXE_URL="${VER_EXE:0:1}.${VER_EXE:1:2}.${VER_EXE:3}" # 4924 → 4.92.4
HTML_CONTENT=$(read_page)
AUTOINSTALL_MSI="${WH_TMP_DIR}/NalogUL${VER_MSI}.msi"
AUTOINSTALL_EXE="${WH_TMP_DIR}/NalogUL${VER_EXE}.exe"
# Извлекаем все версии одним проходом (и 3, и 4 цифры, и MSI, и EXE)
declare -A VERSIONS
while IFS= read -r match; do
if [[ -n "$match" ]]; then
ver=$(echo "$match" | grep -oP '\d+')
ext="${match##*.}"
digits="${#ver}"
VERSIONS["${digits}_${ext}"]="$ver"
fi
done < <(echo "$HTML_CONTENT" | grep -oP 'NalogUL\d{3,4}\.(msi|exe)')
prepair_wine
if try_download "https://data.nalog.ru/files/nalul/${VER_MSI_URL}/NalogUL${VER_MSI}.msi" "${AUTOINSTALL_MSI}" ; then
wine_run_install "${AUTOINSTALL_MSI}" /quiet INSTALLDIR="c:\npul"
try_remove_file "${AUTOINSTALL_MSI}"
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi")"
wine_run_install "${PRINT_INSTALLER}" "/q"
# Процедуры конвертации, индексация данных. По завершении процедур программа сама закроется. Признаком успешного завершения будет отсутствие в папке с программой файла setupmarker.ini. Если не принять процедуру, .exe файл обновления не установиться.
wine_run_install "$DRIVE_C/npul/INPUTDOC/inputdoc.exe" go.ini
#TODO: Сделать запуск процедуры в тихом режиме.
# Версия программы (3 цифры, приоритет MSI)
if [[ -n "${VERSIONS[3_msi]}" ]]; then
PROG_VER="${VERSIONS[3_msi]}"
PROG_EXT="msi"
elif [[ -n "${VERSIONS[3_exe]}" ]]; then
PROG_VER="${VERSIONS[3_exe]}"
PROG_EXT="exe"
else
print_error "Не найдена установочная версия программы"
exit 1
fi
if [[ -n "$VER_EXE" ]] \
&& try_download "https://data.nalog.ru/files/nalul/${VER_EXE_URL}/NalogUL${VER_EXE}.exe" "${AUTOINSTALL_EXE}"
then
wine_run_install "${AUTOINSTALL_EXE}" "/SILENT"
try_remove_file "${AUTOINSTALL_EXE}"
# Версия обновления (4 цифры, приоритет EXE)
if [[ -n "${VERSIONS[4_exe]}" ]]; then
UPDATE_VER="${VERSIONS[4_exe]}"
UPDATE_EXT="exe"
elif [[ -n "${VERSIONS[4_msi]}" ]]; then
UPDATE_VER="${VERSIONS[4_msi]}"
UPDATE_EXT="msi"
else
UPDATE_VER=""
UPDATE_EXT=""
fi
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
# Формируем URL пути
PROG_URL_PATH="${PROG_VER:0:1}.${PROG_VER:1}"
AUTOINSTALL_PROG="${WH_TMP_DIR}/NalogUL${PROG_VER}.${PROG_EXT}"
if [[ -n "$UPDATE_VER" ]]; then
UPDATE_URL_PATH="${UPDATE_VER:0:1}.${UPDATE_VER:1:2}.${UPDATE_VER:3}"
AUTOINSTALL_UPDATE="${WH_TMP_DIR}/NalogUL${UPDATE_VER}.${UPDATE_EXT}"
fi
prepair_wine
# Устанавливаем основную программу
if [[ "$PROG_EXT" == "msi" ]]; then
if try_download "https://data.nalog.ru/files/nalul/${PROG_URL_PATH}/NalogUL${PROG_VER}.msi" "${AUTOINSTALL_PROG}"; then
wine_run_install "${AUTOINSTALL_PROG}" /quiet /norestart INSTALLDIR="c:\npul"
try_remove_file "${AUTOINSTALL_PROG}"
else
print_error "Не удалось скачать установочный файл MSI"
exit 1
fi
else
if try_download "https://data.nalog.ru/files/nalul/${PROG_URL_PATH}/NalogUL${PROG_VER}.exe" "${AUTOINSTALL_PROG}"; then
wine_run_install "${AUTOINSTALL_PROG}" /VERYSILENT /NORESTART /DIR="c:\npul"
try_remove_file "${AUTOINSTALL_PROG}"
else
print_error "Не удалось скачать установочный файл EXE"
exit 1
fi
fi
# Устанавливаем печать PDF417
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
if [[ -n "$PRINT_INSTALLER" ]]; then
wine_run_install "${PRINT_INSTALLER}" "/q"
fi
# Процедуры конвертации, индексация данных
wine_run_install "$DRIVE_C/npul/INPUTDOC/inputdoc.exe" go.ini
# Устанавливаем обновление (если есть)
if [[ -n "$UPDATE_VER" ]]; then
if [[ "$UPDATE_EXT" == "msi" ]]; then
if try_download "https://data.nalog.ru/files/nalul/${UPDATE_URL_PATH}/NalogUL${UPDATE_VER}.msi" "${AUTOINSTALL_UPDATE}"; then
wine_run_install "${AUTOINSTALL_UPDATE}" /quiet /norestart
try_remove_file "${AUTOINSTALL_UPDATE}"
# После обновления может появиться новая версия печати
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
if [[ -n "$PRINT_INSTALLER" ]]; then
wine_run_install "${PRINT_INSTALLER}" /quiet /norestart
fi
fi
else
if try_download "https://data.nalog.ru/files/nalul/${UPDATE_URL_PATH}/NalogUL${UPDATE_VER}.exe" "${AUTOINSTALL_UPDATE}"; then
wine_run_install "${AUTOINSTALL_UPDATE}" /VERYSILENT /NORESTART
try_remove_file "${AUTOINSTALL_UPDATE}"
# После обновления может появиться новая версия печати
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
if [[ -n "$PRINT_INSTALLER" ]]; then
wine_run_install "${PRINT_INSTALLER}" /quiet /norestart
fi
fi
fi
else
print_info "Дополнительных обновлений не требуется."
fi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Цифровая платформа для обучения и оценки специалистов нефтегазовой отрасли.
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_i586"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export PROG_NAME="РТСИМ.Карьера"
export WINEPREFIX="rtsim"
export PROG_ICON="rtsim"
+3 -3
View File
@@ -2,12 +2,12 @@
# info_ru: SCAD Office — это программный комплекс для расчёта строительных конструкций.
########################################################################
export PROG_URL="https://scadoffice.ru"
export WH_WINE_USE="wine-8.8-staging-amd64"
export WH_WINE_USE="wine-11.6.1-scadoffice-alt5-wow64"
export WINEPREFIX="scadoffice"
export PROG_NAME="SCAD Office"
export PROG_ICON="scadoffice"
export BASE_PFX="none"
export WH_WINDOWS_VER="10"
export BASE_PFX="scadoffice_pfx_x64_v06"
export WH_WINDOWS_VER="7"
export WINEARCH="win64"
export INSTALL_DLL="dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6" #dotnet20
export WH_XDG_OPEN="rtf"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Программный комплекс СуперОкна для фабрик по изготовлению окон и дверей.
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="swnd"
export PROG_NAME="СуперОкна 8"
export PROG_ICON="swnd"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Бесплатная версия программы СуперОкна для небольших предприятий.
########################################################################
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WINEPREFIX="swnd"
export PROG_NAME="СуперОкна 8 бесплатная"
export PROG_ICON="swnd"
+301 -74
View File
@@ -49,6 +49,8 @@ else
done
fi
export WH_DIST_DIR="$USER_WORK_PATH/dist"
##### MESSAGES FUNCTIONS #####
if [[ $WH_USE_GUI != "1" ]] ; then
print_error () { printf "\E[31m%s Ошибка: $* %s\e[0m\n" ;}
@@ -65,7 +67,7 @@ fi
print_var () { for vp in $@ ; do echo "${vp}=${!vp}" ; done ;}
fatal () {
print_error "$@ Аварийное завершение работы WineHelper!"
[[ -n "$@" ]] && print_error "$@"
[[ -n "$WINESERVER" ]] && "$WINESERVER" -w
exit 1
}
@@ -82,12 +84,13 @@ print_confirmation () {
##### CHECK NOEXEC FOR /HOME #####
if mount -l | grep -E "[[:space:]]/home[[:space:]]" | grep -q "noexec" ; then
# export WH_DIST_DIR="/opt/winehelper/dist"
# print_warning "/home примонтирован в /etc/fstab с аргументом noexec.\nИспользуем каталог $WH_DIST_DIR"
fatal "/home примонтирован в /etc/fstab с аргументом noexec.\nЗапуск портативной версии wine невозможен из домашнего каталога."
export WH_NOEXEC_HOME="1"
export WH_TMP_DIR="/var/tmp/winehelper-$USER/tmp"
else
export WH_DIST_DIR="$USER_WORK_PATH/dist"
export WH_NOEXEC_HOME="0"
export WH_TMP_DIR="$USER_WORK_PATH/tmp"
fi
[[ ! -d "$WH_TMP_DIR" ]] && mkdir -p "$WH_TMP_DIR"
if [[ $1 == "gui" ]] ; then
export WH_USE_GUI="1"
@@ -121,7 +124,6 @@ fi
##### DEFAULT VARIABLES #####
WH_VULKAN_LIBDIR="$USER_WORK_PATH/vulkan"
WH_PREFIXES_DIR="$USER_WORK_PATH/prefixes"
WH_TMP_DIR="$USER_WORK_PATH/tmp"
WH_IMAGE_PATH="$DATA_PATH/image"
WH_DB_DIR="$DATA_PATH/database"
@@ -717,7 +719,9 @@ EOF
# Обновляем кэш desktop-файлов
update-desktop-database "$HOME/.local/share/applications"
if [[ "$desktop_filename_arg" != "nocopy" ]] ; then
if [[ "$desktop_filename_arg" != "nocopy" ]] \
&& [[ "$WH_NOEXEC_HOME" != "1" ]]
then
desktop_path="$(xdg-user-dir DESKTOP)"
print_info "В меню и на рабочем столе создан ярлык: $desktop_filename.desktop"
cp -f "$USER_WORK_PATH/$desktop_filename.desktop" "$desktop_path"
@@ -737,7 +741,7 @@ EOF
-e "PROG_NAME=" -e "PROG_ICON=" -e "var_" -e "WH_MAIN_DECORATED" \
-e "WH_VIRTUAL_DESKTOP" -e "WINE_TOP_WINDOW" -e "WH_FONT_SMOOTHING "\
-e "WH_FONT_MSS_REPLACE" -e "FREETYPE_PROPERTIES" -e "WINE_D3D_CONFIG" \
-e "WH_USE_MESA_GL_OVERRIDE" \
-e "WH_USE_MESA_GL_OVERRIDE" -e "WINEDLLOVERRIDES" \
"$INSTALL_SCRIPT" | awk '{$1=$1;print}' >> "$exe_file".whdb
print_info "Создан файл настроек для $exe_file"
@@ -781,6 +785,8 @@ check_installed_programs () {
elif [[ "$desktop_file" =~ ${2}.desktop ]] ; then
export EXE_PATH
return 0
elif [[ -f "$2" ]] ; then
return 0
fi
;;
*)
@@ -875,6 +881,105 @@ init_vkd3d () {
done
}
# Подготовка Wine для работы с noexec на /home
# ELF бинарники копируются во временный каталог
# Windows DLL остаются в /home через симлинки
prepare_wine_noexec() {
print_warning "/home примонтирован с noexec."
local wine_name="$1"
local wine_home_dir="$WH_DIST_DIR/$wine_name"
local dist_tmp_dir="/var/tmp/winehelper-$USER/dist"
local wine_tmp_dir="$dist_tmp_dir/$wine_name"
export WH_DIST_DIR="$dist_tmp_dir"
export WINEDIR="$wine_tmp_dir"
update_cpscp_proxy () {
if [[ $UPDATE_CPCSP_PROXY == "1" ]] ; then
print_info "Обновляем cpcsp_proxy во временном каталоге wine..."
if [[ $WH_WINE_WOW64 == "1" ]] ; then
cp -fv "$wine_home_dir/lib64/wine/x86_64-unix/cpcsp_proxy.so" "$wine_tmp_dir/lib64/wine/x86_64-unix/"
cp -fv "$wine_home_dir/lib64/wine/x86_64-windows/cpcsp_proxy.dll" "$wine_tmp_dir/lib64/"
cp -fv "$wine_home_dir/lib64/wine/i386-windows/cpcsp_proxy.dll" "$wine_tmp_dir/lib/"
local i386_proxy_dll="$wine_tmp_dir/lib64/wine/i386-windows/cpcsp_proxy.dll"
if [[ ! -L "$i386_proxy_dll" ]] ; then
try_remove_file "$i386_proxy_dll"
try_force_link_file "$wine_tmp_dir/lib/cpcsp_proxy.dll" "$i386_proxy_dll"
fi
local x86_64_proxy_dll="$wine_tmp_dir/lib64/wine/x86_64-windows/cpcsp_proxy.dll"
if [[ ! -L "$x86_64_proxy_dll" ]] ; then
try_remove_file "$x86_64_proxy_dll"
try_force_link_file "$wine_tmp_dir/lib64/cpcsp_proxy.dll" "$x86_64_proxy_dll"
fi
fi
fi
}
if [[ -f "$wine_tmp_dir/bin/wine" ]] ; then
update_cpscp_proxy
print_ok "Wine готов для работы с noexec. Пропускаем настройку..."
return 0
fi
print_info "Частичное копирование Wine во временный каталог..."
create_new_dir "$wine_tmp_dir/bin"
cp -a "$wine_home_dir/bin/"* "$wine_tmp_dir/bin/"
create_new_dir "$wine_tmp_dir/lib/wine"
if [[ -f "$wine_home_dir/lib/wine/wine" ]] \
|| [[ -f "$wine_home_dir/lib/wine/wine64" ]]
then
cp -a "$wine_home_dir/lib/wine"/wine* "$wine_tmp_dir/lib/wine/"
fi
for unix_dir in i386-unix x86_64-unix; do
if [[ -d "$wine_home_dir/lib/wine/$unix_dir" ]] ; then
cp -a "$wine_home_dir/lib/wine/$unix_dir" "$wine_tmp_dir/lib/wine/"
fi
done
for win_dir in i386-windows x86_64-windows; do
if [[ -d "$wine_home_dir/lib/wine/$win_dir" ]] ; then
try_force_link_dir "$wine_home_dir/lib/wine/$win_dir" "$wine_tmp_dir/lib/wine/$win_dir"
fi
done
if [[ -d "$wine_home_dir/lib/gstreamer-1.0" ]] ; then
cp -a "$wine_home_dir/lib/gstreamer-1.0" "$wine_tmp_dir/lib/"
fi
if [[ -L "$wine_home_dir/lib64/wine" ]] ; then
create_new_dir "$wine_tmp_dir/lib64/"
try_force_link_dir "$wine_tmp_dir/lib/wine/" "$wine_tmp_dir/lib64/"
elif [[ -d "$wine_home_dir/lib64" ]]; then
create_new_dir "$wine_tmp_dir/lib64/wine"
for unix_dir in i386-unix x86_64-unix; do
if [[ -d "$wine_home_dir/lib64/wine/$unix_dir" ]] ; then
cp -a "$wine_home_dir/lib64/wine/$unix_dir" "$wine_tmp_dir/lib64/wine/"
fi
done
for win_dir in i386-windows x86_64-windows; do
if [[ -d "$wine_home_dir/lib64/wine/$win_dir" ]] ; then
try_force_link_dir "$wine_home_dir/lib64/wine/$win_dir" "$wine_tmp_dir/lib64/wine/$win_dir"
fi
done
if [[ -d "$wine_home_dir/lib64/gstreamer-1.0" ]] ; then
cp -a "$wine_home_dir/lib64/gstreamer-1.0" "$wine_tmp_dir/lib64/"
fi
fi
update_cpscp_proxy
try_force_link_file "$wine_home_dir/cpcsp_proxy.ver" "$wine_tmp_dir/cpcsp_proxy.ver"
try_force_link_dir "$wine_home_dir/share" "$wine_tmp_dir/share"
try_copy_file "$wine_home_dir/version" "$wine_tmp_dir/"
print_ok "Wine подготовлен для работы с noexec"
}
init_wine_ver () {
if [[ "$WH_WINE_USE" != system* ]] ; then
export WINEDIR="$WH_DIST_DIR/$WH_WINE_USE"
@@ -902,6 +1007,13 @@ init_wine_ver () {
try_remove_dir "$WINEDIR/files"
fi
if [[ ! -d "$WINEDIR/lib64/wine" ]] \
&& [[ -d "$WINEDIR/lib/wine/x86_64-unix" ]]
then
create_new_dir "$WINEDIR/lib64/"
try_force_link_dir "$WINEDIR/lib/wine/" "$WINEDIR/lib64/"
fi
if [[ ! -f "$WINEDIR/version" ]] ; then
echo "$WH_WINE_USE" > "$WINEDIR/version"
fi
@@ -913,6 +1025,50 @@ init_wine_ver () {
check_deps_i586
fi
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
unset UPDATE_CPCSP_PROXY
local cpcsp_proxy_ver
if [[ $WH_WINE_WOW64 == "1" ]]
then cpcsp_proxy_ver="$CPCSP_PROXY_WOW64_VER"
else cpcsp_proxy_ver="$CPCSP_PROXY_X86_64_VER"
fi
if [[ ! -f "$WINEDIR/cpcsp_proxy.ver" ]] \
|| ! grep -q "$cpcsp_proxy_ver" "$WINEDIR/cpcsp_proxy.ver"
then
export UPDATE_CPCSP_PROXY="1"
local cpcsp_proxy_name="wine-cpcsp_proxy-$cpcsp_proxy_ver"
local 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
unpack "$WH_TMP_DIR/$cpcsp_proxy_name.tar.xz" "$WH_TMP_DIR"
if [[ $WH_WINE_WOW64 == "1" ]] ; then
for dir_name in x86_64-unix i386-windows x86_64-windows ; do
if [[ -f "$WINEDIR/lib64/wine/$dir_name/cpcsp_proxy.dll" ]] ; then
try_remove_file "$WINEDIR/lib64/wine/$dir_name/cpcsp_proxy.dll"
fi
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib64/wine/$dir_name/"
done
else
for dir_name in i386-unix i386-windows ; do
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib/wine/$dir_name/"
done
if [[ -d "$WINEDIR/lib64" ]] ; then
for dir_name in x86_64-unix x86_64-windows ; do
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib64/wine/$dir_name/"
done
fi
fi
try_remove_dir "$WH_TMP_DIR/$cpcsp_proxy_name"
echo "$cpcsp_proxy_ver" > "$WINEDIR/cpcsp_proxy.ver"
fi
fi
# При noexec на /home: готовим временную копию ELF файлов wine перед запуском
if [[ "$WH_NOEXEC_HOME" == "1" ]] ; then
prepare_wine_noexec "$WH_WINE_USE"
fi
export WINE="$WINEDIR/bin/wine"
export WINELOADER="$WINEDIR/bin/wine"
export WINESERVER="$WINEDIR/bin/wineserver"
@@ -925,13 +1081,6 @@ init_wine_ver () {
var_ld_library_path_update "$WINEDIR/lib"
export WINEDLLPATH="$WINEDIR/lib/wine"
if [[ ! -d "$WINEDIR/lib64/wine" ]] \
&& [[ -d "$WINEDIR/lib/wine/x86_64-unix" ]]
then
create_new_dir "$WINEDIR/lib64/"
try_force_link_dir "$WINEDIR/lib/wine/" "$WINEDIR/lib64/"
fi
if [[ -d "$WINEDIR/lib64" ]] \
&& [[ ! -L "$WINEDIR/lib64/wine" ]]
then
@@ -945,34 +1094,6 @@ init_wine_ver () {
export GST_PLUGIN_SYSTEM_PATH_1_0+=":$WINEDIR/lib64/gstreamer-1.0"
fi
fi
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
local cpcsp_proxy_ver
if [[ $WH_WINE_WOW64 == "1" ]]
then cpcsp_proxy_ver="$CPCSP_PROXY_WOW64_VER"
else cpcsp_proxy_ver="$CPCSP_PROXY_X86_64_VER"
fi
if [[ ! -f "$WINEDIR/cpcsp_proxy.ver" ]] \
|| ! grep -q "$cpcsp_proxy_ver" "$WINEDIR/cpcsp_proxy.ver"
then
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
unpack "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" "$WH_TMP_DIR"
if [[ $WH_WINE_WOW64 == "1" ]] ; then
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"* "$WINEDIR/lib64/wine/"
else
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"i386-* "$WINEDIR/lib/wine/"
if [[ -d "$WINEDIR/lib64" ]] ; then
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"x86_64-* "$WINEDIR/lib64/wine/"
fi
fi
try_remove_dir "$WH_TMP_DIR/$CPCSP_PROXY_NAME"
echo "$cpcsp_proxy_ver" > "$WINEDIR/cpcsp_proxy.ver"
fi
fi
else
# use system WINE
if [[ ! -f "/usr/bin/wine" ]] ; then
@@ -1220,16 +1341,18 @@ init_wineprefix () {
wait_wineserver
fi
local winver
if [[ "${WH_WINDOWS_VER,,}" == "xp64" ]]
then winver="xp 64"
else winver="$WH_WINDOWS_VER"
fi
if [[ -f "$WINEPREFIX/system.reg" ]] \
&& ! grep -iq "Microsoft Windows $WH_WINDOWS_VER" "$WINEPREFIX/system.reg"
&& ! grep -iq "Microsoft Windows ${winver}\"" "$WINEPREFIX/system.reg"
then
if [[ $(echo "$WH_WINDOWS_VER" | sed 's/.*/\L&/') == "xp" ]] \
&& [[ "$WINEARCH" != "win32" ]]
then export WH_WINDOWS_VER="xp64"
fi
"$WINELOADER" winecfg -v $(echo "win${WH_WINDOWS_VER}" | sed 's/.*/\L&/')
[[ "$winver" == "xp 64" ]] && winver="xp64"
"$WINELOADER" winecfg -v $(echo "win${winver}" | sed 's/.*/\L&/')
wait_wineserver
print_info "Windows версия изменена на win${WH_WINDOWS_VER}"
print_info "Windows версия изменена на win${winver}"
fi
if [[ $WH_USE_MESA_GL_OVERRIDE == "1" ]] \
@@ -1307,11 +1430,13 @@ init_wineprefix () {
# добавление ассоциаций файлов для запуска нативного приложения из wine
# пример переменной: WH_XDG_OPEN="txt doc pdf"
check_variables WH_XDG_OPEN "0"
check_variables WH_XDG_OPEN "default"
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
if [[ $WH_XDG_OPEN == "default" ]] ; then
: # пропускаем изменения в реестре
elif [[ $WH_XDG_OPEN != "0" ]] ; then
# проверяем на наличие запрещённых расширений
local forbidden_extensions="cpl dll exe lnk msi"
for ext in $WH_XDG_OPEN; do
@@ -1351,16 +1476,18 @@ init_wineprefix () {
# настраиваем префикс для работы с cpcsp_proxy
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
if ! grep -q "cpcsp_proxy.dll" "$WINEPREFIX/system.reg" \
|| [[ $REBOOT_PFX == "1" ]]
if [[ $UPDATE_CPCSP_PROXY == "1" ]] \
|| [[ $REBOOT_PFX == "1" ]] \
|| ! grep -q "cpcsp_proxy.dll" "$WINEPREFIX/system.reg"
then
print_info "Запускаем настройку cpcsp_proxy..."
CPCSP_PROXY_OK="0"
try_copy_wine_dll_to_pfx_64 "cpcsp_proxy.dll"
try_copy_wine_dll_to_pfx_32 "cpcsp_proxy.dll"
if [[ $WH_WINE_WOW64 == "1" ]] ; then
print_info "Запускаем настройку cpcsp_proxy версии $CPCSP_PROXY_WOW64_VER"
print_info "Запускаем регистрацию 64-битного cpcsp_proxy.dll..."
env WINEDEBUG="fixme-all" "$WINE" "C:\\windows\\system32\\regsvr32.exe" \
/s /n /i cpcsp_proxy.dll 2>&1 | tee "$WINEPREFIX/cpcsp_setup.log"
@@ -1371,6 +1498,8 @@ init_wineprefix () {
/s /n /i cpcsp_proxy.dll 2>&1 | tee -a "$WINEPREFIX/cpcsp_setup.log"
wait_wineserver
else
print_info "Запускаем настройку cpcsp_proxy версии $CPCSP_PROXY_X86_64_VER"
try_copy_wine_dll_to_pfx_64 "cpcsp_proxy_setup.exe"
try_copy_wine_dll_to_pfx_32 "cpcsp_proxy_setup.exe"
@@ -1388,8 +1517,14 @@ init_wineprefix () {
done
fi
if grep -q "failed to load /opt/cprocsp/" "$WINEPREFIX/cpcsp_setup.log" ; then
print_error "Проверьте правильность установки КриптоПро в системе.\n Инструкция: https://www.altlinux.org/CryptoPro"
if [[ ! -d "/opt/cprocsp/" ]] \
|| grep -q "failed to load /opt/cprocsp/" "$WINEPREFIX/cpcsp_setup.log"
then
print_error "Проверьте правильность установки КриптоПро в системе!
Официальный сайт КриптоПро: https://cryptopro.ru/products/csp
Инструкция установки КриптоПро в систему: https://www.altlinux.org/CryptoPro
"
else
CPCSP_PROXY_OK="1"
fi
@@ -1397,7 +1532,7 @@ init_wineprefix () {
if [[ $CPCSP_PROXY_OK == "1" ]]
then print_info "Настройка cpcsp_proxy успешно завершена."
else fatal "Ошибка во время настройки cpcsp_proxy."
else fatal
fi
fi
fi
@@ -1423,8 +1558,8 @@ init_wineprefix () {
fi
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_CPCSP_PROXY WH_USE_EXTRA_FONTS \
WINEUSERNAME
WINE_CPU_TOPOLOGY DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_EXTRA_FONTS \
WINEUSERNAME WH_USE_CPCSP_PROXY CPCSP_PROXY_X86_64_VER CPCSP_PROXY_WOW64_VER
do
update_last_conf_var "$var" "${!var}"
done
@@ -1604,6 +1739,32 @@ wine_run () {
echo -e "\n##### Лог WINE #####" | tee -a "$log_file"
$MANGOHUD_RUN "$WINELOADER" $wh_add_args "$win_file_exec" "$@" 2>&1 | tee -a "$log_file"
# Проверка размера лога и создание архива при превышении порога
local MAX_LOG_SIZE_MB=10
local MAX_LOG_SIZE_BYTES=$((MAX_LOG_SIZE_MB * 1048576))
local log_size_bytes log_size_mb
log_size_bytes=$(stat -c%s "$log_file" 2>/dev/null || echo 0)
log_size_mb=$((log_size_bytes / 1048576))
if (( log_size_bytes > MAX_LOG_SIZE_BYTES )); then
local timestamp archive_name archive_path
timestamp=$(date +"%d.%m.%Y-%H.%M")
archive_name="log_${PREFIX_NAME}_${win_file_name%.*}_${timestamp}.tar.xz"
archive_path="$log_dir/$archive_name"
print_warning "Файл лога больше ${MAX_LOG_SIZE_MB}Мб (размер: ${log_size_mb}Мб)."
print_warning "Создаем сжатый архив: $archive_name"
tar -cJf "$archive_path" -C "$log_dir" "$(basename "$log_file")"
if [[ -f "$archive_path" ]]; then
print_ok "Архив успешно создан: $archive_path"
rm -f "$log_file"
else
print_error "Не удалось создать архив: $archive_path"
fi
fi
else
$MANGOHUD_RUN "$WINELOADER" $wh_add_args "$win_file_exec" "$@"
fi
@@ -1612,9 +1773,47 @@ wine_run () {
cd "$DRIVE_C"
}
wine_debug_run () {
local winedbg_args win_file_exec win_file_path win_file_name wh_add_args
winedbg_args="$1"
[[ "$winedbg_args" == "--interactive" ]] && winedbg_args=""
shift
if [[ -f "$1" ]]; then
win_file_exec="$(readlink -f "$1")"
win_file_path="$(dirname "$win_file_exec")"
win_file_name="$(basename "$win_file_exec")"
else
fatal "Файл не найден: $1"
fi
shift
cd "$win_file_path"
local log_dir log_file
log_dir="$HOME/winehelper_backup_log"
log_file="$log_dir/winedbg_${PREFIX_NAME}_${win_file_name%.*}.log"
create_new_dir "$log_dir"
date > "$log_file"
echo -e "\n##### Основные переменные #####" | tee -a "$log_file"
env | grep -e "WH_" -e "WINE" -e "DXVK" -e "VKD3D" -e "LD_" \
| grep -v "ICON" | sort | tee -a "$log_file"
echo -e "\n##### WineHelper Debug Mode: winedbg $winedbg_args #####" | tee -a "$log_file"
print_info "Запуск отладчика (режим: $winedbg_args)"
print_warning "Лог отладки будет сохранен по пути: $log_file"
"$WINELOADER" winedbg $winedbg_args "$win_file_exec" "$@" 2>&1 | tee -a "$log_file"
wait_wineserver
cd "$DRIVE_C"
}
wine_run_install () {
print_info "Запускаем установку: $1."
case "$WH_INSTALL_MODE" in
"another") print_warning "Используется скрипт сторонний установки!" ;;
"manual") print_warning "Рекомендуется не менять пути для установки приложения!" ;;
"test") print_warning "Установка приложения из списка экспериментальных скриптов." ;;
esac
@@ -1643,6 +1842,9 @@ run_autoinstall () {
elif [[ -f "$WH_TESTINSTALL_DIR/$INSTALL_SCRIPT_NAME" ]] ; then
INSTALL_SCRIPT="$WH_TESTINSTALL_DIR/$INSTALL_SCRIPT_NAME"
WH_INSTALL_MODE="test"
elif [[ -f "$1" ]] ; then
INSTALL_SCRIPT="$1"
WH_INSTALL_MODE="another"
else
INSTALL_SCRIPT="0"
fi
@@ -2074,6 +2276,7 @@ remove_winehelper () {
# Удаление рабочих каталогов
try_remove_dir "$USER_WORK_PATH"
try_remove_dir "$HOME/winehelper_backup_log"
try_remove_dir "/var/tmp/winehelper-$USER"
# Удаление файлов меню
try_remove_dir "$WH_MENU_DIR"
@@ -2082,7 +2285,7 @@ remove_winehelper () {
# Удаление desktop-файлов
for desktop_file in "$(xdg-user-dir DESKTOP)"/*.desktop; do
if grep -q "Exec=env \"$RUN_SCRIPT\"" "$desktop_file"; then
if [[ -f "$desktop_file" ]] && grep -q "Exec=env \"$RUN_SCRIPT\"" "$desktop_file"; then
try_remove_file "$desktop_file"
fi
done
@@ -2170,9 +2373,9 @@ backup_prefix() {
local backup_base_dir backup_archive_name backup_dest_path temp_backup_dir temp_prefix_dir temp_users_dir
backup_base_dir="$HOME/winehelper_backup_log"
create_new_dir "$backup_base_dir"
backup_archive_name="backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M.%S).whpack"
backup_archive_name="backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M).whpack"
backup_dest_path="$backup_base_dir/$backup_archive_name"
temp_backup_dir="$WH_TMP_DIR/backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M.%S)"
temp_backup_dir="$WH_TMP_DIR/backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M)"
temp_prefix_dir="$temp_backup_dir/prefixes/$PREFIX_NAME"
temp_users_dir="$temp_prefix_dir/drive_c/users"
@@ -2205,13 +2408,11 @@ backup_prefix() {
return 1
fi
if [[ -n "$WH_WINE_USE" ]] \
&& [[ $WH_WINE_USE != system* ]] \
&& [[ -d "$WH_DIST_DIR/$WH_WINE_USE" ]]
then
if [[ $WH_WINE_USE != system* ]] ; then
init_wine_ver
print_info "Копирование используемой версии WINE: $WH_WINE_USE"
if cp -a "$WH_DIST_DIR/$WH_WINE_USE" "$temp_backup_dir/dist/"
if cp -aL "$WH_DIST_DIR/$WH_WINE_USE" "$temp_backup_dir/dist/"
then print_ok "WINE скопирован."
else fatal "Не удалось скопировать WINE."
fi
@@ -2221,7 +2422,7 @@ backup_prefix() {
echo "$PREFIX_NAME" > "$temp_backup_dir/prefix_name.whmeta"
print_info "Создание squashfs архива..."
if mksquashfs "$temp_backup_dir" "$backup_dest_path" -comp zstd ; then
if mksquashfs "$temp_backup_dir" "$backup_dest_path" -comp xz ; then
print_ok "Архив префикса $PREFIX_NAME успешно создан по пути:\n$backup_dest_path"
else
print_error "Не удалось создать архив префикса $PREFIX_NAME."
@@ -2285,7 +2486,7 @@ restore_prefix() {
if [[ -d "$WH_PREFIXES_DIR/$prefix_name" ]] ; then
try_remove_dir "$temp_extract_dir"
fatal "Префикс $prefix_name уже существует!\n Удалите его, если действительно желаете восстановить префикс заново.\n Команда для удаления: $SCRIPT_NAME remove-prefix $prefix_name"
fatal "Префикс $prefix_name уже существует!\n Удалите его перед восстановлением префикса заново.\n Команда для удаления: $SCRIPT_NAME remove-prefix $prefix_name"
fi
if [[ -d "$temp_extract_dir/prefixes/$prefix_name" ]] ; then
@@ -2306,16 +2507,17 @@ restore_prefix() {
try_remove_dir "$temp_extract_dir"
fatal "Не удалось восстановить префикс $prefix_name."
else
print_ok "Префикс $prefix_name восстановлен."
export WINEPREFIX="$WH_PREFIXES_DIR/$prefix_name"
if [[ -f "$WINEPREFIX/last.conf" ]] ; then
source "$WINEPREFIX/last.conf"
export REBOOT_PFX="1"
prepair_wine
if [[ -f "$WINEPREFIX/restore_pfx.sh" ]] ; then
source "$WINEPREFIX/restore_pfx.sh"
try_copy_file "$WINEPREFIX/restore_pfx.sh" "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
source "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
fi
fi
print_ok "Префикс $prefix_name восстановлен."
if [[ -f "$WH_PREFIXES_DIR/$prefix_name/desktop.list" ]] ; then
print_info "Восстановление ярлыков для префикса $prefix_name..."
@@ -2342,6 +2544,7 @@ restore_prefix() {
fi
try_remove_dir "$temp_extract_dir"
try_remove_file "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
print_ok "Полное восстановление префикса $prefix_name завершено."
return 0
}
@@ -2581,7 +2784,12 @@ wh_info () {
change-wine [версия] изменить версию Wine для текущего префикса
installed список установленных программ
run [программа] запуск программы (отладка)
run [программа] запуск программы
debug [флаги] [программа] запуск программы в отладчике winedbg
Флаги:
--auto, -a автоматический backtrace
--gdb, -g режим gdb
--interactive, -i интерактивный режим
remove-all удалить WineHelper и все связанные данные
create-prefix создать префикс
remove-prefix [имя_префикса] удалить префикс и все связанные данные
@@ -2651,6 +2859,22 @@ case "$arg1" in
wine_run "$WIN_FILE_EXEC" "$@"
fi
;;
debug)
case "$1" in
--gdb|-g) debug_flag="--gdb" ;;
--interactive|-i) debug_flag="--interactive" ;;
--auto|-a) debug_flag="--auto" ;;
*) fatal "Неизвестный флаг отладки!" ;;
esac
shift
if check_installed_programs check_only "$1" ; then
WIN_FILE_EXEC="$(readlink -f "$EXE_PATH")"
shift
prepair_wine
wine_debug_run $debug_flag "$WIN_FILE_EXEC" "$@"
fi
;;
*)
if [[ -f "$arg1" ]] ; then
WIN_FILE_EXEC="$(readlink -f "$arg1")"
@@ -2665,6 +2889,9 @@ case "$arg1" in
else
wine_run "$WIN_FILE_EXEC" "$@"
fi
elif [[ "${arg1,,}" =~ \.dll ]] ; then
prepair_wine
wine_run "$arg1"
else
print_error "Команды $arg1 не существует."
wh_info
+509 -2247
View File
File diff suppressed because it is too large. Load diff
+72
View File
@@ -0,0 +1,72 @@
"""
GUI пакет для winehelper.
Модульная структура:
- dialogs: Диалоги (выбор версий, настройки и т.д.)
- widgets: Кастомные виджеты
- services: Бизнес-логика (кэширование, установка и т.д.)
- parsers: Парсеры (sha256sum.list, winetricks output)
- utils: Утилиты (обработчики логов и т.д.)
"""
# Parsers
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
from winehelperqt.parsers.script_parser import ScriptParser
# Utils
from winehelperqt.utils.log_handler import LogProgressHandler, MultiLogHandler
from winehelperqt.utils.cache_service import CacheService, FileCacheService
# Services
from winehelperqt.services.winetricks_service import WinetricksCacheService
from winehelperqt.services.winetricks_service import WinetricksLogReader
from winehelperqt.services.prefix_manager import PrefixManager
from winehelperqt.services.process_handler import ProcessHandler
from winehelperqt.services.process_handler import CommandRunner
from winehelperqt.services.desktop_file_manager import DesktopFileManager
from winehelperqt.services.application_installer import ApplicationInstaller
from winehelperqt.services.script_manager import ScriptManager
from winehelperqt.services.script_manager import ScriptInfo
from winehelperqt.services.installation_manager import InstallationManager
from winehelperqt.services.app_launcher import AppLauncher
from winehelperqt.services.settings_manager import SettingsManager
# Widgets
from winehelperqt.widgets.icon_manager import IconManager
# Dialogs
from winehelperqt.dialogs.winetricks_manager import WinetricksManagerDialog
__all__ = [
# Parsers
'Sha256Parser',
'WinetricksOutputParser',
'ScriptParser',
# Utils
'LogProgressHandler',
'MultiLogHandler',
'CacheService',
'FileCacheService',
# Services
'WinetricksCacheService',
'WinetricksLogReader',
'PrefixManager',
'ProcessHandler',
'CommandRunner',
'DesktopFileManager',
'ApplicationInstaller',
'ScriptManager',
'ScriptInfo',
'InstallationManager',
'AppLauncher',
'SettingsManager',
# Widgets
'IconManager',
# Dialogs
'WinetricksManagerDialog',
]
+30
View File
@@ -0,0 +1,30 @@
"""
Диалоги для winehelper GUI.
"""
from winehelperqt.dialogs.base_version_dialog import (
BaseVersionSelectionDialog,
WineVersionSelectionDialogBase,
ComponentVersionSelectionDialogBase,
)
from winehelperqt.dialogs.winetricks_manager import WinetricksManagerDialog
from winehelperqt.dialogs.wine_version import WineVersionSelectionDialog
from winehelperqt.dialogs.component_version import ComponentVersionSelectionDialog
from winehelperqt.dialogs.create_prefix import CreatePrefixDialog
from winehelperqt.dialogs.file_associations import FileAssociationsDialog
from winehelperqt.dialogs.settings import AdditionalSettingsDialog, AppAdditionalSettingsDialog
__all__ = [
# Base classes
'BaseVersionSelectionDialog',
'WineVersionSelectionDialogBase',
'ComponentVersionSelectionDialogBase',
# Dialogs
'WinetricksManagerDialog',
'WineVersionSelectionDialog',
'ComponentVersionSelectionDialog',
'CreatePrefixDialog',
'FileAssociationsDialog',
'AdditionalSettingsDialog',
'AppAdditionalSettingsDialog',
]
+321
View File
@@ -0,0 +1,321 @@
"""
Базовые классы для диалогов выбора версий.
Устраняет дублирование кода между WineVersionSelectionDialog и ComponentVersionSelectionDialog.
"""
import os
import re
import shutil
import subprocess
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QScrollArea,
QTabWidget, QLineEdit, QPushButton, QLabel, QWidget, QMessageBox
)
from PyQt5.QtCore import Qt, pyqtSignal
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.utils.environment import EnvironmentConfig
class BaseVersionSelectionDialog(QDialog):
"""
Базовый класс для диалогов выбора версий.
Предоставляет общую функциональность:
- Поле поиска с фильтрацией
- Сетка кнопок с версиями
- Базовая логика выбора версии
"""
NUM_COLUMNS = 3
version_selected = pyqtSignal(str)
def __init__(self, title, parent=None, min_width=600, min_height=400):
"""
Инициализирует базовый диалог выбора версий.
"""
super().__init__(parent)
self.setWindowTitle(title)
self.setMinimumSize(min_width, min_height)
self.setModal(True)
self.selected_version = None
self.buttons = []
self.search_edit = None
self.main_layout = None
self._setup_ui()
def _get_sha256_path(self):
"""Возвращает путь к файлу sha256sum.list."""
data_path = EnvironmentConfig.DATA_PATH
if data_path:
return os.path.join(data_path, "sha256sum.list")
# Фолбэк: ищем рядом с этим файлом (для отладки)
return os.path.join(os.path.dirname(__file__), '../../sha256sum.list')
def _setup_ui(self):
"""Создает базовый UI диалога."""
self.main_layout = QVBoxLayout(self)
self.search_edit = QLineEdit()
self.search_edit.setPlaceholderText("Поиск версии...")
self.search_edit.textChanged.connect(self.filter_versions)
self.main_layout.addWidget(self.search_edit)
self._create_version_container()
def _create_version_container(self):
"""Создает контейнер для отображения версий."""
raise NotImplementedError("Подкласс должен реализовать _create_version_container")
def load_versions(self):
"""Загружает список версий."""
raise NotImplementedError("Подкласс должен реализовать load_versions")
def filter_versions(self):
"""Фильтрует версии по тексту поиска."""
search_text = self.search_edit.text().lower()
visible_buttons = []
for btn_widget in self.buttons:
if search_text in btn_widget.text().lower():
visible_buttons.append(btn_widget)
for btn_widget in self.buttons:
btn_widget.setVisible(False)
for i, btn_widget in enumerate(visible_buttons):
row, col = divmod(i, self.NUM_COLUMNS)
if hasattr(self, 'grid_layout'):
self.grid_layout.addWidget(btn_widget, row, col)
btn_widget.setVisible(True)
def on_version_selected(self, version_name):
"""Обрабатывает выбор версии."""
self.selected_version = version_name
self.version_selected.emit(version_name)
self.accept()
def get_selected_version(self):
"""Возвращает выбранную версию."""
return self.selected_version
@staticmethod
def create_grid_buttons(
grid_layout,
versions_list,
on_click_callback,
start_row=0,
num_columns=3,
is_download_tab=False,
version_filter=None
):
"""Создает сетку кнопок для версий."""
buttons = []
row, col = start_row, 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(lambda checked, vn=value_name: on_click_callback(vn))
if version_filter and not is_download_tab:
should_disable = version_filter(value_name)
if should_disable:
btn.setEnabled(False)
btn.setToolTip("Эта версия несовместима с выбранной архитектурой.")
grid_layout.addWidget(btn, row, col)
buttons.append(btn)
col += 1
if col >= num_columns:
col = 0
row += 1
return buttons
class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
"""
Базовый класс для диалога выбора версии Wine.
"""
def __init__(self, architecture, parent=None):
"""
Инициализирует диалог выбора версии Wine.
"""
self.architecture = architecture
self.wine_versions_data = {}
self.system_wine_display_name = "Системная версия"
self.selected_display_text = None
super().__init__(
title=f"Выбор версии Wine для {architecture} префикса",
parent=parent,
min_width=900,
min_height=500
)
def _create_version_container(self):
"""Создает контейнер с вкладками для версий Wine."""
self.main_tabs = QTabWidget()
self.main_layout.addWidget(self.main_tabs)
self.load_versions()
def load_versions(self):
"""Загружает и отображает версии Wine."""
self.main_tabs.clear()
loading_widget = QWidget()
loading_layout = QVBoxLayout(loading_widget)
status_label = QLabel("Загрузка, пожалуйста, подождите...")
status_label.setAlignment(Qt.AlignCenter)
loading_layout.addWidget(status_label)
self.main_tabs.addTab(loading_widget, "Загрузка...")
self.main_tabs.setEnabled(False)
self._parse_sha256_list()
self.populate_ui()
self.main_tabs.setEnabled(True)
def _parse_sha256_list(self):
"""Парсит sha256sum.list для получения списка версий Wine."""
sha256_path = self._get_sha256_path()
self.wine_versions_data = Sha256Parser.parse_wine_versions(sha256_path)
if not self.wine_versions_data:
QMessageBox.warning(
self,
"Ошибка",
f"Файл с версиями не найден или пуст:\n{sha256_path}"
)
def _get_installed_versions(self):
"""Возвращает список локально установленных версий Wine."""
dist_path = EnvironmentConfig.WH_DIST_DIR
if not dist_path:
return []
return Sha256Parser.get_installed_versions(dist_path)
def _get_system_wine_version(self):
"""Получает версию системного Wine."""
if shutil.which('wine'):
try:
result = subprocess.run(
['wine', '--version'],
capture_output=True,
text=True,
check=True,
encoding='utf-8'
)
return result.stdout.strip()
except (FileNotFoundError, subprocess.CalledProcessError) as e:
print(f"Не удалось получить версию системного wine: {e}")
return "Системная версия"
def _is_version_compatible(self, version_name):
"""Проверяет совместимость версии с выбранной архитектурой."""
is_win64_prefix = self.architecture == "win64"
is_win32_prefix = self.architecture == "win32"
re_32bit_version = re.compile(r'i[3-6]86|i586')
if is_win64_prefix and re_32bit_version.search(version_name):
return False
if is_win32_prefix:
if version_name == "system" or 'wow64' in version_name:
return False
return True
def populate_ui(self):
"""Заполняет UI версиями Wine."""
raise NotImplementedError("Подкласс должен реализовать populate_ui")
class ComponentVersionSelectionDialogBase(BaseVersionSelectionDialog):
"""
Базовый класс для диалога выбора версии компонента (DXVK, VKD3D).
"""
def __init__(
self,
component_group,
title,
parent=None,
add_extra_options=True
):
"""
Инициализирует диалог выбора версии компонента.
"""
self.component_group = component_group
self.versions_data = []
self.add_extra_options = add_extra_options
super().__init__(
title=title,
parent=parent,
min_width=600,
min_height=400
)
def _create_version_container(self):
"""Создает контейнер для версий компонента."""
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.main_layout.addWidget(self.scroll_area)
scroll_content = QWidget()
self.scroll_area.setWidget(scroll_content)
self.grid_layout = QGridLayout(scroll_content)
self.grid_layout.setAlignment(Qt.AlignTop)
self.load_versions(start_row=0)
if self.add_extra_options:
self._create_action_buttons()
def _create_action_buttons(self):
"""Создает кнопки действий (Удалить, Отмена)."""
button_layout = QHBoxLayout()
uninstall_btn = QPushButton("Удалить из префикса")
uninstall_btn.setToolTip("Удаляет текущую установленную версию компонента из префикса.")
uninstall_btn.clicked.connect(lambda: self.on_version_selected("none"))
button_layout.addWidget(uninstall_btn)
button_layout.addStretch(1)
cancel_button = QPushButton("Отмена")
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(cancel_button)
self.main_layout.addLayout(button_layout)
def load_versions(self, start_row=0):
"""Загружает и отображает версии компонента."""
self._parse_sha256_list()
self.populate_ui(start_row)
def _parse_sha256_list(self):
"""Парсит sha256sum.list для получения списка версий компонента."""
sha256_path = self._get_sha256_path()
self.versions_data = Sha256Parser.parse_component_versions(
sha256_path,
self.component_group
)
def populate_ui(self, start_row):
"""Заполняет UI версиями компонента."""
raise NotImplementedError("Подкласс должен реализовать populate_ui")
+68
View File
@@ -0,0 +1,68 @@
"""
Диалог для выбора версии компонента (DXVK, VKD3D).
"""
from functools import partial
from PyQt5.QtWidgets import (
QPushButton
)
from winehelperqt.dialogs.base_version_dialog import ComponentVersionSelectionDialogBase
class ComponentVersionSelectionDialog(ComponentVersionSelectionDialogBase):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
def __init__(self, component_group, title, parent=None, add_extra_options=True):
"""
Инициализирует диалог выбора версии компонента.
Args:
component_group: Группа компонента (dxvk, vkd3d)
title: Заголовок диалога
parent: Родительский виджет
add_extra_options: Добавлять ли кнопки действий
"""
super().__init__(
component_group=component_group,
title=title,
parent=parent,
add_extra_options=add_extra_options
)
def populate_ui(self, start_row):
"""Заполняет UI кнопками версий."""
versions = sorted(self.versions_data, reverse=True)
row, col = start_row, 0
for version_name in versions:
btn = QPushButton(version_name)
btn.clicked.connect(partial(self.on_version_selected, version_name))
self.grid_layout.addWidget(btn, row, col)
self.buttons.append(btn)
col += 1
if col >= self.NUM_COLUMNS:
col = 0
row += 1
def filter_versions(self):
"""Фильтрует видимость кнопок версий и перестраивает сетку."""
search_text = self.search_edit.text().lower()
visible_buttons = []
for btn_widget in self.buttons:
if search_text in btn_widget.text().lower():
visible_buttons.append(btn_widget)
for btn_widget in self.buttons:
btn_widget.setVisible(False)
for i, btn_widget in enumerate(visible_buttons):
row, col = divmod(i, self.NUM_COLUMNS)
self.grid_layout.addWidget(btn_widget, row, col)
btn_widget.setVisible(True)
def on_version_selected(self, version_name):
"""Обрабатывает выбор версии."""
self.selected_version = version_name
self.accept()
+202
View File
@@ -0,0 +1,202 @@
"""
Диалог для создания нового префикса.
"""
import os
import re
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QPushButton,
QLabel, QLineEdit, QRadioButton, QComboBox, QMessageBox, QWidget
)
from PyQt5.QtGui import QFont
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.dialogs.wine_version import WineVersionSelectionDialog
from winehelperqt.utils.environment import EnvironmentConfig
class CreatePrefixDialog(QDialog):
"""Диалог для создания нового префикса."""
def __init__(self, parent=None):
super().__init__(parent)
self.parent_gui = parent
self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(680, 250)
self.setModal(True)
self.prefix_name = None
self.wine_arch = None
self.base_pfx = None
self.prepared_prefixes = {}
self.selected_wine_version_value = None
self.selected_wine_version_display = None
layout = QVBoxLayout(self)
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)
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)
form_layout.addRow("<b>Разрядность:</b>", arch_widget)
self.prefix_template_selector = QComboBox()
self.prefix_template_selector.setToolTip("Выберите шаблон для создания префикса.")
form_layout.addRow("<b>Шаблон префикса:</b>", self.prefix_template_selector)
self.wine_version_edit = QLineEdit()
self.wine_version_edit.setReadOnly(True)
self.wine_version_edit.setPlaceholderText("Версия не выбрана")
select_version_button = QPushButton("Выбрать версию...")
select_version_button.clicked.connect(self.open_wine_version_dialog)
version_layout = QHBoxLayout()
version_layout.addWidget(self.wine_version_edit)
version_layout.addWidget(select_version_button)
form_layout.addRow("<b>Версия Wine:</b>", version_layout)
layout.addLayout(form_layout)
button_layout = QHBoxLayout()
self.create_button = QPushButton("Создать")
self.create_button.setFont(QFont('Arial', 11, QFont.Bold))
self.create_button.setStyleSheet("background-color: #0078d7; color: white;")
self.create_button.setEnabled(False)
self.create_button.clicked.connect(self.accept_creation)
cancel_button = QPushButton("Отмена")
cancel_button.clicked.connect(self.reject)
button_layout.addStretch()
button_layout.addWidget(self.create_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.arch_win32_radio.toggled.connect(self.on_architecture_changed)
self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
self.wine_version_edit.textChanged.connect(self.update_create_button_state)
self._load_prepared_prefixes()
self.on_architecture_changed()
def _load_prepared_prefixes(self):
"""Загружает и парсит шаблоны префиксов из sha256sum.list."""
self.prepared_prefixes = {'win32': [], 'win64': []}
data_path = EnvironmentConfig.DATA_PATH
if not data_path:
QMessageBox.warning(self, "Ошибка", "Переменная окружения DATA_PATH не установлена.")
return
sha256_file = os.path.join(data_path, "sha256sum.list")
if not os.path.exists(sha256_file):
QMessageBox.warning(self, "Ошибка", f"Файл с описаниями префиксов не найден: {sha256_file}")
return
prepared = Sha256Parser.parse_prepared_prefixes(sha256_file)
self.prepared_prefixes = prepared
def open_wine_version_dialog(self):
"""Открывает диалог выбора версии Wine."""
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
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
def on_architecture_changed(self):
"""Обновляет список шаблонов и сбрасывает выбор версии Wine."""
self.wine_version_edit.clear()
self.selected_wine_version_value = None
self.prefix_template_selector.blockSignals(True)
self.prefix_template_selector.clear()
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
templates = self.prepared_prefixes.get(architecture, [])
for value, description in templates:
display_name = "Чистый префикс" if value == "none" else value
self.prefix_template_selector.addItem(display_name, userData={'value': value, 'tooltip': description})
self.prefix_template_selector.blockSignals(False)
self.prefix_template_selector.currentIndexChanged.connect(
lambda i: self.prefix_template_selector.setToolTip(
self.prefix_template_selector.itemData(i)['tooltip']
)
)
self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(0)['tooltip'])
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()
)
def accept_creation(self):
"""Валидирует данные, сохраняет их и закрывает диалог с успехом."""
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(EnvironmentConfig.USER_WORK_PATH, "prefixes", prefix_name)
if os.path.exists(prefix_path):
QMessageBox.warning(self, "Ошибка", f"Префикс с именем '{prefix_name}' уже существует.")
return
self.prefix_name = prefix_name
self.wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64"
current_index = self.prefix_template_selector.currentIndex()
self.base_pfx = self.prefix_template_selector.itemData(current_index)['value']
self.selected_wine_version_display = self.wine_version_edit.text()
self.accept()
+75
View File
@@ -0,0 +1,75 @@
"""
Диалог для управления ассоциациями файлов (WH_XDG_OPEN).
"""
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QMessageBox
)
from PyQt5.QtCore import Qt
class FileAssociationsDialog(QDialog):
"""Диалог для управления ассоциациями файлов."""
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()
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)
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()
+356
View File
@@ -0,0 +1,356 @@
"""
Диалоги для дополнительных настроек префикса и приложений.
"""
import os
import re
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton,
QLabel, QCheckBox, QComboBox, QMessageBox, QSizePolicy
)
from PyQt5.QtCore import Qt
class AdditionalSettingsDialog(QDialog):
"""Диалог для дополнительных настроек префикса (ESync, FSync, DXVK, VKD3D)."""
def __init__(self, prefix_name, parent=None):
super().__init__(parent)
self.parent_gui = parent
self.prefix_name = prefix_name
self.setWindowTitle("Дополнительные настройки")
self.setMinimumWidth(400)
self.setModal(True)
layout = QVBoxLayout(self)
grid_layout = QGridLayout()
grid_layout.setSpacing(10)
# ESync/FSync
self.esync_button = QPushButton("ESync")
self.esync_button.setCheckable(True)
self.esync_button.setMinimumHeight(32)
self.esync_button.setToolTip("Включить/выключить Eventfd-based synchronization.")
self.esync_button.clicked.connect(
lambda checked: self._toggle_sync_option("WINEESYNC", checked, self.esync_button)
)
self.esync_button.toggled.connect(
lambda checked, btn=self.esync_button: self._update_button_text(btn, "ESync", checked)
)
self.esync_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
grid_layout.addWidget(self.esync_button, 0, 0)
self.fsync_button = QPushButton("FSync")
self.fsync_button.setMinimumHeight(32)
self.fsync_button.setCheckable(True)
self.fsync_button.setToolTip("Включить/выключить Futex-based synchronization.")
self.fsync_button.clicked.connect(
lambda checked: self._toggle_sync_option("WINEFSYNC", checked, self.fsync_button)
)
self.fsync_button.toggled.connect(
lambda checked, btn=self.fsync_button: self._update_button_text(btn, "FSync", checked)
)
self.fsync_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
grid_layout.addWidget(self.fsync_button, 0, 1)
self.esync_button.setFocusPolicy(Qt.NoFocus)
self.fsync_button.setFocusPolicy(Qt.NoFocus)
# DXVK/VKD3D
self.dxvk_manage_button = QPushButton("Управление DXVK")
self.dxvk_manage_button.setMinimumHeight(32)
self.dxvk_manage_button.clicked.connect(
lambda: self._open_component_manager('dxvk')
)
self.dxvk_manage_button.setToolTip("Установка или удаление определенной версии DXVK в префиксе.")
grid_layout.addWidget(self.dxvk_manage_button, 1, 0)
self.vkd3d_manage_button = QPushButton("Управление VKD3D")
self.vkd3d_manage_button.setMinimumHeight(32)
self.vkd3d_manage_button.clicked.connect(
lambda: self._open_component_manager('vkd3d-proton')
)
self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.")
grid_layout.addWidget(self.vkd3d_manage_button, 1, 1)
self.dxvk_manage_button.setFocusPolicy(Qt.NoFocus)
self.vkd3d_manage_button.setFocusPolicy(Qt.NoFocus)
# Ассоциации файлов
self.file_associations_button = QPushButton("Ассоциации файлов")
self.file_associations_button.setMinimumHeight(32)
self.file_associations_button.clicked.connect(self.parent_gui.open_file_associations_manager)
self.file_associations_button.setToolTip(
"Настройка открытия определенных типов файлов с помощью нативных приложений Linux."
)
grid_layout.addWidget(self.file_associations_button, 2, 0)
self.file_associations_button.setFocusPolicy(Qt.NoFocus)
# CPCSP_PROXY
self.cpcsp_proxy_button = QPushButton("CPCSP_PROXY")
self.cpcsp_proxy_button.setMinimumHeight(32)
self.cpcsp_proxy_button.setCheckable(True)
self.cpcsp_proxy_button.setToolTip("Включить/выключить использование CPCSP_PROXY.")
self.cpcsp_proxy_button.clicked.connect(
lambda checked: self._toggle_sync_option("WH_USE_CPCSP_PROXY", checked, self.cpcsp_proxy_button)
)
self.cpcsp_proxy_button.toggled.connect(
lambda checked, btn=self.cpcsp_proxy_button: self._update_button_text(btn, "CPCSP_PROXY", checked)
)
self.cpcsp_proxy_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
grid_layout.addWidget(self.cpcsp_proxy_button, 2, 1)
self.cpcsp_proxy_button.setFocusPolicy(Qt.NoFocus)
layout.addLayout(grid_layout)
self.close_button = QPushButton("Закрыть")
self.close_button.setMinimumHeight(32)
self.close_button.clicked.connect(self.accept)
self.close_button.setFocusPolicy(Qt.NoFocus)
layout.addWidget(self.close_button, alignment=Qt.AlignRight)
self.update_button_states()
def _toggle_sync_option(self, option_name, is_checked, button):
"""Обновляет опцию синхронизации."""
self.parent_gui.update_sync_option(option_name, is_checked)
def _update_button_text(self, button, base_text, is_checked):
"""Обновляет текст кнопки, добавляя или убирая галочку."""
if is_checked:
button.setText(f"✓ {base_text}")
else:
button.setText(base_text)
def _open_component_manager(self, component_name):
"""Открывает менеджер компонентов."""
self.parent_gui.open_component_version_manager(component_name)
def update_button_states(self):
"""Обновляет состояние кнопок на основе данных из last.conf."""
esync_enabled = self.parent_gui._get_prefix_component_version(
self.prefix_name, "WINEESYNC"
) == "1"
fsync_enabled = self.parent_gui._get_prefix_component_version(
self.prefix_name, "WINEFSYNC"
) == "1"
cpcsp_proxy_enabled = self.parent_gui._get_prefix_component_version(
self.prefix_name, "WH_USE_CPCSP_PROXY"
) == "1"
self.esync_button.blockSignals(True)
self.fsync_button.blockSignals(True)
self.cpcsp_proxy_button.blockSignals(True)
self.esync_button.setChecked(esync_enabled)
self.fsync_button.setChecked(fsync_enabled)
self.cpcsp_proxy_button.setChecked(cpcsp_proxy_enabled)
self._update_button_text(self.esync_button, "ESync", esync_enabled)
self._update_button_text(self.fsync_button, "FSync", fsync_enabled)
self._update_button_text(self.cpcsp_proxy_button, "CPCSP_PROXY", cpcsp_proxy_enabled)
self.esync_button.blockSignals(False)
self.fsync_button.blockSignals(False)
self.cpcsp_proxy_button.blockSignals(False)
class AppAdditionalSettingsDialog(QDialog):
"""Диалог для дополнительных настроек приложения из .whdb файла."""
def __init__(self, exe_path, parent=None):
super().__init__(parent)
self.parent_gui = parent
self.exe_path = exe_path
self.whdb_path = f"{exe_path}.whdb"
self.settings = {
'WH_MAIN_DECORATED': 'default',
'WH_VIRTUAL_DESKTOP': 'default',
'WH_FONT_SMOOTHING': '0',
'WH_FONT_MSS_REPLACE': '0',
'WINE_D3D_CONFIG': ''
}
self.setWindowTitle("Дополнительные настройки приложения")
self.setMinimumSize(380, 200)
self.setModal(True)
layout = QVBoxLayout(self)
checkboxes_layout = QHBoxLayout()
checkboxes_layout.setSpacing(20)
left_column = QVBoxLayout()
left_column.setSpacing(10)
self.wh_main_decorated_checkbox = QCheckBox("MAIN_DECORATED")
self.wh_main_decorated_checkbox.setToolTip("Управляет декорациями окон главного приложения")
left_column.addWidget(self.wh_main_decorated_checkbox)
self.wh_font_smoothing_checkbox = QCheckBox("FONT_SMOOTHING")
self.wh_font_smoothing_checkbox.setToolTip("Сглаживание шрифтов")
left_column.addWidget(self.wh_font_smoothing_checkbox)
right_column = QVBoxLayout()
right_column.setSpacing(10)
self.wh_virtual_desktop_checkbox = QCheckBox("VIRTUAL_DESKTOP")
self.wh_virtual_desktop_checkbox.setToolTip("Виртуальный рабочий стол для приложения")
right_column.addWidget(self.wh_virtual_desktop_checkbox)
self.wh_font_mss_replace_checkbox = QCheckBox("FONT_MSS_REPLACE")
self.wh_font_mss_replace_checkbox.setToolTip("Замена шрифтов MSS")
right_column.addWidget(self.wh_font_mss_replace_checkbox)
checkboxes_layout.addLayout(left_column)
checkboxes_layout.addLayout(right_column)
layout.addLayout(checkboxes_layout)
layout.addSpacing(15)
d3d_layout = QHBoxLayout()
d3d_label = QLabel("WINE_D3D_CONFIG:")
d3d_layout.addWidget(d3d_label)
self.wine_d3d_config_combo = QComboBox()
self.wine_d3d_config_combo.setMinimumWidth(250)
self.wine_d3d_config_combo.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
self.wine_d3d_config_combo.addItem("Не задано", "")
self.wine_d3d_config_combo.addItem("renderer=vulkan", "renderer=vulkan")
self.wine_d3d_config_combo.addItem("renderer=gdi", "renderer=gdi")
self.wine_d3d_config_combo.addItem("renderer=gl", "renderer=gl")
self.wine_d3d_config_combo.setToolTip("Выберите режим рендеринга Direct3D")
d3d_layout.addWidget(self.wine_d3d_config_combo, stretch=1)
d3d_layout.addStretch()
layout.addLayout(d3d_layout)
layout.addSpacing(20)
button_layout = QHBoxLayout()
button_layout.addStretch()
self.save_button = QPushButton("Сохранить")
self.save_button.setMinimumHeight(32)
self.save_button.clicked.connect(self._save_settings)
self.save_button.setFocusPolicy(Qt.NoFocus)
button_layout.addWidget(self.save_button)
self.close_button = QPushButton("Закрыть")
self.close_button.setMinimumHeight(32)
self.close_button.clicked.connect(self.accept)
self.close_button.setFocusPolicy(Qt.NoFocus)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
self._load_settings()
def _load_settings(self):
"""Загружает настройки из .whdb файла."""
if not os.path.exists(self.whdb_path):
self._update_ui_from_settings()
return
try:
with open(self.whdb_path, 'r', encoding='utf-8') as f:
content = f.read()
for line in content.split('\n'):
line = line.strip()
if not line or line.startswith('#'):
continue
match = re.match(r'export\s+(\w+)="([^"]*)"', line)
if match:
var_name = match.group(1)
var_value = match.group(2)
if var_name in self.settings:
self.settings[var_name] = var_value
self._update_ui_from_settings()
except Exception as e:
QMessageBox.warning(
self, "Ошибка",
f"Не удалось прочитать файл настроек:\n{self.whdb_path}\n\nОшибка: {e}"
)
self._update_ui_from_settings()
def _update_ui_from_settings(self):
"""Обновляет элементы управления на основе загруженных настроек."""
self.wh_main_decorated_checkbox.setChecked(
self.settings.get('WH_MAIN_DECORATED') == '1'
)
self.wh_virtual_desktop_checkbox.setChecked(
self.settings.get('WH_VIRTUAL_DESKTOP') == '1' or
(self.settings.get('WH_VIRTUAL_DESKTOP', '').lower() not in ('0', 'default', ''))
)
self.wh_font_smoothing_checkbox.setChecked(
self.settings.get('WH_FONT_SMOOTHING') == '1'
)
self.wh_font_mss_replace_checkbox.setChecked(
self.settings.get('WH_FONT_MSS_REPLACE') == '1'
)
d3d_value = self.settings.get('WINE_D3D_CONFIG', '')
index = self.wine_d3d_config_combo.findData(d3d_value)
if index >= 0:
self.wine_d3d_config_combo.setCurrentIndex(index)
else:
self.wine_d3d_config_combo.setCurrentIndex(0)
def _save_settings(self):
"""Сохраняет настройки в .whdb файл."""
try:
existing_lines = []
if os.path.exists(self.whdb_path):
with open(self.whdb_path, 'r', encoding='utf-8') as f:
existing_lines = f.readlines()
updated_vars = set()
new_lines = []
for line in existing_lines:
stripped = line.strip()
match = re.match(r'export\s+(\w+)="([^"]*)"', stripped)
if match:
var_name = match.group(1)
if var_name in self.settings:
new_lines.append(self._create_export_line(var_name))
updated_vars.add(var_name)
continue
new_lines.append(line)
for var_name in self.settings:
if var_name not in updated_vars:
new_lines.append(self._create_export_line(var_name))
updated_vars.add(var_name)
with open(self.whdb_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
self.accept()
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить настройки:\n{e}")
def _create_export_line(self, var_name):
"""Создает строку экспорта переменной."""
if var_name == 'WINE_D3D_CONFIG':
value = self.wine_d3d_config_combo.currentData()
elif var_name == 'WH_MAIN_DECORATED':
value = '1' if self.wh_main_decorated_checkbox.isChecked() else 'default'
elif var_name == 'WH_VIRTUAL_DESKTOP':
value = '1' if self.wh_virtual_desktop_checkbox.isChecked() else 'default'
elif var_name == 'WH_FONT_SMOOTHING':
value = '1' if self.wh_font_smoothing_checkbox.isChecked() else '0'
elif var_name == 'WH_FONT_MSS_REPLACE':
value = '1' if self.wh_font_mss_replace_checkbox.isChecked() else '0'
else:
value = ''
return f'export {var_name}="{value}"\n'
+219
View File
@@ -0,0 +1,219 @@
"""
Диалог для выбора версии Wine с группировкой.
"""
import re
from functools import partial
from PyQt5.QtWidgets import (
QGridLayout, QScrollArea, QTabWidget, QPushButton, QLabel, QWidget, QVBoxLayout
)
from PyQt5.QtCore import Qt
from winehelperqt.dialogs.base_version_dialog import WineVersionSelectionDialogBase
class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
"""Диалог для выбора версии Wine с группировкой."""
def __init__(self, architecture, parent=None):
"""
Инициализирует диалог выбора версии Wine.
Args:
architecture: Архитектура префикса (win32/win64)
parent: Родительский виджет
"""
self.main_tabs = None
self.installed_grid_layout = None
self.installed_buttons = []
self.download_buttons = []
super().__init__(architecture=architecture, parent=parent)
def _create_version_container(self):
"""Создает контейнер с вкладками для версий Wine."""
self.main_tabs = QTabWidget()
self.main_layout.addWidget(self.main_tabs)
self.load_versions()
def populate_ui(self):
"""Заполняет UI версиями Wine."""
self.main_tabs.clear()
# Вкладка "Установленные версии Wine"
installed_tab = QWidget()
self.main_tabs.addTab(installed_tab, "Установленные версии Wine")
installed_layout = QVBoxLayout(installed_tab)
installed_scroll_area = QScrollArea()
installed_scroll_area.setWidgetResizable(True)
installed_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
installed_layout.addWidget(installed_scroll_area)
installed_content = QWidget()
installed_scroll_area.setWidget(installed_content)
self.installed_grid_layout = QGridLayout(installed_content)
self.installed_grid_layout.setAlignment(Qt.AlignTop)
installed_versions_for_grid = []
# Системная версия
self.system_wine_display_name = self._get_system_wine_version()
installed_versions_for_grid.append((self.system_wine_display_name, "system"))
# Локально установленные версии
local_versions = self._get_installed_versions()
installed_versions_for_grid.extend(local_versions)
self._create_grid_buttons_for_wine(self.installed_grid_layout, installed_versions_for_grid, is_download_tab=False)
# Вкладки с версиями для скачивания
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.main_tabs.addTab(error_widget, "Ошибка")
self.filter_versions()
return
# Фильтрация и сортировка групп в зависимости от архитектуры
is_win64 = self.architecture == "win64"
if is_win64:
allowed_groups_for_arch = {"WINE AMD64", "WINE WOW64"}
tab_order = ["WINE WOW64", "WINE AMD64"]
display_names = {
"WINE WOW64": "Загрузить wine-wow64",
"WINE AMD64": "Загрузить wine-amd64"
}
else:
allowed_groups_for_arch = {"WINE I586", "WINE AMD64"}
tab_order = ["WINE I586", "WINE AMD64"]
display_names = {
"WINE I586": "Загрузить wine-i586",
"WINE AMD64": "Загрузить wine-amd64"
}
available_keys = [
key for key in self.wine_versions_data.keys()
if key in allowed_groups_for_arch
]
group_keys = sorted(available_keys, key=lambda k: tab_order.index(k))
for key in group_keys:
versions = self.wine_versions_data.get(key, [])
if not versions:
continue
display_name = display_names.get(key, key)
self._create_version_tab_directly(display_name, sorted(versions, reverse=True))
self.filter_versions()
def _create_version_tab_directly(self, title, versions_list):
"""Создает вкладку с сеткой кнопок и добавляет её напрямую в main_tabs."""
tab_page = QWidget()
tab_layout = QVBoxLayout(tab_page)
tab_layout.setContentsMargins(5, 5, 5, 5)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
tab_layout.addWidget(scroll_area)
scroll_content = QWidget()
scroll_area.setWidget(scroll_content)
grid_layout = QGridLayout(scroll_content)
grid_layout.setAlignment(Qt.AlignTop)
self._create_grid_buttons_for_wine(grid_layout, versions_list, is_download_tab=True)
self.main_tabs.addTab(tab_page, title)
def _create_grid_buttons_for_wine(self, grid_layout, versions_list, is_download_tab=False):
"""Создает кнопки для версий в сетке."""
is_win64_prefix = self.architecture == "win64"
is_win32_prefix = self.architecture == "win32"
re_32bit_version = re.compile(r'i[3-6]86|i586')
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, display_name))
if value_name == "system":
btn.setText(f"{display_name} (Системный)")
btn.setToolTip("Системная версия Wine, установленная в вашей ОС.")
# Отключение 32-битных версий для 64-битного префикса
if not is_download_tab and is_win64_prefix:
if re_32bit_version.search(value_name):
btn.setEnabled(False)
btn.setToolTip("Эта 32-битная версия Wine несовместима с 64-битным префиксом.")
# Отключение 64-битных версий для 32-битного префикса
if not is_download_tab and is_win32_prefix:
if value_name == "system" or 'wow64' in value_name:
btn.setEnabled(False)
btn.setToolTip("Эта 64-битная версия Wine несовместима с 32-битным префиксом.")
grid_layout.addWidget(btn, row, col)
# Сохраняем для фильтрации
if is_download_tab:
self.download_buttons.append(btn)
else:
self.installed_buttons.append(btn)
col += 1
if col >= self.NUM_COLUMNS:
col = 0
row += 1
def filter_versions(self):
"""Фильтрует видимость кнопок версий на основе текста поиска."""
search_text = self.search_edit.text().lower()
# Фильтр для вкладки "Установленные"
if hasattr(self, 'installed_grid_layout'):
self._filter_grid(self.installed_grid_layout, search_text)
# Фильтр для вкладок с версиями (теперь напрямую в main_tabs)
# Пропускаем первую вкладку "Установленные" (индекс 0)
for i in range(1, self.main_tabs.count()):
tab_widget = self.main_tabs.widget(i)
# Ищем QGridLayout внутри вкладки
scroll_area = tab_widget.findChild(QScrollArea)
if scroll_area:
scroll_content = scroll_area.widget()
if scroll_content:
grid_layout = scroll_content.layout()
if grid_layout:
any_visible = self._filter_grid(grid_layout, search_text)
self.main_tabs.setTabEnabled(i, any_visible)
def _filter_grid(self, grid_layout, search_text):
"""Helper-функция для фильтрации кнопок в одной сетке."""
any_visible_in_grid = False
if not grid_layout:
return False
for j in range(grid_layout.count()):
item = grid_layout.itemAt(j)
if item:
btn_widget = item.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_grid = True
return any_visible_in_grid
def on_version_selected(self, version_name, display_name=None):
"""Обрабатывает выбор версии Wine."""
self.selected_version = version_name
self.selected_display_text = display_name or version_name
self.accept()
+599
View File
@@ -0,0 +1,599 @@
"""
Диалог для управления компонентами Winetricks.
"""
import os
import shlex
import hashlib
import json
import time
from functools import partial
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget,
QTextEdit, QListWidget, QListWidgetItem, QMessageBox, QLineEdit, QWidget
)
from PyQt5.QtCore import Qt, QProcess, QTimer, QProcessEnvironment, pyqtSignal
from PyQt5.QtGui import QTextCursor, QTextCharFormat, QFont
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
from winehelperqt.services.winetricks_service import WinetricksLogReader
from winehelperqt.utils.environment import EnvironmentConfig
class NoAutoScrollListWidget(QListWidget):
"""QListWidget с отключённой автопрокруткой к текущему элементу."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._scroll_enabled = False # Флаг для ручной прокрутки
def scrollTo(self, index, hint=QListWidget.EnsureVisible):
"""Переопределяем, чтобы игнорировать автоматическую прокрутку."""
if self._scroll_enabled:
super().scrollTo(index, hint)
def enable_scroll(self):
"""Включает прокрутку для ручного управления."""
self._scroll_enabled = True
def disable_scroll(self):
"""Отключает прокрутку."""
self._scroll_enabled = False
class WinetricksManagerDialog(QDialog):
"""Диалог для управления компонентами Winetricks."""
INFO_TEXT = (
"Компоненты можно только установить либо переустановить.\n"
"Удаление компонентов не реализовано в Winetricks.\n"
"Для установки нового компонента: Поставьте галочку и нажмите «Применить».\n"
"Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»."
)
installation_complete = pyqtSignal()
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
self.user_cancelled = False
self.processes = {}
self.category_statuses = {}
self.previous_tab_widget = None
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks")
os.makedirs(self.cache_dir, exist_ok=True)
self.is_reloading_after_cache_clear = False
self.cache_clear_process = None
self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
self.setMinimumSize(800, 500)
main_layout = QVBoxLayout(self)
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
self.categories = {
"Библиотеки": "dlls",
"Шрифты": "fonts",
"Настройки": "settings"
}
self.list_widgets = {}
self.search_edits = {}
for display_name, internal_name in self.categories.items():
list_widget, search_edit = self._create_category_tab(display_name)
self.list_widgets[internal_name] = list_widget
self.search_edits[internal_name] = search_edit
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
self.log_output.setMaximumHeight(150)
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
button_layout = QHBoxLayout()
self.clear_cache_button = QPushButton("Очистить кеш")
self.clear_cache_button.setStyleSheet("background-color: red; color: white;")
self.clear_cache_button.clicked.connect(self.clear_winetricks_cache)
button_layout.addWidget(self.clear_cache_button)
button_layout.addStretch(1)
self.apply_button = QPushButton("Применить")
self.apply_button.setEnabled(False)
self.apply_button.clicked.connect(self.apply_changes)
button_layout.addWidget(self.apply_button)
self.reinstall_button = QPushButton("Переустановить")
self.reinstall_button.setEnabled(False)
self.reinstall_button.clicked.connect(self.reinstall_selected)
button_layout.addWidget(self.reinstall_button)
self.close_button = QPushButton("Закрыть")
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.close_button)
main_layout.addLayout(button_layout)
self.tabs.currentChanged.connect(self.on_tab_switched)
self.load_all_categories()
self.previous_tab_widget = self.tabs.currentWidget()
def on_tab_switched(self, index):
"""Обрабатывает переключение вкладок."""
if self.previous_tab_widget:
search_edit = self.previous_tab_widget.findChild(QLineEdit)
if search_edit:
search_edit.clear()
if self.installation_finished:
self.log_output.setText(self.INFO_TEXT)
self.installation_finished = False
self._update_ui_state()
self.previous_tab_widget = self.tabs.widget(index)
def _create_category_tab(self, title):
"""Создает вкладку с поиском и списком."""
tab = QWidget()
layout = QVBoxLayout(tab)
search_edit = QLineEdit()
search_edit.setPlaceholderText("Поиск...")
layout.addWidget(search_edit)
list_widget = NoAutoScrollListWidget()
list_widget.setAutoScroll(False) # Отключаем автопрокрутку
list_widget.itemChanged.connect(self._on_item_changed)
list_widget.currentItemChanged.connect(self._update_ui_state)
layout.addWidget(list_widget)
search_edit.textChanged.connect(lambda text, lw=list_widget: self.filter_list(text, lw))
self.tabs.addTab(tab, title)
return list_widget, search_edit
def filter_list(self, text, list_widget):
"""Фильтрует элементы в списке."""
# Блокируем все сигналы виджета, чтобы избежать автопрокрутки
# и лишних вызовов _update_ui_state при скрытии элементов
list_widget.blockSignals(True)
list_widget.disable_scroll() # Отключаем прокрутку
for i in range(list_widget.count()):
item = list_widget.item(i)
item.setHidden(text.lower() not in item.text().lower())
# Включаем прокрутку и прокручиваем к первому видимому элементу
list_widget.enable_scroll()
for i in range(list_widget.count()):
item = list_widget.item(i)
if not item.isHidden():
list_widget.scrollToItem(item, QListWidget.PositionAtTop)
break
list_widget.blockSignals(False)
# Обновляем состояние кнопок после фильтрации
self._update_ui_state()
def load_all_categories(self):
"""Запускает загрузку всех категорий."""
self.loading_count = len(self.categories)
for internal_name in self.categories.values():
self._start_load_process(internal_name)
def _get_cache_path(self, category):
"""Возвращает путь к файлу кэша для указанной категории."""
return os.path.join(self.cache_dir, f"{category}.json")
def _get_winetricks_hash(self):
"""Вычисляет хэш файла winetricks для проверки его обновления."""
try:
hasher = hashlib.sha256()
with open(self.winetricks_path, 'rb') as f:
while chunk := f.read(4096):
hasher.update(chunk)
return hasher.hexdigest()
except (IOError, OSError):
return None
def _start_load_process(self, category):
"""Запускает QProcess для получения списка компонентов, используя кэш."""
cache_path = self._get_cache_path(category)
cache_ttl_seconds = 86400
if os.path.exists(cache_path):
try:
with open(cache_path, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
cache_age = time.time() - cache_data.get("timestamp", 0)
winetricks_hash = self._get_winetricks_hash()
if cache_age < cache_ttl_seconds and cache_data.get("hash") == winetricks_hash:
QTimer.singleShot(0, lambda: self._on_load_finished(
category, 0, QProcess.NormalExit, from_cache=cache_data.get("output")
))
return
except (json.JSONDecodeError, IOError, KeyError):
self._log(f"--- Кэш для '{category}' поврежден, будет выполнена перезагрузка. ---")
process = QProcess(self)
self.processes[category] = process
process.setProcessChannelMode(QProcess.MergedChannels)
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
env.insert("WINE", self.wine_executable)
process.setProcessEnvironment(env)
process.finished.connect(partial(self._on_load_finished, category))
process.start(self.winetricks_path, [category, "list"])
def _parse_winetricks_log(self):
"""Читает winetricks.log и возвращает множество установленных компонентов."""
log_reader = WinetricksLogReader(self.prefix_path)
return log_reader.get_installed_verbs()
def _parse_winetricks_list_output(self, output, installed_verbs, list_widget, category):
"""Парсит вывод 'winetricks list' и заполняет QListWidget."""
parser = WinetricksOutputParser()
items, found_items = parser.parse_list_output(output, installed_verbs, category)
for item_data in items:
item = QListWidgetItem(item_data.item_text)
item.setData(Qt.UserRole, item_data.name)
item.setFont(QFont("DejaVu Sans Mono", 10))
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if item_data.is_installed else Qt.Unchecked)
list_widget.addItem(item)
self.initial_states[item_data.name] = item_data.is_installed
return found_items
def _on_load_finished(self, category, exit_code, exit_status, from_cache=None):
"""Обрабатывает завершение загрузки списка компонентов."""
if from_cache is not None:
output = from_cache
process = None
else:
process = self.processes[category]
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
list_widget = self.list_widgets[category]
category_display_name = next(k for k, v in self.categories.items() if v == category)
list_widget.clear()
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")
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:
installed_verbs = self._parse_winetricks_log()
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget, category)
if from_cache is None:
cache_path = self._get_cache_path(category)
winetricks_hash = self._get_winetricks_hash()
if winetricks_hash:
cache_data = {
"timestamp": time.time(),
"hash": winetricks_hash,
"output": output
}
try:
with open(cache_path, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
except (IOError, OSError) as e:
self._log(f"--- Не удалось сохранить кэш для '{category}': {e} ---")
if not found_items and output.strip():
self._log(f"--- Не удалось распознать вывод для категории '{category}' ---")
self._log(output)
self._log("--------------------------------------------------")
has_errors = any(status == 'error' for status in self.category_statuses.values())
self.loading_count -= 1
if self.loading_count == 0:
self._update_ui_state()
if self.is_reloading_after_cache_clear:
if has_errors:
self._log("\n=== Ошибка при обновлении списков. Проверьте лог выше. ===", "red")
else:
self._log("\n=== Списки успешно обновлены ===")
self.is_reloading_after_cache_clear = False
def _on_item_changed(self, item):
"""Обрабатывает изменение состояния чекбокса."""
name = item.data(Qt.UserRole)
if name in self.initial_states and self.initial_states.get(name) is True:
if item.checkState() == Qt.Unchecked:
list_widget = item.listWidget()
if list_widget:
list_widget.blockSignals(True)
item.setCheckState(Qt.Checked)
if list_widget:
list_widget.blockSignals(False)
self._update_ui_state()
def _update_ui_state(self, *args):
"""Обновляет состояние кнопок 'Применить' и 'Переустановить'."""
has_changes = False
for list_widget in self.list_widgets.values():
for i in range(list_widget.count()):
item = list_widget.item(i)
name = item.data(Qt.UserRole)
if name in self.initial_states:
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
if current_state != initial_state:
has_changes = True
break
if has_changes:
break
self.apply_button.setEnabled(has_changes)
is_reinstallable = False
if not has_changes:
current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
if current_list_widget:
current_item = current_list_widget.currentItem()
if current_item:
name = current_item.data(Qt.UserRole)
if self.initial_states.get(name, False):
is_reinstallable = True
self.reinstall_button.setEnabled(is_reinstallable)
def reinstall_selected(self):
"""Переустанавливает выбранный компонент."""
current_list_widget = self.tabs.currentWidget().findChild(QListWidget)
if not current_list_widget:
return
current_item = current_list_widget.currentItem()
if not current_item:
return
name = current_item.data(Qt.UserRole)
if not name:
return
self.log_output.setText(self.INFO_TEXT)
self.apply_button.setEnabled(False)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(False)
verbs_to_reinstall = [name]
self._start_install_process([], verbs_to_reinstall)
def apply_changes(self):
"""Применяет выбранные изменения."""
verbs_to_install = []
verbs_to_reinstall = []
for list_widget in self.list_widgets.values():
for i in range(list_widget.count()):
item = list_widget.item(i)
name = item.data(Qt.UserRole)
if name not in self.initial_states:
continue
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
if current_state != initial_state:
if initial_state:
verbs_to_reinstall.append(name)
else:
verbs_to_install.append(name)
if not verbs_to_install and not verbs_to_reinstall:
QMessageBox.information(self, "Нет изменений", "Не выбрано ни одного компонента для установки.")
return
self.log_output.setText(self.INFO_TEXT)
self.apply_button.setEnabled(False)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(False)
self._start_install_process(verbs_to_install, verbs_to_reinstall)
def _start_install_process(self, verbs_to_install, verbs_to_reinstall=None):
"""Запускает процесс установки/переустановки winetricks.
Args:
verbs_to_install: Список компонентов для новой установки (без --force)
verbs_to_reinstall: Список компонентов для переустановки (с --force)
"""
if verbs_to_reinstall is None:
verbs_to_reinstall = []
# Устанавливает новые компоненты (без --force)
if verbs_to_install:
self._log(f"Выполнение установки: winetricks --unattended {' '.join(verbs_to_install)}")
self._run_winetricks_process(verbs_to_install, use_force=False)
# Переустанавливает компоненты (с --force)
if verbs_to_reinstall:
if verbs_to_install:
# Если уже что-то устанавливается, добавляем небольшую задержку
self._log("\n---\n")
self._log(f"Выполнение переустановки: winetricks --unattended --force {' '.join(verbs_to_reinstall)}")
self._run_winetricks_process(verbs_to_reinstall, use_force=True)
def _run_winetricks_process(self, verbs, use_force=False):
"""Запускает процесс winetricks с указанными компонентами.
Args:
verbs: Список компонентов для установки
use_force: Использовать ли флаг --force
"""
args = ["--unattended"]
if use_force:
args.append("--force")
args.extend(verbs)
if not self.apply_process:
self.apply_process = QProcess(self)
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)
self.apply_process.start(self.winetricks_path, args)
def on_apply_finished(self, exit_code, exit_status):
"""Обрабатывает завершение применения изменений."""
if self.user_cancelled:
self._log("\n=== Установка прервана пользователем. ===")
self.close()
return
if exit_code != 0 or exit_status != QProcess.NormalExit:
self._log(f"\n=== Ошибка во время выполнения операций (код: {exit_code}). ===", "red")
self._show_message_box(
"Ошибка",
"Произошла ошибка во время выполнения операций.\nПодробности смотрите в логе.",
QMessageBox.Warning,
{"buttons": {"OK": QMessageBox.AcceptRole}}
)
self.log_output.setCurrentCharFormat(QTextCharFormat())
self.apply_button.setEnabled(True)
self.close_button.setEnabled(True)
return
self._log("\n=== Все операции успешно завершены ===")
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(True)
for search_edit in self.search_edits.values():
search_edit.clear()
self.initial_states.clear()
self.load_all_categories()
self.installation_complete.emit()
self.installation_finished = True
def clear_winetricks_cache(self):
"""Запускает очистку кэша Winetricks."""
reply = self._show_message_box(
"Очистка кэша Winetricks",
"Вы собираетесь удалить все скачанные установщики и списки компонентов Winetricks.\n\n"
"Это действие может потребоваться, если у вас возникают проблемы со скачиванием или установкой компонентов.\n\n"
"Продолжить?",
QMessageBox.Question,
{"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}
)
if reply != "Да":
return
self.tabs.setEnabled(False)
self.clear_cache_button.setEnabled(False)
self.apply_button.setEnabled(False)
self.reinstall_button.setEnabled(False)
self.close_button.setEnabled(False)
self.log_output.clear()
self.cache_clear_process = QProcess(self)
self.cache_clear_process.setProcessChannelMode(QProcess.MergedChannels)
def handle_output():
output = self.cache_clear_process.readAll().data().decode('utf-8', 'ignore').strip()
if output:
self._log(output)
def handle_finish(exit_code, exit_status):
if exit_code == 0:
self.is_reloading_after_cache_clear = True
self.category_statuses.clear()
os.makedirs(self.cache_dir, exist_ok=True)
self._log("Обновление списков...")
self.initial_states.clear()
self.load_all_categories()
else:
self._log(f"\n=== Ошибка (код: {exit_code}) ===", "red")
self.tabs.setEnabled(True)
self.clear_cache_button.setEnabled(True)
self.close_button.setEnabled(True)
self._update_ui_state()
self.cache_clear_process.readyRead.connect(handle_output)
self.cache_clear_process.finished.connect(handle_finish)
winehelper_path = self.parent().winehelper_path if hasattr(self.parent(), 'winehelper_path') else EnvironmentConfig.RUN_SCRIPT
args = ["clear-winetricks-cache", "--force"]
self._log(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(args)}\n")
self.cache_clear_process.start(winehelper_path, args)
def closeEvent(self, event):
"""Обрабатывает закрытие окна."""
if self.apply_process and self.apply_process.state() == QProcess.Running:
reply = self._show_message_box(
'Подтверждение',
"Процесс установки еще не завершен. Вы уверены, что хотите прервать его?",
QMessageBox.Question,
{"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}
)
if reply == "Да":
self.user_cancelled = True
self.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===")
self.apply_process.terminate()
event.ignore()
else:
event.ignore()
else:
event.accept()
def _show_message_box(self, title, text, icon, config):
"""Централизованный метод для создания и показа QMessageBox."""
msg_box = QMessageBox(self)
msg_box.setWindowTitle(title)
msg_box.setText(text)
msg_box.setIcon(icon)
buttons = {}
for btn_text, role in config.get("buttons", {}).items():
buttons[btn_text] = msg_box.addButton(btn_text, role)
default_btn_text = config.get("default")
if default_btn_text and default_btn_text in buttons:
msg_box.setDefaultButton(buttons[default_btn_text])
msg_box.exec_()
clicked_button = msg_box.clickedButton()
return clicked_button.text() if clicked_button else None
def _log(self, message, color=None):
"""Добавляет сообщение в лог с возможностью указания цвета."""
if color:
self.log_output.append(f'<span style="color:{color};">{message}</span>')
self.log_output.setCurrentCharFormat(QTextCharFormat())
else:
self.log_output.append(message)
self.log_output.moveCursor(QTextCursor.End)
+13
View File
@@ -0,0 +1,13 @@
"""
Парсеры для winehelper.
"""
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser, WinetricksItem
from winehelperqt.parsers.script_parser import ScriptParser
__all__ = [
'Sha256Parser',
'WinetricksOutputParser',
'WinetricksItem',
'ScriptParser',
]
+99
View File
@@ -0,0 +1,99 @@
"""
Парсер для извлечения информации из скриптов установки.
"""
import shlex
class ScriptParser:
"""Утилитарный класс для парсинга информации из скриптов установки."""
@staticmethod
def extract_icons_from_script(script_path):
"""
Извлекает иконку для скрипта.
Сначала ищет переменную 'export ICONS_BUTTON_GUI=', если не находит,
то ищет 'export PROG_ICON=', затем ищет вызовы 'create_desktop'.
Returns:
Список имен иконок
"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Приоритет у ICONS_BUTTON_GUI
for line in lines:
if line.strip().startswith('export ICONS_BUTTON_GUI='):
icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
if icon_names_str:
return icon_names_str.split()
for line in lines:
if line.strip().startswith('export PROG_ICON='):
icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
if icon_names_str:
return icon_names_str.split()
# Поиск в create_desktop
icon_names = []
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
if 'create_desktop' in line:
try:
parts = shlex.split(line)
for i, part in enumerate(parts):
if part == 'create_desktop':
if len(parts) > i + 3:
icon_name = parts[i + 3]
if icon_name and icon_name.lower() not in ('auto', 'nocopy'):
icon_names.append(icon_name)
except (ValueError, IndexError):
continue
return icon_names
except Exception as e:
print(f"Ошибка чтения файла для извлечения иконки: {str(e)}")
return []
@staticmethod
def extract_prog_name_from_script(script_path):
"""Извлекает имя программы из строки PROG_NAME= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip().startswith(('export PROG_NAME=', 'PROG_NAME=')):
name = line.split('=', 1)[1].strip().strip('"\'')
if name:
return name
return None
except Exception as e:
print(f"Ошибка чтения файла для извлечения PROG_NAME: {str(e)}")
return None
@staticmethod
def extract_prog_url_from_script(script_path):
"""Извлекает URL из строки export PROG_URL= в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('export PROG_URL='):
return line.replace('export PROG_URL=', '').strip().strip('"\'')
return None
except Exception as e:
print(f"Ошибка чтения файла для извлечения PROG_URL: {str(e)}")
return None
@staticmethod
def extract_info_ru(script_path):
"""Извлекает информацию из строки # info_ru: в скрипте"""
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('# info_ru:'):
return line.replace('# info_ru:', '').strip()
return "Описание отсутствует"
except Exception as e:
return f"Ошибка чтения файла: {str(e)}"
+233
View File
@@ -0,0 +1,233 @@
"""
Парсер для файла sha256sum.list.
Извлекает версии Wine и компонентов из файла хешей.
"""
import os
import re
class Sha256Parser:
"""Парсер файла sha256sum.list для извлечения версий пакетов."""
WINE_ALLOWED_GROUPS = {"WINE WOW64", "WINE AMD64", "WINE I586"}
GROUP_HEADER_PATTERN = re.compile(r'^#+\s*(.*?)\s*#*$')
HASH_PATTERN = re.compile(r'^[a-f0-9]{64}')
ARCHIVE_EXTENSION = '.tar.xz'
@classmethod
def parse_wine_versions(cls, sha256_path):
"""
Парсит файл sha256sum.list и возвращает версии Wine, сгруппированные по архитектурам.
Args:
sha256_path: Путь к файлу sha256sum.list
Returns:
Словарь {группа: [версии]}, например:
{"WINE AMD64": ["8.0", "7.0"], "WINE WOW64": ["8.0-wow64"]}
"""
versions_data = {}
if not os.path.exists(sha256_path):
return 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 = cls.GROUP_HEADER_PATTERN.match(line)
if match:
group_name = match.group(1).strip()
if group_name in cls.WINE_ALLOWED_GROUPS:
current_group = group_name
if current_group not in versions_data:
versions_data[current_group] = []
else:
current_group = None
continue
if current_group and cls.HASH_PATTERN.match(line):
parts = line.split(maxsplit=1)
if len(parts) == 2:
filename = parts[1]
if filename.endswith(cls.ARCHIVE_EXTENSION):
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
versions_data[current_group].append(version_name)
except IOError:
return {}
return versions_data
@classmethod
def parse_component_versions(cls, sha256_path, component_group):
"""
Парсит файл sha256sum.list и возвращает версии конкретного компонента.
Args:
sha256_path: Путь к файлу sha256sum.list
component_group: Имя группы компонента (например, "DXVK", "VKD3D")
Returns:
Список версий компонента, например: ["2.3", "2.2", "2.1"]
"""
versions_data = []
if not os.path.exists(sha256_path):
return 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 = cls.GROUP_HEADER_PATTERN.match(line)
if match:
current_group = match.group(1).strip()
continue
if current_group == component_group and cls.HASH_PATTERN.match(line):
parts = line.split(maxsplit=1)
if len(parts) == 2:
filename = parts[1]
if filename.endswith(cls.ARCHIVE_EXTENSION):
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
versions_data.append(version_name)
except IOError:
return []
return versions_data
@classmethod
def parse_prepared_prefixes(cls, sha256_path):
"""
Парсит файл sha256sum.list и возвращает информацию о подготовленных префиксах.
Args:
sha256_path: Путь к файлу sha256sum.list
Returns:
Словарь {архитектура: [(имя_префикса, описание)]}, например:
{'win32': [('prefix_x86_v1', 'Описание')], 'win64': [...]}
"""
prepared_prefixes = {'win32': [], 'win64': []}
if not os.path.exists(sha256_path):
return prepared_prefixes
in_prefix_section = False
current_description = ""
current_prefix_name = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if "##### PREFIX #####" in line:
in_prefix_section = True
continue
elif line.startswith("#####"):
if current_prefix_name and current_description:
cls._add_prefix_to_list(
prepared_prefixes,
current_prefix_name,
current_description
)
in_prefix_section = False
current_description = ""
current_prefix_name = None
continue
if not in_prefix_section:
continue
if cls.HASH_PATTERN.match(line):
if current_prefix_name and current_description:
cls._add_prefix_to_list(
prepared_prefixes,
current_prefix_name,
current_description
)
current_description = ""
parts = line.split(maxsplit=1)
if len(parts) == 2:
current_prefix_name = parts[1].replace('.tar.xz', '')
else:
current_prefix_name = None
elif line.startswith('# '):
comment_line = line[2:].strip()
if current_description:
current_description += "\n" + comment_line
else:
current_description = comment_line
if current_prefix_name and current_description:
cls._add_prefix_to_list(
prepared_prefixes,
current_prefix_name,
current_description
)
except IOError:
return {'win32': [], 'win64': []}
cls._add_clean_prefix_option(prepared_prefixes)
return prepared_prefixes
@classmethod
def _add_prefix_to_list(cls, prepared_prefixes, prefix_name, description):
"""Добавляет префикс в список с определением архитектуры."""
arch = None
if "_x86_" in prefix_name or "_i586_" in prefix_name:
arch = 'win32'
elif "_x64_" in prefix_name or "_amd64_" in prefix_name:
arch = 'win64'
if arch:
prepared_prefixes[arch].append(
(prefix_name, description.strip().replace('\\n', '\n'))
)
@classmethod
def _add_clean_prefix_option(cls, prepared_prefixes):
"""Добавляет опцию 'Чистый префикс' для обеих архитектур."""
clean_prefix_description = 'Создать чистый префикс без дополнительных библиотек'
prepared_prefixes['win32'].insert(0, ('none', clean_prefix_description))
prepared_prefixes['win64'].insert(0, ('none', clean_prefix_description))
@classmethod
def get_installed_versions(cls, dist_path):
"""
Возвращает список локально установленных версий из директории dist.
Args:
dist_path: Путь к директории с установленными версиями
Returns:
Отсортированный список имен версий
"""
if not os.path.isdir(dist_path):
return []
try:
return sorted([
name for name in os.listdir(dist_path)
if os.path.isdir(os.path.join(dist_path, name))
], reverse=True)
except OSError:
return []
+176
View File
@@ -0,0 +1,176 @@
"""
Парсер для вывода утилиты Winetricks.
Извлекает списки компонентов и информацию об установленных пакетах.
"""
import os
import re
class WinetricksItem:
"""Элемент списка компонентов Winetricks."""
def __init__(self, name, description, is_installed, item_text):
self.name = name
self.description = description
self.is_installed = is_installed
self.item_text = item_text
class WinetricksOutputParser:
"""Парсер вывода команды winetricks list."""
LINE_PATTERN = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
DLLS_BLACKLIST_PATTERN = re.compile(
r'^(dont_use|dxvk|vkd3d|galliumnine|faudio|foundation)',
re.IGNORECASE
)
FONTS_BLACKLIST_PATTERN = re.compile(
r'^(dont_use)',
re.IGNORECASE
)
SETTINGS_BLACKLIST_PATTERN = re.compile(
r'^(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)',
re.IGNORECASE
)
DOWNLOAD_AVAILABLE_PATTERN = re.compile(r'\[\s*доступно для скачивания[^]]*\]')
IN_CACHE_PATTERN = re.compile(r'\[\s*в кэше\s*\]')
SERVICE_WORDS = {'executing', 'using', 'warning:'}
@classmethod
def parse_installed_verbs(cls, log_path):
"""
Читает winetricks.log и возвращает множество установленных компонентов.
Args:
log_path: Путь к файлу winetricks.log
Returns:
Множество имен установленных компонентов
"""
installed_verbs = set()
if not os.path.exists(log_path):
return installed_verbs
try:
with open(log_path, 'r', encoding='utf-8') as f:
for line in f:
verb = line.split('#', 1)[0].strip()
if verb:
installed_verbs.add(verb)
except Exception as e:
print(f"Предупреждение: не удалось прочитать {log_path}: {e}")
return installed_verbs
@classmethod
def parse_list_output(cls, output, installed_verbs, category):
"""
Парсит вывод 'winetricks list' и возвращает список компонентов.
Args:
output: Вывод команды winetricks list
installed_verbs: Множество установленных компонентов
category: Категория компонентов ('dlls', 'fonts', 'settings')
Returns:
Кортеж (список элементов, найдены ли элементы)
"""
items = []
found_items = False
for line in output.splitlines():
match = cls.LINE_PATTERN.match(line)
if not match:
continue
found_items = True
_status, name, description = match.groups()
if not cls._is_valid_component(name, category):
continue
description = cls._clean_description(description)
is_checked = name in installed_verbs
item_text = f"{name.ljust(27)}{description.strip()}"
items.append(WinetricksItem(
name=name,
description=description.strip(),
is_installed=is_checked,
item_text=item_text
))
return items, found_items
@classmethod
def _get_blacklist_pattern(cls, category):
"""Возвращает шаблон blacklist для указанной категории."""
patterns = {
'dlls': cls.DLLS_BLACKLIST_PATTERN,
'fonts': cls.FONTS_BLACKLIST_PATTERN,
'settings': cls.SETTINGS_BLACKLIST_PATTERN
}
return patterns.get(category)
@classmethod
def _is_valid_component(cls, name, category):
"""
Проверяет, является ли имя допустимым компонентом.
Args:
name: Имя компонента
category: Категория компонента
Returns:
True если компонент допустим, False иначе
"""
if '/' in name or '\\' in name:
return False
if name.lower() in cls.SERVICE_WORDS or name.endswith(':'):
return False
blacklist_pattern = cls._get_blacklist_pattern(category)
if blacklist_pattern and blacklist_pattern.search(name):
return False
return True
@classmethod
def _clean_description(cls, description):
"""
Очищает описание от избыточной информации.
Args:
description: Исходное описание
Returns:
Очищенное описание
"""
description = cls.DOWNLOAD_AVAILABLE_PATTERN.sub('', description)
description = cls.IN_CACHE_PATTERN.sub('', description)
return description.strip()
@classmethod
def get_blacklist_pattern_for_category(cls, category):
"""
Возвращает строковое представление blacklist паттерна для категории.
Полезно для отладки и логирования.
Args:
category: Категория ('dlls', 'fonts', 'settings')
Returns:
Строковое представление паттерна или None
"""
patterns = {
'dlls': cls.DLLS_BLACKLIST_PATTERN.pattern,
'fonts': cls.FONTS_BLACKLIST_PATTERN.pattern,
'settings': cls.SETTINGS_BLACKLIST_PATTERN.pattern
}
return patterns.get(category)
+27
View File
@@ -0,0 +1,27 @@
"""
Сервисы для winehelper GUI.
"""
from winehelperqt.services.winetricks_service import WinetricksCacheService, WinetricksLogReader
from winehelperqt.services.prefix_manager import PrefixManager
from winehelperqt.services.process_handler import ProcessHandler, CommandRunner
from winehelperqt.services.desktop_file_manager import DesktopFileManager
from winehelperqt.services.application_installer import ApplicationInstaller
from winehelperqt.services.script_manager import ScriptManager, ScriptInfo
from winehelperqt.services.installation_manager import InstallationManager
from winehelperqt.services.app_launcher import AppLauncher
from winehelperqt.services.settings_manager import SettingsManager
__all__ = [
'WinetricksCacheService',
'WinetricksLogReader',
'PrefixManager',
'ProcessHandler',
'CommandRunner',
'DesktopFileManager',
'ApplicationInstaller',
'ScriptManager',
'ScriptInfo',
'InstallationManager',
'AppLauncher',
'SettingsManager',
]
+293
View File
@@ -0,0 +1,293 @@
"""
Сервис для запуска и управления приложениями WineHelper.
Предоставляет функциональность:
- Запуск приложений из .desktop файлов
- Остановка приложений
- Мониторинг запущенных приложений
- Создание логов запуска (в обычном и debug режиме)
"""
import os
import shlex
from typing import Dict, Optional
from datetime import datetime
from PyQt5.QtCore import QProcess, QProcessEnvironment
from winehelperqt.utils.process_utils import ProcessUtils
class AppLauncher:
"""Сервис для запуска и управления приложениями Wine."""
def __init__(self, winehelper_path: str, user_work_path: str):
"""
Инициализирует лаунчер приложений.
Args:
winehelper_path: Путь к скрипту winehelper
user_work_path: Путь к рабочей директории пользователя
"""
self.winehelper_path = winehelper_path
self.user_work_path = user_work_path
self._active_processes: Dict[str, QProcess] = {}
def launch_app(self, desktop_path: str, debug: bool = False) -> QProcess:
"""
Запускает приложение из .desktop файла.
Args:
desktop_path: Путь к .desktop файлу
debug: Запустить в режиме отладки
Returns:
QProcess запущенного приложения
"""
if not os.path.exists(desktop_path):
raise FileNotFoundError(f"Desktop файл не найден: {desktop_path}")
# Парсим .desktop файл
exec_command = self._get_exec_from_desktop(desktop_path)
if not exec_command:
raise ValueError(f"Не удалось получить команду запуска из: {desktop_path}")
process = QProcess()
process.setProcessChannelMode(QProcess.MergedChannels)
# В режиме debug включаем полный вывод
if debug:
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEDEBUG", "+loaddll,+seh")
process.setProcessEnvironment(env)
# Запускаем команду
args = shlex.split(exec_command)
executable = args[0]
process.start(executable, args[1:])
if not process.waitForStarted():
raise RuntimeError(f"Не удалось запустить приложение: {desktop_path}")
# Сохраняем процесс
self._active_processes[desktop_path] = process
# Подключаем обработчик завершения
process.finished.connect(
lambda: self._on_app_finished(desktop_path, process)
)
return process
def launch_app_with_log(self, desktop_path: str, debug: bool = False) -> QProcess:
"""
Запускает приложение с созданием лога.
Args:
desktop_path: Путь к .desktop файлу
debug: Создать debug лог
Returns:
QProcess запущенного приложения
"""
process = self.launch_app(desktop_path, debug)
# Создаём лог если в debug режиме
if debug:
log_path = self._create_debug_log(desktop_path, process)
if log_path:
print(f"Debug лог создан: {log_path}")
return process
def launch_app_by_command(self, desktop_path: str,
command: str,
env_vars: Optional[Dict[str, str]] = None,
debug: bool = False) -> QProcess:
"""
Запускает приложение по команде (для сложных случаев с env).
Args:
desktop_path: Путь к .desktop файлу (для отслеживания)
command: Команда запуска
env_vars: Дополнительные переменные окружения
debug: Режим отладки
Returns:
QProcess запущенного приложения
"""
if desktop_path in self._active_processes:
raise RuntimeError(f"Приложение уже запущено: {desktop_path}")
process = QProcess()
process.setProcessChannelMode(QProcess.MergedChannels)
# Настраиваем окружение
env = QProcessEnvironment.systemEnvironment()
if env_vars:
for key, value in env_vars.items():
env.insert(key, value)
if debug:
env.insert("WINEDEBUG", "+loaddll,+seh")
process.setProcessEnvironment(env)
# Парсим команду
args = shlex.split(command)
executable = args[0]
process.start(executable, args[1:])
if not process.waitForStarted():
raise RuntimeError(f"Не удалось запустить приложение: {desktop_path}")
self._active_processes[desktop_path] = process
# НЕ подключаем обработчик — пусть вызывает сам
return process
def mark_app_finished(self, desktop_path: str) -> Optional[QProcess]:
"""
Отмечает приложение как завершённое и возвращает процесс.
Args:
desktop_path: Путь к .desktop файлу
Returns:
QProcess или None
"""
return self._active_processes.pop(desktop_path, None)
def stop_app(self, desktop_path: str) -> None:
"""
Останавливает запущенное приложение.
Args:
desktop_path: Путь к .desktop файлу
"""
process = self._active_processes.get(desktop_path)
ProcessUtils.stop_process(process)
def is_app_running(self, desktop_path: str) -> bool:
"""
Проверяет, запущено ли приложение.
Args:
desktop_path: Путь к .desktop файлу
Returns:
True если приложение запущено
"""
process = self._active_processes.get(desktop_path)
return process and process.state() == QProcess.Running
def get_running_apps(self) -> Dict[str, QProcess]:
"""
Возвращает список запущенных приложений.
Returns:
Словарь {desktop_path: QProcess}
"""
return {
path: proc for path, proc in self._active_processes.items()
if proc.state() == QProcess.Running
}
def get_process_for_app(self, desktop_path: str) -> Optional[QProcess]:
"""
Возвращает процесс для приложения.
Args:
desktop_path: Путь к .desktop файлу
Returns:
QProcess или None
"""
return self._active_processes.get(desktop_path)
def connect_output_callback(self, desktop_path: str,
callback) -> None:
"""
Подключает callback для обработки вывода приложения.
Args:
desktop_path: Путь к .desktop файлу
callback: Функция для обработки вывода
"""
process = self._active_processes.get(desktop_path)
if process:
def read_output():
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
if output:
callback(output)
process.readyReadStandardOutput.connect(read_output)
def stop_all(self) -> None:
"""Останавливает все запущенные приложения."""
for desktop_path in list(self._active_processes.keys()):
self.stop_app(desktop_path)
def _on_app_finished(self, desktop_path: str, process: QProcess) -> None:
"""Обработчик завершения приложения."""
self._active_processes.pop(desktop_path, None)
def _get_exec_from_desktop(self, desktop_path: str) -> Optional[str]:
"""
Получает команду запуска из .desktop файла.
Args:
desktop_path: Путь к .desktop файлу
Returns:
Команда запуска или None
"""
try:
with open(desktop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('Exec='):
# Убираем %F, %f, %U, %u (параметры desktop-файла)
exec_cmd = line[5:].strip()
exec_cmd = exec_cmd.replace('%F', '').replace('%f', '')
exec_cmd = exec_cmd.replace('%U', '').replace('%u', '')
return exec_cmd.strip()
except (IOError, OSError) as e:
print(f"Ошибка чтения {desktop_path}: {e}")
return None
def _create_debug_log(self, desktop_path: str, process: QProcess) -> Optional[str]:
"""
Создаёт debug лог для приложения.
Args:
desktop_path: Путь к .desktop файлу
process: QProcess приложения
Returns:
Путь к файлу лога или None
"""
try:
# Создаём директорию для логов
log_dir = os.path.join(self.user_work_path, "logs")
os.makedirs(log_dir, exist_ok=True)
# Формируем имя файла
app_name = os.path.splitext(os.path.basename(desktop_path))[0]
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = f"{app_name}_{timestamp}.log"
log_path = os.path.join(log_dir, log_filename)
# Читаем вывод и записываем в лог
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
with open(log_path, 'w', encoding='utf-8') as f:
f.write(f"Debug лог для: {desktop_path}\n")
f.write(f"Время запуска: {timestamp}\n")
f.write("=" * 80 + "\n\n")
f.write(output)
return log_path
except (IOError, OSError) as e:
print(f"Ошибка создания debug лога: {e}")
return None
@@ -0,0 +1,132 @@
"""
Сервис для установки приложений Wine.
"""
from typing import List, Optional
from PyQt5.QtCore import QProcess, QProcessEnvironment
from winehelperqt.services.script_manager import ScriptManager, ScriptInfo
class ApplicationInstaller:
"""
Сервис для установки приложений через Wine.
"""
def __init__(self, data_path, user_work_path):
"""
Инициализирует установщик приложений.
Args:
data_path: Путь к директории данных
user_work_path: Путь к рабочей директории пользователя
"""
self.data_path = data_path
self.user_work_path = user_work_path
self._script_manager = ScriptManager(data_path)
def get_script_folders(self, folder_name):
"""
Возвращает список папок со скриптами.
"""
return self._script_manager.get_script_folders(folder_name)
def get_script_info(self, script_path):
"""
Получает информацию о скрипте установки.
Returns:
Словарь с информацией о скрипте
"""
script_info: Optional[ScriptInfo] = self._script_manager.get_script_info(script_path)
if script_info is None:
return {}
return {
'name': script_info.name,
'url': script_info.url,
'description': script_info.description,
'icons': script_info.icons
}
def list_available_apps(self, folder_name):
"""
Возвращает список доступных приложений.
Returns:
Список словарей с информацией о приложениях
"""
scripts: List[ScriptInfo] = self._script_manager.list_available_apps(folder_name)
apps = []
for script in scripts:
apps.append({
'name': script.name,
'folder': script.folder,
'script': script.script_path,
'description': script.description,
'icons': script.icons,
'url': script.url
})
return apps
def get_autoinstall_apps(self):
"""Возвращает список приложений для автоустановки."""
return self.list_available_apps(self._script_manager.AUTOINSTALL_DIR)
def get_manualinstall_apps(self):
"""Возвращает список приложений для ручной установки."""
return self.list_available_apps(self._script_manager.MANUALINSTALL_DIR)
def get_testinstall_apps(self):
"""Возвращает список приложений для тестовой установки."""
return self.list_available_apps(self._script_manager.TESTINSTALL_DIR)
def run_installation(self, script_path, winehelper_path, log_callback=None):
"""
Запускает установку приложения.
Args:
script_path: Путь к скрипту установки
winehelper_path: Путь к winehelper
log_callback: Callback для логирования (опционально)
Returns:
QProcess запущенного процесса
"""
process = QProcess()
process.setProcessChannelMode(QProcess.MergedChannels)
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEHELPER_PATH", winehelper_path)
process.setProcessEnvironment(env)
if log_callback:
process.readyReadStandardOutput.connect(
lambda: log_callback(process.readAllStandardOutput().data().decode('utf-8', 'ignore'))
)
process.start(script_path)
return process
def search_apps(self, apps, search_text):
"""
Ищет приложения по названию.
Args:
apps: Список приложений (словари)
search_text: Текст для поиска
Returns:
Отфильтрованный список приложений
"""
if not search_text:
return apps
search_lower = search_text.lower()
return [
app for app in apps
if search_lower in app['name'].lower() or
search_lower in app.get('description', '').lower()
]
@@ -0,0 +1,190 @@
"""
Сервис для управления .desktop файлами.
"""
import os
import shutil
class DesktopFileManager:
"""
Сервис для управления .desktop файлами приложений.
Предоставляет функциональность:
- Создание .desktop файлов
- Чтение информации из .desktop файлов
- Удаление .desktop файлов
- Поиск установленных приложений
"""
def __init__(self, user_work_path):
"""
Инициализирует менеджер .desktop файлов.
Args:
user_work_path: Путь к рабочей директории пользователя
"""
self.user_work_path = user_work_path
# .desktop файлы создаются прямо в USER_WORK_PATH (см. winehelper: create_desktop)
self.desktop_dir = user_work_path
def get_desktop_file_path(self, app_name):
"""
Возвращает путь к .desktop файлу приложения.
Args:
app_name: Имя приложения
Returns:
Путь к .desktop файлу
"""
return os.path.join(self.desktop_dir, f"{app_name}.desktop")
def list_desktop_files(self):
"""
Возвращает список всех .desktop файлов.
Returns:
Список имен .desktop файлов (без расширения)
"""
if not os.path.isdir(self.desktop_dir):
return []
try:
return [
f.replace('.desktop', '') for f in os.listdir(self.desktop_dir)
if f.endswith('.desktop')
]
except OSError:
return []
def read_desktop_file(self, app_name):
"""
Читает .desktop файл и возвращает его содержимое.
Args:
app_name: Имя приложения
Returns:
Словарь с полями из .desktop файла
"""
desktop_path = self.get_desktop_file_path(app_name)
if not os.path.exists(desktop_path):
return {}
result = {}
try:
with open(desktop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or line.startswith('['):
continue
if '=' in line:
key, value = line.split('=', 1)
result[key.strip()] = value.strip()
except (IOError, OSError):
return {}
return result
def get_app_name_from_desktop(self, app_name):
"""
Получает имя приложения из .desktop файла.
Args:
app_name: Имя приложения
Returns:
Отображаемое имя приложения
"""
data = self.read_desktop_file(app_name)
return data.get('Name', app_name)
def get_app_icon_from_desktop(self, app_name):
"""
Получает путь к иконке приложения.
Args:
app_name: Имя приложения
Returns:
Путь к иконке или None
"""
data = self.read_desktop_file(app_name)
return data.get('Icon')
def delete_desktop_file(self, app_name):
"""
Удаляет .desktop файл приложения.
Args:
app_name: Имя приложения
Returns:
True если удаление успешно, False иначе
"""
desktop_path = self.get_desktop_file_path(app_name)
if not os.path.exists(desktop_path):
return False, f"Файл '{desktop_path}' не найден"
try:
os.remove(desktop_path)
return True, "Файл удален"
except OSError as e:
return False, f"Ошибка удаления: {e}"
def desktop_file_exists(self, app_name):
"""
Проверяет существование .desktop файла.
Args:
app_name: Имя приложения
Returns:
True если файл существует, False иначе
"""
return os.path.exists(self.get_desktop_file_path(app_name))
def get_installed_apps_with_info(self):
"""
Возвращает информацию обо всех установленных приложениях.
Returns:
Список словарей с информацией о приложениях
"""
apps = []
for app_name in self.list_desktop_files():
data = self.read_desktop_file(app_name)
apps.append({
'name': app_name,
'display_name': data.get('Name', app_name),
'icon': data.get('Icon'),
'exec': data.get('Exec'),
'path': self.get_desktop_file_path(app_name)
})
return apps
def copy_desktop_file(self, source_path, dest_name):
"""
Копирует .desktop файл в директорию пользователя.
Args:
source_path: Путь к исходному файлу
dest_name: Имя destination файла (без расширения)
Returns:
True если копирование успешно, False иначе
"""
if not os.path.exists(source_path):
return False, f"Исходный файл не найден: {source_path}"
os.makedirs(self.desktop_dir, exist_ok=True)
dest_path = self.get_desktop_file_path(dest_name)
try:
shutil.copy2(source_path, dest_path)
return True, "Файл скопирован"
except (IOError, OSError) as e:
return False, f"Ошибка копирования: {e}"
@@ -0,0 +1,180 @@
"""
Сервис для управления процессом установки приложений WineHelper.
Предоставляет функциональность:
- Запуск процесса установки
- Мониторинг прогресса установки
- Обработка логов (stdout/stderr)
- Управление процессами (остановка, завершение)
- Отмена установки
"""
import os
from typing import Dict, Optional, Callable
from PyQt5.QtCore import QProcess, QProcessEnvironment
from winehelperqt.utils.process_utils import ProcessUtils
class InstallationManager:
"""Сервис для управления процессом установки приложений."""
def __init__(self, winehelper_path: str):
"""
Инициализирует менеджер установки.
Args:
winehelper_path: Путь к скрипту winehelper
"""
self.winehelper_path = winehelper_path
self._active_processes: Dict[str, QProcess] = {}
def start_install(self, script_path: str,
work_path: Optional[str] = None,
env_vars: Optional[Dict[str, str]] = None) -> QProcess:
"""
Запускает процесс установки приложения.
Args:
script_path: Путь к скрипту установки (install.sh)
work_path: Рабочая директория (по умолчанию директория скрипта)
env_vars: Дополнительные переменные окружения
Returns:
QProcess запущенного процесса
"""
if not os.path.exists(script_path):
raise FileNotFoundError(f"Скрипт установки не найден: {script_path}")
process = QProcess()
process.setProcessChannelMode(QProcess.MergedChannels)
# Настраиваем окружение
env = QProcessEnvironment.systemEnvironment()
# Добавляем пользовательские переменные окружения
if env_vars:
for key, value in env_vars.items():
env.insert(key, value)
process.setProcessEnvironment(env)
# Устанавливаем рабочую директорию
if work_path:
process.setWorkingDirectory(work_path)
# Запускаем скрипт
process.start(script_path)
if not process.waitForStarted():
raise RuntimeError(f"Не удалось запустить процесс установки: {script_path}")
return process
def start_manual_install(self, script_path: str,
installer_file: str,
env_vars: Optional[Dict[str, str]] = None) -> QProcess:
"""
Запускает ручную установку с указанием файла установщика.
Args:
script_path: Путь к скрипту установки
installer_file: Путь к файлу установщика (.exe или .msi)
env_vars: Дополнительные переменные окружения
Returns:
QProcess запущенного процесса
"""
# Для ручной установки передаём путь к файлу через окружение
custom_env = env_vars.copy() if env_vars else {}
custom_env['USER_INSTALL_FILE'] = installer_file
return self.start_install(script_path, env_vars=custom_env)
def stop_install(self, process: QProcess) -> None:
"""
Останавливает процесс установки.
Args:
process: Процесс установки для остановки
"""
ProcessUtils.stop_process(process)
def is_running(self, process: QProcess) -> bool:
"""
Проверяет, запущен ли процесс установки.
Args:
process: Процесс для проверки
Returns:
True если процесс запущен
"""
return process and process.state() == QProcess.Running
def connect_output_callback(self, process: QProcess,
callback: Callable[[str], None]) -> None:
"""
Подключает callback для обработки вывода процесса.
Args:
process: Процесс установки
callback: Функция для обработки вывода (принимает строку)
"""
def read_output():
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
if output:
callback(output)
process.readyReadStandardOutput.connect(read_output)
def connect_error_callback(self, process: QProcess,
callback: Callable[[str], None]) -> None:
"""
Подключает callback для обработки ошибок.
Args:
process: Процесс установки
callback: Функция для обработки ошибок (принимает строку)
"""
def read_error():
error = process.readAllStandardError().data().decode('utf-8', 'ignore')
if error:
callback(error)
process.readyReadStandardError.connect(read_error)
def get_install_script_for_folder(self, folder_path: str) -> Optional[str]:
"""
Возвращает путь к скрипту установки для папки.
Args:
folder_path: Путь к папке скрипта
Returns:
Путь к install.sh или None
"""
script_path = os.path.join(folder_path, 'install.sh')
if os.path.exists(script_path):
return script_path
return None
def get_active_installations(self) -> Dict[str, QProcess]:
"""
Возвращает список активных процессов установки.
Returns:
Словарь {script_path: QProcess}
"""
return {
path: proc for path, proc in self._active_processes.items()
if self.is_running(proc)
}
def cancel_all(self) -> None:
"""Отменяет все активные процессы установки."""
for path, process in list(self._active_processes.items()):
if self.is_running(process):
self.stop_install(process)
self._active_processes.clear()
+270
View File
@@ -0,0 +1,270 @@
"""
Сервис для управления префиксами Wine.
"""
import os
import re
import subprocess
import shutil
class PrefixManager:
"""
Сервис для управления префиксами Wine.
Предоставляет функциональность:
- Создание префиксов
- Удаление префиксов
- Получение информации о префиксах
- Управление конфигурацией префикса
"""
def __init__(self, user_work_path):
"""
Инициализирует менеджер префиксов.
Args:
user_work_path: Путь к рабочей директории пользователя
"""
self.user_work_path = user_work_path
self.prefixes_path = os.path.join(user_work_path, "prefixes")
def get_prefix_path(self, prefix_name):
"""
Возвращает полный путь к префиксу.
Args:
prefix_name: Имя префикса
Returns:
Полный путь к директории префикса
"""
return os.path.join(self.prefixes_path, prefix_name)
def get_prefix_config_path(self, prefix_name, config_file="last.conf"):
"""
Возвращает путь к файлу конфигурации префикса.
Args:
prefix_name: Имя префикса
config_file: Имя файла конфигурации
Returns:
Путь к файлу конфигурации
"""
prefix_path = self.get_prefix_path(prefix_name)
return os.path.join(prefix_path, config_file)
def prefix_exists(self, prefix_name):
"""
Проверяет существование префикса.
Args:
prefix_name: Имя префикса
Returns:
True если префикс существует, False иначе
"""
return os.path.isdir(self.get_prefix_path(prefix_name))
def list_prefixes(self):
"""
Возвращает список всех префиксов.
Returns:
Список имен префиксов
"""
if not os.path.isdir(self.prefixes_path):
return []
try:
return sorted([
name for name in os.listdir(self.prefixes_path)
if os.path.isdir(os.path.join(self.prefixes_path, name))
])
except OSError:
return []
def create_prefix(self, prefix_name, wine_arch="win64", base_pfx="none"):
"""
Создает новый префикс.
Args:
prefix_name: Имя префикса
wine_arch: Архитектура ("win32" или "win64")
base_pfx: Базовый шаблон префикса
Returns:
True если создание успешно, False иначе
"""
if not prefix_name:
return False, "Имя префикса не может быть пустым"
if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
return False, "Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания"
prefix_path = self.get_prefix_path(prefix_name)
if os.path.exists(prefix_path):
return False, f"Префикс с именем '{prefix_name}' уже существует"
# Создаем директорию префикса
try:
os.makedirs(prefix_path, exist_ok=True)
return True, "Префикс создан"
except OSError as e:
return False, f"Ошибка создания префикса: {e}"
def delete_prefix(self, prefix_name):
"""
Удаляет префикс.
Args:
prefix_name: Имя префикса
Returns:
True если удаление успешно, False иначе
"""
prefix_path = self.get_prefix_path(prefix_name)
if not os.path.exists(prefix_path):
return False, f"Префикс '{prefix_name}' не найден"
try:
shutil.rmtree(prefix_path)
return True, "Префикс удален"
except OSError as e:
return False, f"Ошибка удаления префикса: {e}"
def read_prefix_config(self, prefix_name, config_file="last.conf"):
"""
Читает конфигурацию префикса.
Args:
prefix_name: Имя префикса
config_file: Имя файла конфигурации
Returns:
Словарь с переменными из конфигурации
"""
config_path = self.get_prefix_config_path(prefix_name, config_file)
if not os.path.exists(config_path):
return {}
all_vars = {}
try:
with open(config_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
match = re.match(r'export\s+(\w+)="([^"]*)"', line)
if match:
var_name = match.group(1)
var_value = match.group(2)
all_vars[var_name] = var_value
except (IOError, OSError):
return {}
return all_vars
def write_prefix_config(self, prefix_name, variables, config_file="last.conf"):
"""
Записывает конфигурацию префикса.
Args:
prefix_name: Имя префикса
variables: Словарь с переменными для записи
config_file: Имя файла конфигурации
Returns:
True если запись успешна, False иначе
"""
config_path = self.get_prefix_config_path(prefix_name, config_file)
try:
# Создаём директорию если не существует
os.makedirs(os.path.dirname(config_path), exist_ok=True)
existing_lines = []
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
existing_lines = f.readlines()
updated_vars = set()
new_lines = []
for line in existing_lines:
stripped = line.strip()
match = re.match(r'export\s+(\w+)="([^"]*)"', stripped)
if match:
var_name = match.group(1)
if var_name in variables:
new_lines.append(f'export {var_name}="{variables[var_name]}"\n')
updated_vars.add(var_name)
continue
new_lines.append(line)
for var_name in variables:
if var_name not in updated_vars:
new_lines.append(f'export {var_name}="{variables[var_name]}"\n')
with open(config_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
return True
except (IOError, OSError) as e:
print(f"Ошибка записи конфигурации: {e}")
return False
def get_prefix_component_version(self, prefix_name, component):
"""
Возвращает версию установленного компонента в префиксе.
Args:
prefix_name: Имя префикса
component: Имя компонента
Returns:
Версия компонента или пустая строка
"""
config = self.read_prefix_config(prefix_name)
return config.get(component, "")
def open_prefix_folder(self, prefix_name):
"""
Открывает папку префикса в файловом менеджере.
Args:
prefix_name: Имя префикса
Returns:
True если успешно, False иначе
"""
prefix_path = self.get_prefix_path(prefix_name)
if not os.path.isdir(prefix_path):
return False, f"Директория префикса не найдена: {prefix_path}"
try:
subprocess.Popen(['xdg-open', prefix_path])
return True, "Папка открыта"
except Exception as e:
return False, f"Ошибка открытия папки: {e}"
def get_prefixes_with_arch(self):
"""
Возвращает список префиксов с их архитектурой.
Returns:
Список кортежей (имя, архитектура)
"""
prefixes = self.list_prefixes()
result = []
for prefix_name in prefixes:
config = self.read_prefix_config(prefix_name)
arch = config.get('WINE_ARCH', 'unknown')
result.append((prefix_name, arch))
return result
+159
View File
@@ -0,0 +1,159 @@
"""
Сервис для выполнения и обработки процессов.
"""
import shlex
from PyQt5.QtCore import QProcess, QProcessEnvironment
from winehelperqt.utils.process_utils import ProcessUtils
class ProcessHandler:
"""
Сервис для выполнения процессов с обработкой вывода.
Предоставляет функциональность:
- Запуск процессов
- Обработка вывода (stdout/stderr)
- Обработка завершения процесса
"""
def __init__(self, parent=None):
"""
Инициализирует обработчик процессов.
Args:
parent: Родительский QObject
"""
self.parent = parent
self.process = None
self.output_buffer = ""
self.log_widget = None
def set_log_widget(self, log_widget):
"""
Устанавливает виджет для вывода логов.
Args:
log_widget: QTextEdit для вывода
"""
self.log_widget = log_widget
def run_command(self, executable, arguments, log_widget=None):
"""
Запускает команду и выводит лог.
Args:
executable: Путь к исполняемому файлу
arguments: Список аргументов
log_widget: QTextEdit для вывода (опционально)
"""
if log_widget:
self.log_widget = log_widget
self.process = QProcess(self.parent)
self.process.setProcessChannelMode(QProcess.MergedChannels)
self.process.setProcessEnvironment(QProcessEnvironment.systemEnvironment())
# Логирование команды
if self.log_widget:
cmd_line = f"Выполнение: {shlex.quote(executable)} {' '.join(shlex.quote(a) for a in arguments)}"
self._append_to_log(cmd_line)
self.process.start(executable, arguments)
return self.process
def run_simple_command(self, executable, arguments, log_widget=None):
"""
Запускает простую команду без сложной обработки.
Args:
executable: Путь к исполняемому файлу
arguments: Список аргументов
log_widget: QTextEdit для вывода (опционально)
"""
return self.run_command(executable, arguments, log_widget)
def _append_to_log(self, text, is_error=False):
"""Добавляет текст в лог."""
if not self.log_widget:
return
if is_error:
self.log_widget.append(f'<span style="color:red;">{text}</span>')
else:
self.log_widget.append(text)
def get_process(self):
"""Возвращает текущий процесс."""
return self.process
def is_running(self):
"""Проверяет, запущен ли процесс."""
return self.process and self.process.state() == QProcess.Running
def terminate(self):
"""Завершает процесс."""
ProcessUtils.stop_process(self.process)
def kill(self):
"""Убивает процесс."""
ProcessUtils.stop_process(self.process)
class CommandRunner:
"""
Утилита для запуска команд winehelper.
"""
def __init__(self, winehelper_path):
"""
Инициализирует исполнитель команд.
Args:
winehelper_path: Путь к скрипту winehelper
"""
self.winehelper_path = winehelper_path
def run(self, command, arguments, log_widget=None):
"""
Запускает команду winehelper.
Args:
command: Имя команды
arguments: Дополнительные аргументы
log_widget: QTextEdit для вывода (опционально)
"""
process_handler = ProcessHandler()
process_handler.set_log_widget(log_widget)
args = [command] + arguments
return process_handler.run_command(self.winehelper_path, args, log_widget)
def remove_prefix(self, prefix_name, log_widget=None):
"""
Удаляет префикс.
Args:
prefix_name: Имя префикса
log_widget: QTextEdit для вывода (опционально)
"""
return self.run("remove-prefix", [prefix_name, "--force"], log_widget)
def create_base_prefix(self, prefix_name, log_widget=None):
"""
Создает базовый префикс.
Args:
prefix_name: Имя префикса
log_widget: QTextEdit для вывода (опционально)
"""
return self.run("create-base-pfx", [prefix_name], log_widget)
def clear_winetricks_cache(self, log_widget=None):
"""
Очищает кеш winetricks.
Args:
log_widget: QTextEdit для вывода (опционально)
"""
return self.run("clear-winetricks-cache", ["--force"], log_widget)
+232
View File
@@ -0,0 +1,232 @@
"""
Сервис для управления скриптами установки WineHelper.
Предоставляет функциональность:
- Загрузка и фильтрация скриптов установки
- Получение информации о скриптах (имя, описание, иконки, URL)
- Поиск по скриптам
- Управление тестовыми скриптами
- Кэширование информации о скриптах
"""
import os
from typing import List, Optional
from dataclasses import dataclass
from winehelperqt.parsers.script_parser import ScriptParser
from winehelperqt.utils.cache_service import CacheService
@dataclass
class ScriptInfo:
"""Информация о скрипте установки."""
name: str # Имя программы
folder: str # Путь к папке скрипта
script_path: str # Путь к install.sh
description: str # Описание на русском
icons: List[str] # Список путей к иконкам
url: str # URL для скачивания (если есть)
def icon_paths(self) -> List[str]:
"""Возвращает полные пути к иконкам."""
return self.icons
class ScriptManager:
"""Сервис для управления скриптами установки."""
# Константы для папок со скриптами
AUTOINSTALL_DIR = "autoinstall"
MANUALINSTALL_DIR = "manualinstall"
TESTINSTALL_DIR = "testinstall"
# TTL для кэширования информации о скриптах (1 час)
SCRIPT_INFO_CACHE_TTL = 3600
def __init__(self, data_path: str, cache_service: Optional[CacheService] = None):
"""
Инициализирует менеджер скриптов.
Args:
data_path: Путь к директории данных WineHelper
cache_service: Сервис кэширования (опционально)
"""
self.data_path = data_path
self.cache_service = cache_service or CacheService(ttl=self.SCRIPT_INFO_CACHE_TTL)
self.parser = ScriptParser()
def get_script_folders(self, folder_name: str) -> List[str]:
"""
Возвращает список папок со скриптами.
Args:
folder_name: Имя папки (autoinstall, manualinstall, testinstall)
Returns:
Список путей к папкам со скриптами
"""
folder_path = os.path.join(self.data_path, folder_name)
if not os.path.isdir(folder_path):
return []
try:
return sorted([
os.path.join(folder_path, d) for d in os.listdir(folder_path)
if os.path.isdir(os.path.join(folder_path, d))
])
except OSError:
return []
def get_script_info(self, script_path: str) -> Optional[ScriptInfo]:
"""
Получает информацию о скрипте установки.
Args:
script_path: Путь к скрипту (install.sh)
Returns:
ScriptInfo или None если информация не найдена
"""
# Создаём ключ кэша на основе пути и времени модификации файла
cache_key = self._get_script_cache_key(script_path)
# Проверяем кэш
cached = self.cache_service.get(cache_key)
if cached is not None:
return cached
# Проверяем существование файла
if not os.path.exists(script_path):
return None
# Парсим информацию из скрипта
prog_name = self.parser.extract_prog_name_from_script(script_path)
description = self.parser.extract_info_ru(script_path)
icons = self.parser.extract_icons_from_script(script_path)
url = self.parser.extract_prog_url_from_script(script_path)
# Получаем папку скрипта
script_folder = os.path.dirname(script_path)
# Если имя программы не найдено, используем имя папки как fallback
if not prog_name:
prog_name = os.path.basename(script_folder)
script_info = ScriptInfo(
name=prog_name,
folder=script_folder,
script_path=script_path,
description=description or "",
icons=icons or [],
url=url or ""
)
# Сохраняем в кэш
self.cache_service.set(cache_key, script_info)
return script_info
def list_available_apps(self, folder_name: str) -> List[ScriptInfo]:
"""
Возвращает список доступных приложений из папки.
Args:
folder_name: Имя папки со скриптами
Returns:
Список ScriptInfo
"""
folders = self.get_script_folders(folder_name)
apps = []
for folder in folders:
script_file = os.path.join(folder, 'install.sh')
if os.path.exists(script_file):
info = self.get_script_info(script_file)
if info:
apps.append(info)
return apps
def get_autoinstall_scripts(self) -> List[ScriptInfo]:
"""Возвращает список скриптов для автоматической установки."""
return self.list_available_apps(self.AUTOINSTALL_DIR)
def get_manualinstall_scripts(self) -> List[ScriptInfo]:
"""Возвращает список скриптов для ручной установки."""
return self.list_available_apps(self.MANUALINSTALL_DIR)
def get_testinstall_scripts(self) -> List[ScriptInfo]:
"""Возвращает список скриптов для тестовой установки."""
return self.list_available_apps(self.TESTINSTALL_DIR)
def search_scripts(self, scripts: List[ScriptInfo], search_text: str) -> List[ScriptInfo]:
"""
Ищет скрипты по названию и описанию.
Args:
scripts: Список скриптов для поиска
search_text: Текст для поиска
Returns:
Отфильтрованный список скриптов
"""
if not search_text or not search_text.strip():
return scripts
search_lower = search_text.lower()
return [
script for script in scripts
if search_lower in script.name.lower() or
search_lower in script.description.lower()
]
def get_script_by_name(self, name: str, folder_name: str) -> Optional[ScriptInfo]:
"""
Находит скрипт по имени в указанной папке.
Args:
name: Имя скрипта
folder_name: Имя папки
Returns:
ScriptInfo или None
"""
scripts = self.list_available_apps(folder_name)
for script in scripts:
if script.name.lower() == name.lower():
return script
return None
def clear_cache(self) -> None:
"""Очищает кэш информации о скриптах."""
self.cache_service.clear()
def invalidate_script_cache(self, script_path: str) -> bool:
"""
Удаляет конкретный скрипт из кэша.
Args:
script_path: Путь к скрипту
Returns:
True если запись была в кэше
"""
cache_key = self._get_script_cache_key(script_path)
return self.cache_service.delete(cache_key)
def _get_script_cache_key(self, script_path: str) -> str:
"""
Создаёт ключ кэша для скрипта на основе пути и времени модификации.
Args:
script_path: Путь к скрипту
Returns:
Ключ для кэша
"""
try:
mtime = os.path.getmtime(script_path)
return f"script_info:{script_path}:{mtime}"
except OSError:
return f"script_info:{script_path}"
+228
View File
@@ -0,0 +1,228 @@
"""
Сервис для управления настройками WineHelper.
Предоставляет функциональность:
- Чтение/запись настроек префикса
- Дополнительные настройки приложения
- Backup/restore настроек
- Загрузка настроек по умолчанию
"""
import os
import shutil
from typing import Dict, Optional
from datetime import datetime
class SettingsManager:
"""
Сервис для управления настройками приложений и префиксов.
Примерты использования:
manager = SettingsManager(prefix_manager)
settings = manager.load_prefix_settings('my_prefix')
settings['WINE_ARCH'] = 'win32'
manager.save_prefix_settings('my_prefix', settings)
"""
def __init__(self, prefix_manager):
"""
Инициализирует менеджер настроек.
Args:
prefix_manager: Экземпляр PrefixManager
"""
self.prefix_manager = prefix_manager
def load_prefix_settings(self, prefix_name: str) -> Dict[str, str]:
"""
Загружает настройки префикса из last.conf.
Args:
prefix_name: Имя префикса
Returns:
Словарь с настройками
"""
return self.prefix_manager.read_prefix_config(prefix_name)
def save_prefix_settings(self, prefix_name: str, settings: Dict[str, str]) -> bool:
"""
Сохраняет настройки префикса в last.conf.
Args:
prefix_name: Имя префикса
settings: Словарь с настройками
Returns:
True если сохранение успешно
"""
return self.prefix_manager.write_prefix_config(prefix_name, settings)
def load_app_settings(self, desktop_path: str) -> Dict[str, str]:
"""
Загружает дополнительные настройки приложения.
Args:
desktop_path: Путь к .desktop файлу
Returns:
Словарь с настройками приложения
"""
if not os.path.exists(desktop_path):
return {}
settings = {}
try:
with open(desktop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
settings[key.strip()] = value.strip()
except (IOError, OSError):
pass
return settings
def save_app_settings(self, desktop_path: str, settings: Dict[str, str]) -> bool:
"""
Сохраняет дополнительные настройки приложения.
Args:
desktop_path: Путь к .desktop файлу
settings: Словарь с настройками
Returns:
True если сохранение успешно
"""
if not os.path.exists(desktop_path):
return False
try:
# Читаем существующие настройки
existing_settings = {}
with open(desktop_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
existing_settings[key.strip()] = value.strip()
# Объединяем с новыми настройками
existing_settings.update(settings)
# Перезаписываем файл
with open(desktop_path, 'w', encoding='utf-8') as f:
f.write("[Desktop Entry]\n")
for key, value in existing_settings.items():
f.write(f"{key}={value}\n")
return True
except (IOError, OSError) as e:
print(f"Ошибка сохранения настроек: {e}")
return False
def backup_settings(self, source_path: str, backup_dir: Optional[str] = None) -> Optional[str]:
"""
Создаёт резервную копию файла настроек.
Args:
source_path: Путь к файлу настроек
backup_dir: Директория для бэкапа (по умолчанию - рядом с файлом)
Returns:
Путь к файлу бэкапа или None
"""
if not os.path.exists(source_path):
return None
try:
# Определяем директорию для бэкапа
if backup_dir is None:
backup_dir = os.path.join(
os.path.dirname(source_path),
"backups"
)
os.makedirs(backup_dir, exist_ok=True)
# Формируем имя файла бэкапа
filename = os.path.basename(source_path)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"{filename}.{timestamp}.backup"
backup_path = os.path.join(backup_dir, backup_filename)
# Копируем файл
shutil.copy2(source_path, backup_path)
return backup_path
except (IOError, OSError) as e:
print(f"Ошибка создания бэкапа: {e}")
return None
def restore_settings(self, backup_path: str, target_path: str) -> bool:
"""
Восстанавливает настройки из бэкапа.
Args:
backup_path: Путь к файлу бэкапа
target_path: Путь для восстановления
Returns:
True если восстановление успешно
"""
if not os.path.exists(backup_path):
return False
try:
# Создаём директорию назначения
target_dir = os.path.dirname(target_path)
if target_dir:
os.makedirs(target_dir, exist_ok=True)
# Копируем файл бэкапа
shutil.copy2(backup_path, target_path)
return True
except (IOError, OSError) as e:
print(f"Ошибка восстановления настроек: {e}")
return False
def get_default_settings(self, prefix_name: str) -> Dict[str, str]:
"""
Возвращает настройки по умолчанию для префикса.
Args:
prefix_name: Имя префикса
Returns:
Словарь с настройками по умолчанию
"""
return {
'WINE_ARCH': 'win64',
'WINEESYNC': '0',
'WINEFSYNC': '0',
'WH_WINDOWS_VER': '10',
'WH_USE_SHADER_CACHE': '1',
'WH_USE_MESA_GL_OVERRIDE': '0',
'WH_USE_WINE_DXGI': '0',
'WINE_LARGE_ADDRESS_AWARE': '1',
'WINE_FULLSCREEN_FSR': '1',
}
def merge_settings(self, base: Dict[str, str],
override: Dict[str, str]) -> Dict[str, str]:
"""
Объединяет две конфигурации настроек.
Args:
base: Базовые настройки
override: Переопределяющие настройки
Returns:
Объединённый словарь
"""
result = base.copy()
result.update(override)
return result
+147
View File
@@ -0,0 +1,147 @@
"""
Сервисы для управления кэшем и компонентами Winetricks.
"""
import os
import hashlib
from pathlib import Path
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
from winehelperqt.utils.cache_service import FileCacheService
class WinetricksCacheService:
"""
Сервис для управления кэшем Winetricks.
Использует FileCacheService для хранения + проверку хеша winetricks.
"""
DEFAULT_CACHE_TTL_SECONDS = 86400
CACHE_DIR = Path.home() / ".cache" / "winehelper" / "winetricks"
def __init__(self, cache_dir=None, cache_ttl=None):
"""
Инициализирует сервис кэша.
Args:
cache_dir: Путь к директории кэша
cache_ttl: Время жизни кэша в секундах
"""
self.cache_dir = Path(cache_dir) if cache_dir else self.CACHE_DIR
self.cache_ttl = cache_ttl if cache_ttl is not None else self.DEFAULT_CACHE_TTL_SECONDS
self._ensure_cache_dir()
self._file_cache = FileCacheService(cache_dir=str(self.cache_dir), cache_ttl=self.cache_ttl)
def _ensure_cache_dir(self):
"""Создает директорию кэша если она не существует."""
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _get_winetricks_hash(self, winetricks_path):
"""
Вычисляет SHA256 хэш файла winetricks.
Args:
winetricks_path: Путь к файлу winetricks
"""
try:
hasher = hashlib.sha256()
with open(winetricks_path, 'rb') as f:
while chunk := f.read(4096):
hasher.update(chunk)
return hasher.hexdigest()
except (IOError, OSError):
return None
def get_cached_data(self, category, winetricks_path):
"""
Получает данные из кэша если они актуальны.
Args:
category: Категория компонентов
winetricks_path: Путь к файлу winetricks
"""
cached = self._file_cache.get_json(category)
if cached is None:
return None
winetricks_hash = self._get_winetricks_hash(winetricks_path)
if cached.get("hash") != winetricks_hash:
return None
return cached.get("output")
def save_to_cache(self, category, output, winetricks_path):
"""
Сохраняет данные в кэш.
Args:
category: Категория компонентов
output: Данные для сохранения
winetricks_path: Путь к файлу winetricks
"""
winetricks_hash = self._get_winetricks_hash(winetricks_path)
if not winetricks_hash:
return False
cache_data = {
"hash": winetricks_hash,
"output": output
}
return self._file_cache.set_json(category, cache_data)
def clear_cache(self):
"""
Очищает весь кэш Winetricks.
"""
return self._file_cache.clear()
def is_cache_valid(self, category, winetricks_path):
"""
Проверяет актуальность кэша для категории.
"""
return self.get_cached_data(category, winetricks_path) is not None
def get_cache_age(self, category):
"""
Возвращает возраст кэша в секундах.
"""
return self._file_cache.get_cache_age(category)
class WinetricksLogReader:
"""
Сервис для чтения и анализа лога Winetricks.
"""
def __init__(self, prefix_path):
"""
Инициализирует читатель лога.
Args:
prefix_path: Путь к префиксу Wine
"""
self.prefix_path = prefix_path
self.log_path = os.path.join(prefix_path, "winetricks.log")
def get_installed_verbs(self):
"""
Читает winetricks.log и возвращает множество установленных компонентов.
"""
return WinetricksOutputParser.parse_installed_verbs(self.log_path)
def is_verb_installed(self, verb):
"""
Проверяет, установлен ли указанный компонент.
"""
installed_verbs = self.get_installed_verbs()
return verb in installed_verbs
def get_log_path(self):
"""Возвращает путь к файлу лога."""
return self.log_path
def log_exists(self):
"""Проверяет существование файла лога."""
return os.path.exists(self.log_path)
+12
View File
@@ -0,0 +1,12 @@
"""
Утилиты для winehelper GUI.
"""
from winehelperqt.utils.log_handler import LogProgressHandler, MultiLogHandler
from winehelperqt.utils.cache_service import CacheService, FileCacheService
__all__ = [
'LogProgressHandler',
'MultiLogHandler',
'CacheService',
'FileCacheService',
]
+334
View File
@@ -0,0 +1,334 @@
"""
Универсальный сервис кэширования для WineHelper.
Предоставляет функциональность:
- TTL-based кэширование (time-to-live)
- In-memory кэш для быстрых операций
- File-based кэш для постоянных данных
- Автоматическая очистка устаревших записей
"""
import json
import time
import hashlib
from pathlib import Path
from typing import Any, Optional, Dict
class CacheEntry:
"""Запись в кэше с метаданными."""
def __init__(self, data: Any, ttl: Optional[float] = None):
"""
Инициализирует запись кэша.
Args:
data: Данные для кэширования
ttl: Время жизни в секундах (None = бессрочно)
"""
self.data = data
self.timestamp = time.time()
self.ttl = ttl
def is_expired(self) -> bool:
"""Проверяет, истекло ли время жизни записи."""
if self.ttl is None:
return False
return (time.time() - self.timestamp) > self.ttl
def age(self) -> float:
"""Возвращает возраст записи в секундах."""
return time.time() - self.timestamp
class CacheService:
"""Универсальный сервис кэширования с поддержкой TTL."""
def __init__(self, ttl: Optional[float] = None, max_size: int = 1000):
"""
Инициализирует сервис кэширования.
Args:
ttl: Время жизни по умолчанию (в секундах)
max_size: Максимальный размер кэша в памяти
"""
self._cache: Dict[str, CacheEntry] = {}
self._default_ttl = ttl
self._max_size = max_size
def get(self, key: str, default: Any = None) -> Any:
"""
Получает значение из кэша.
Args:
key: Ключ
default: Значение по умолчанию, если ключ не найден
Returns:
Значение из кэша или default
"""
entry = self._cache.get(key)
if entry is None:
return default
if entry.is_expired():
self._cache.pop(key, None)
return default
return entry.data
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
"""
Сохраняет значение в кэш.
Args:
key: Ключ
value: Значение
ttl: Время жизни (по умолчанию из настроек сервиса)
"""
# Проверяем размер кэша
if len(self._cache) >= self._max_size and key not in self._cache:
self._evict_expired()
# Если всё ещё превышен размер, удаляем oldest entry
if len(self._cache) >= self._max_size and key not in self._cache:
self._evict_oldest()
effective_ttl = ttl if ttl is not None else self._default_ttl
self._cache[key] = CacheEntry(value, effective_ttl)
def delete(self, key: str) -> bool:
"""
Удаляет запись из кэша.
Args:
key: Ключ для удаления
Returns:
True если запись была удалена
"""
return self._cache.pop(key, None) is not None
def has(self, key: str) -> bool:
"""
Проверяет наличие ключа в кэше.
Args:
key: Ключ
Returns:
True если ключ существует и не истек
"""
entry = self._cache.get(key)
if entry is None:
return False
if entry.is_expired():
self._cache.pop(key, None)
return False
return True
def clear(self) -> None:
"""Очищает весь кэш."""
self._cache.clear()
def size(self) -> int:
"""Возвращает количество записей в кэше."""
return len(self._cache)
def _evict_expired(self) -> None:
"""Удаляет все истекшие записи."""
expired_keys = [
key for key, entry in self._cache.items()
if entry.is_expired()
]
for key in expired_keys:
del self._cache[key]
def _evict_oldest(self) -> None:
"""Удаляет самую старую запись."""
if not self._cache:
return
oldest_key = min(
self._cache.keys(),
key=lambda k: self._cache[k].timestamp
)
del self._cache[oldest_key]
def cleanup(self) -> int:
"""
Очищает истекшие записи.
Returns:
Количество удалённых записей
"""
before = len(self._cache)
self._evict_expired()
return before - len(self._cache)
class FileCacheService:
"""Сервис файлового кэширования для постоянных данных."""
DEFAULT_CACHE_TTL_SECONDS = 86400 # 24 часа
def __init__(self, cache_dir: Optional[str] = None, cache_ttl: Optional[float] = None):
"""
Инициализирует сервис файлового кэша.
Args:
cache_dir: Путь к директории кэша
cache_ttl: Время жизни кэша в секундах
"""
self.cache_dir = Path(cache_dir) if cache_dir else self._default_cache_dir()
self.cache_ttl = cache_ttl if cache_ttl is not None else self.DEFAULT_CACHE_TTL_SECONDS
self._ensure_cache_dir()
def _default_cache_dir(self) -> Path:
"""Возвращает директорию кэша по умолчанию."""
return Path.home() / ".cache" / "winehelper"
def _ensure_cache_dir(self) -> None:
"""Создаёт директорию кэша, если она не существует."""
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _get_cache_path(self, key: str) -> Path:
"""
Возвращает путь к файлу кэша.
Args:
key: Ключ (будет преобразован в безопасное имя файла)
Returns:
Путь к файлу кэша
"""
# Хэшируем ключ для безопасного имени файла
key_hash = hashlib.md5(key.encode()).hexdigest()
return self.cache_dir / f"{key_hash}.cache"
def get_json(self, key: str, default: Any = None) -> Any:
"""
Получает JSON данные из файлового кэша.
Args:
key: Ключ
default: Значение по умолчанию
Returns:
Данные из кэша или default
"""
cache_path = self._get_cache_path(key)
if not cache_path.exists():
return default
try:
with open(cache_path, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
# Проверяем возраст кэша
cache_age = time.time() - cache_data.get("timestamp", 0)
if cache_age < self.cache_ttl:
return cache_data.get("data")
else:
# Кэш устарел, удаляем
cache_path.unlink(missing_ok=True)
return default
except (json.JSONDecodeError, IOError, KeyError) as e:
print(f"Кэш '{key}' повреждён: {e}")
return default
def set_json(self, key: str, data: Any) -> bool:
"""
Сохраняет данные в файловый кэш как JSON.
Args:
key: Ключ
data: Данные для сохранения
Returns:
True если сохранение успешно
"""
cache_path = self._get_cache_path(key)
cache_data = {
"timestamp": time.time(),
"data": data
}
try:
with open(cache_path, 'w', encoding='utf-8') as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
return True
except (IOError, OSError) as e:
print(f"Не удалось сохранить кэш '{key}': {e}")
return False
def delete(self, key: str) -> bool:
"""
Удаляет запись из файлового кэша.
Args:
key: Ключ для удаления
Returns:
True если запись была удалена
"""
cache_path = self._get_cache_path(key)
try:
cache_path.unlink(missing_ok=True)
return True
except OSError:
return False
def clear(self) -> bool:
"""
Очищает весь файловый кэш.
Returns:
True если очистка успешна
"""
try:
if self.cache_dir.exists():
for file_path in self.cache_dir.glob("*.cache"):
file_path.unlink()
return True
except (IOError, OSError) as e:
print(f"Ошибка очистки кэша: {e}")
return False
def get_cache_age(self, key: str) -> Optional[float]:
"""
Возвращает возраст кэша в секундах.
Args:
key: Ключ
Returns:
Возраст в секундах или None
"""
cache_path = self._get_cache_path(key)
if not cache_path.exists():
return None
try:
with open(cache_path, 'r', encoding='utf-8') as f:
cache_data = json.load(f)
return time.time() - cache_data.get("timestamp", 0)
except (json.JSONDecodeError, IOError, KeyError):
return None
def is_valid(self, key: str) -> bool:
"""
Проверяет актуальность кэша.
Args:
key: Ключ
Returns:
True если кэш актуален
"""
return self.get_json(key) is not None
+28
View File
@@ -0,0 +1,28 @@
"""
Единый модуль для переменных окружения WineHelper.
Устраняет дублирование класса Var в нескольких файлах.
"""
import os
class EnvironmentConfig:
"""Полная конфигурация приложения из переменных окружения."""
# Переменные определяемые в скрипте winehelper
SCRIPT_NAME = os.environ.get("SCRIPT_NAME")
USER_WORK_PATH = os.environ.get("USER_WORK_PATH")
RUN_SCRIPT = os.environ.get("RUN_SCRIPT")
DATA_PATH = os.environ.get("DATA_PATH")
CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE")
WH_ICON_PATH = os.environ.get("WH_ICON_PATH")
WH_ICON_TRAY = os.environ.get("WH_ICON_TRAY")
LICENSE_FILE = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE")
GENERAL = os.environ.get("GENERAL")
WH_WINETRICKS = os.environ.get("WH_WINETRICKS")
WH_DIST_DIR = os.environ.get("WH_DIST_DIR")
# Alias
Var = EnvironmentConfig
+230
View File
@@ -0,0 +1,230 @@
"""
Обработчик логов с поддержкой прогресс-баров.
Обрабатывает вывод процессов с корректным отображением однострочного прогресса.
"""
import re
from PyQt5.QtGui import QTextCursor
class LogProgressHandler:
"""
Обработчик логов для процессов с прогресс-барами.
Корректно обрабатывает строки с '\r' (carriage return),
заменяя предыдущие строки прогресса новыми, вместо добавления новых строк.
"""
PROGRESS_PERCENT_PATTERN = re.compile(r'\d\s*%')
def __init__(self, log_widget=None):
"""
Инициализирует обработчик логов.
Args:
log_widget: Виджет QTextEdit для вывода логов
"""
self.log_widget = log_widget
self.output_buffer = ""
self.last_line_was_progress = False
def set_log_widget(self, log_widget):
"""Устанавливает виджет для вывода логов."""
self.log_widget = log_widget
def process_output(self, raw_output):
"""
Обрабатывает вывод процесса, разбивая на строки и обрабатывая прогресс.
Args:
raw_output: Сырой вывод от процесса
"""
if not self.log_widget:
return
self.output_buffer += raw_output
while True:
idx_n = self.output_buffer.find('\n')
idx_r = self.output_buffer.find('\r')
if idx_n == -1 and idx_r == -1:
break
split_idx = min(idx for idx in [idx_n, idx_r] if idx != -1)
line = self.output_buffer[:split_idx + 1]
self.output_buffer = self.output_buffer[split_idx + 1:]
self._process_log_line(line)
def process_line(self, line):
"""
Обрабатывает одну строку лога.
Args:
line: Строка лога (может содержать '\r' или '\n')
"""
if not self.log_widget:
return
self._process_log_line(line)
def finalize(self):
"""
Завершает обработку лога.
Обрабатывает остаток буфера и добавляет перенос строки если нужно.
"""
if not self.log_widget:
return
if self.output_buffer:
self._process_log_line(self.output_buffer)
if self.last_line_was_progress:
cursor = self.log_widget.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText("\n")
self._reset_state()
def _process_log_line(self, line_with_delimiter):
"""
Обрабатывает одну строку лога, управляя заменой строк прогресса.
Args:
line_with_delimiter: Строка с разделителем (\n или \r)
"""
is_progress_line = '\r' in line_with_delimiter
if is_progress_line:
if not self.PROGRESS_PERCENT_PATTERN.search(line_with_delimiter):
return
clean_line = line_with_delimiter.replace('\r', '').replace('\n', '').strip()
if not clean_line:
return
cursor = self.log_widget.textCursor()
if is_progress_line and self.last_line_was_progress:
cursor.movePosition(QTextCursor.End)
cursor.select(QTextCursor.LineUnderCursor)
cursor.removeSelectedText()
elif not is_progress_line and self.last_line_was_progress:
cursor.movePosition(QTextCursor.End)
cursor.select(QTextCursor.LineUnderCursor)
cursor.removeSelectedText()
add_newline = not is_progress_line
self._append_to_log(clean_line, is_error=False, add_newline=add_newline)
self.last_line_was_progress = is_progress_line
def _append_to_log(self, text, is_error=False, add_newline=True):
"""
Добавляет сообщение в лог.
Args:
text: Текст сообщения
is_error: True если это ошибка (красный цвет)
add_newline: True если добавить перенос строки
"""
if not self.log_widget:
return
cursor = self.log_widget.textCursor()
cursor.movePosition(QTextCursor.End)
if is_error:
cursor.insertHtml(f'<font color="red">{text}</font><br>')
else:
formatted_text = f"{text}\n" if add_newline else text
cursor.insertText(formatted_text)
self.log_widget.ensureCursorVisible()
def _reset_state(self):
"""Сбрасывает состояние обработчика."""
self.last_line_was_progress = False
self.output_buffer = ""
def clear(self):
"""Очищает лог и сбрасывает состояние."""
if self.log_widget:
self.log_widget.clear()
self._reset_state()
def append_error(self, text):
"""
Добавляет сообщение об ошибке в лог.
Args:
text: Текст ошибки
"""
self._append_to_log(text, is_error=True, add_newline=True)
def append_info(self, text, add_newline=True):
"""
Добавляет информационное сообщение в лог.
Args:
text: Текст сообщения
add_newline: True если добавить перенос строки
"""
self._append_to_log(text, is_error=False, add_newline=add_newline)
class MultiLogHandler:
"""
Обработчик для управления несколькими LogProgressHandler.
Полезен когда нужно обрабатывать логи для разных процессов одновременно.
"""
def __init__(self):
"""Инициализирует хранилище обработчиков."""
self.handlers = {}
def get_handler(self, name, log_widget):
"""
Получает или создает обработчик для указанного виджета.
Args:
name: Имя обработчика (например, "install", "command")
log_widget: Виджет QTextEdit для вывода
Returns:
LogProgressHandler для указанного виджета
"""
if name not in self.handlers:
self.handlers[name] = LogProgressHandler(log_widget)
else:
self.handlers[name].set_log_widget(log_widget)
return self.handlers[name]
def get_handler_or_none(self, name):
"""
Получает обработчик по имени или None если не существует.
Args:
name: Имя обработчика
Returns:
LogProgressHandler или None
"""
return self.handlers.get(name)
def remove_handler(self, name):
"""
Удаляет обработчик по имени.
Args:
name: Имя удаляемого обработчика
"""
if name in self.handlers:
del self.handlers[name]
def clear_all(self):
"""Очищает все обработчики."""
for handler in self.handlers.values():
handler.clear()
self.handlers.clear()
+43
View File
@@ -0,0 +1,43 @@
"""
Утилиты для работы с процессами QProcess.
Устраняет дублирование кода остановки процессов в разных частях приложения.
"""
from PyQt5.QtCore import QProcess
class ProcessUtils:
"""Утилиты для управления QProcess."""
# Таймаут ожидания после terminate (мс)
DEFAULT_TERMINATE_TIMEOUT = 3000
@staticmethod
def stop_process(process: QProcess, timeout: int = DEFAULT_TERMINATE_TIMEOUT) -> None:
"""
Останавливает QProcess: сначала terminate, потом kill если не завершился.
Args:
process: Процесс для остановки
timeout: Время ожидания после terminate (мс)
"""
if not process:
return
if process.state() == QProcess.Running:
process.terminate()
if not process.waitForFinished(timeout):
process.kill()
@staticmethod
def is_running(process: QProcess) -> bool:
"""
Проверяет, запущен ли процесс.
Args:
process: Процесс для проверки
Returns:
True если процесс запущен
"""
return process is not None and process.state() == QProcess.Running
+8
View File
@@ -0,0 +1,8 @@
"""
Виджеты для winehelper GUI.
"""
from winehelperqt.widgets.icon_manager import IconManager
__all__ = [
'IconManager',
]
+307
View File
@@ -0,0 +1,307 @@
"""
Сервис для управления иконками и анимациями в WineHelper GUI.
Предоставляет функциональность:
- Создание анимированных кнопок с иконками
- Плавные переходы между иконками (fade анимация)
- Создание цветных иконок (для тёмных/светлых тем)
- Кэширование иконок
- Извлечение иконок из .exe файлов
"""
import os
from typing import List, Optional, Dict, Tuple
from PyQt5.QtCore import Qt, QObject, QTimer, QSize, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor
from PyQt5.QtWidgets import QToolButton, QSizePolicy, QFrame, QVBoxLayout
class IconManager(QObject):
"""
Сервис для управления иконками и анимациями.
Примерты использования:
manager = IconManager(parent_widget)
button = manager.create_animated_button(
text="My App",
icon_paths=["/path/to/icon1.png", "/path/to/icon2.png"],
style_sheet="QToolButton { ... }"
)
"""
# Интервал смены иконок (мс)
DEFAULT_ICON_INTERVAL = 2500
# Длительность анимации fade (мс)
FADE_DURATION = 700
def __init__(self, parent: Optional[QObject] = None):
"""
Инициализирует менеджер иконок.
Args:
parent: Родительский объект
"""
super().__init__(parent)
self._icon_animators: Dict[QToolButton, Dict] = {}
self._icon_cache: Dict[str, QIcon] = {}
def create_animated_button(self, text: str,
icon_paths: List[str],
style_sheet: str,
icon_size: int = 32,
min_width: int = 250,
height: int = 70) -> QToolButton:
"""
Создает кнопку с анимацией смены иконок.
Args:
text: Текст кнопки
icon_paths: Пути к иконкам
style_sheet: Стили для кнопки
icon_size: Размер иконки
min_width: Минимальная ширина
height: Высота кнопки
Returns:
Настроенная кнопка
"""
# Создаём кнопку
btn = QToolButton()
btn.setText(text)
btn.setStyleSheet(style_sheet)
btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
btn.setFocusPolicy(Qt.NoFocus)
btn.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
btn.setMinimumWidth(min_width)
btn.setFixedHeight(height)
btn.setIconSize(QSize(icon_size, icon_size))
# Фильтруем существующие пути
existing_paths = [p for p in icon_paths if p and os.path.exists(p)]
if existing_paths:
if len(existing_paths) > 1:
# Устанавливаем первую иконку и запускаем анимацию
btn.setIcon(QIcon(existing_paths[0]))
# Создаём таймер для смены иконок
main_timer = QTimer(btn)
self._icon_animators[btn] = {
'main_timer': main_timer,
'icons': existing_paths,
'current_index': 0,
'animation': None
}
main_timer.timeout.connect(
lambda b=btn: self._start_fade_animation(b)
)
main_timer.start(self.DEFAULT_ICON_INTERVAL)
else:
# Одна иконка без анимации
btn.setIcon(QIcon(existing_paths[0]))
else:
# Пустая иконка для сохранения отступов
btn.setIcon(QIcon())
return btn
def create_button_with_frame(self, text: str,
icon_paths: List[str],
style_sheet: str,
frame_style: str) -> Tuple[QFrame, QToolButton]:
"""
Создает кнопку в рамке для выделения.
Args:
text: Текст кнопки
icon_paths: Пути к иконкам
style_sheet: Стили для кнопки
frame_style: Стили для рамки
Returns:
Кортеж (рамка, кнопка)
"""
btn = self.create_animated_button(text, icon_paths, style_sheet)
frame = QFrame()
frame.setStyleSheet(frame_style)
layout = QVBoxLayout(frame)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(btn)
return frame, btn
def create_colorized_icon(self, icon_path: str, color: str) -> QIcon:
"""
Загружает иконку (SVG) и применяет указанный цвет.
Args:
icon_path: Путь к иконке
color: Цвет в формате Qt/Hex
Returns:
Перекрашенная иконка
"""
target_pixmap = QPixmap(icon_path)
if target_pixmap.isNull():
return QIcon()
painter = QPainter(target_pixmap)
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
painter.fillRect(target_pixmap.rect(), QColor(color))
painter.end()
return QIcon(target_pixmap)
def cache_icon(self, key: str, icon: QIcon) -> None:
"""
Сохраняет иконку в кэш.
Args:
key: Ключ кэша
icon: Иконка для сохранения
"""
self._icon_cache[key] = icon
def get_cached_icon(self, key: str) -> Optional[QIcon]:
"""
Получает иконку из кэша.
Args:
key: Ключ кэша
Returns:
Иконка или None
"""
return self._icon_cache.get(key)
def clear_icon_cache(self) -> None:
"""Очищает кэш иконок."""
self._icon_cache.clear()
def set_selection_style(self, frame: QFrame, selected: bool) -> None:
"""
Устанавливает стиль выделения для рамки кнопки.
Args:
frame: Рамка кнопки
selected: True для выделения
"""
if selected:
frame.setStyleSheet(
"QFrame { border: 2px solid #0078d7; border-radius: 8px; "
"padding: 0px; background-color: palette(window); }"
)
else:
frame.setStyleSheet(
"QFrame { border: 2px solid transparent; border-radius: 8px; "
"padding: 0px; background-color: palette(window); }"
)
def stop_button_animation(self, button: QToolButton) -> None:
"""
Останавливает анимацию для кнопки.
Args:
button: Кнопка для остановки анимации
"""
if button in self._icon_animators:
anim_data = self._icon_animators.pop(button)
if 'main_timer' in anim_data:
anim_data['main_timer'].stop()
if 'animation' in anim_data and anim_data['animation']:
anim_data['animation'].stop()
def cleanup(self) -> None:
"""Останавливает все анимации и очищает ресурсы."""
for button in list(self._icon_animators.keys()):
self.stop_button_animation(button)
self.clear_icon_cache()
def _start_fade_animation(self, button: QToolButton) -> None:
"""
Запускает анимацию плавного перехода между иконками.
Args:
button: Кнопка для анимации
"""
if button not in self._icon_animators:
return
anim_data = self._icon_animators[button]
# Получаем или создаём объект анимации
animation = anim_data.get('animation')
if not animation:
button.setProperty("fadeProgress", 0.0)
animation = QPropertyAnimation(button, b"fadeProgress", self)
animation.setDuration(self.FADE_DURATION)
animation.setEasingCurve(QEasingCurve.InOutQuad)
animation.valueChanged.connect(
lambda value, b=button: self._update_icon_blend(b, value)
)
animation.finished.connect(
lambda b=button: self._on_animation_finished(b)
)
anim_data['animation'] = animation
# Останавливаем предыдущую анимацию
if animation.state() == QPropertyAnimation.Running:
animation.stop()
# Определяем текущую и следующую иконки
current_icon_path = anim_data['icons'][anim_data['current_index']]
next_icon_index = (anim_data['current_index'] + 1) % len(anim_data['icons'])
next_icon_path = anim_data['icons'][next_icon_index]
# Сохраняем QPixmap для использования в функции обновления
anim_data['pixmaps'] = (QPixmap(current_icon_path), QPixmap(next_icon_path))
# Запускаем анимацию
animation.setStartValue(0.0)
animation.setEndValue(1.0)
animation.start()
def _on_animation_finished(self, button: QToolButton) -> None:
"""Обработчик завершения анимации."""
if button in self._icon_animators:
anim_data = self._icon_animators[button]
anim_data['current_index'] = (anim_data['current_index'] + 1) % len(anim_data['icons'])
def _update_icon_blend(self, button: QToolButton, progress: float) -> None:
"""
Обновляет кадр анимации, смешивая две иконки.
Args:
button: Кнопка
progress: Прогресс от 0.0 до 1.0
"""
anim_data = self._icon_animators.get(button)
if not anim_data or 'pixmaps' not in anim_data:
return
old_pixmap, new_pixmap = anim_data['pixmaps']
# На последнем кадре устанавливаем новую иконку
if progress >= 1.0:
button.setIcon(QIcon(new_pixmap))
return
# Создаём холст для отрисовки
canvas = QPixmap(button.iconSize())
canvas.fill(Qt.transparent)
painter = QPainter(canvas)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
# Плавно скрываем старую иконку
painter.setOpacity(1.0 - progress)
painter.drawPixmap(canvas.rect(), old_pixmap)
# Плавно проявляем новую иконку
painter.setOpacity(progress)
painter.drawPixmap(canvas.rect(), new_pixmap)
painter.end()
button.setIcon(QIcon(canvas))