Compare commits

..
174 Commits
Author SHA1 Message Date
Mikhail Tergoev 266302d643 updated adem pfx with jet40 2026-10-28 08:22:17 +03:00
Mikhail Tergoev 38d3a43c03 fix: dont mkdir prefix if break backup 2026-10-28 08:09:57 +03:00
Mikhail Tergoev c2c5a05013 docs: update AGENTS.md 2026-10-20 16:59:47 +03:00
Mikhail Tergoev 410b24c9b5 Merge branch 'minergenon-link' 2026-10-20 16:49:30 +03:00
Sergey Palcheh a525b9f749 docs: replace CLAUDE.md with symlink to AGENTS.md 2026-10-19 12:36:04 +06:00
Mikhail Tergoev 3c856cd890 added wine-11.19-alt-tkg-wow64 2026-10-16 17:13:19 +03:00
Mikhail Tergoev c2bf75d0da Merge branch 'minergenon-fix/gui-critical-batch1' 2026-10-16 15:20:42 +03:00
Sergey Palcheh 50a2498abb fix(gui): scroll CPCSP_PROXY dialog log to the end on finish 2026-10-16 10:57:57 +06:00
Sergey Palcheh 53ec0f88bd refactor(gui): remove work path setting from settings tab 2026-10-16 10:33:50 +06:00
Sergey Palcheh b8cebe18b7 gui: use system fonts, fix Qt6 deprecations and revive winetricks error branch 2026-10-15 20:44:05 +06:00
Sergey Palcheh fe4473ef76 perf(gui): single-pass script parser, grid caches, paused offscreen timers; license parity with CLI 2026-10-15 18:59:40 +06:00
Sergey Palcheh 7c53112cc9 refactor(gui): info panel controller, tab name constants, dead code cleanup 2026-10-15 17:30:20 +06:00
Sergey Palcheh 453b5a5a99 fix(gui): icon animation leaks, GUI thread blocks, dialog crashes, Qt6 exit hang 2026-10-15 16:07:26 +06:00
Mikhail Tergoev 4990ac941a added support for grdwine system libraries 2026-10-13 13:57:31 +03:00
Mikhail Tergoev 656e20eaa1 added manualinstall: adem 2026-10-09 15:35:33 +03:00
Mikhail Tergoev 72ccbaa747 Merge branch 'minergenon-cript' 2026-10-09 14:55:39 +03:00
Mikhail Tergoev e4bafa7a1f fixed switching between x11 and Wayland 2026-10-09 14:55:18 +03:00
Mikhail Tergoev 271fb70d74 scada-krug-2000: updated wine with fix for demo-server 2026-10-08 14:52:34 +03:00
Mikhail Tergoev 0884034a31 autoinstall: added Zet9 Lite 2026-10-08 10:24:35 +03:00
Sergey Palcheh fe37c07729 fix: use qt_compat instead of direct PyQt5 import in cryptopro_warning 2026-10-01 09:19:36 +06:00
Mikhail Tergoev 5f506223ac Merge branch 'minergenon-pyqt6-migration' 2026-09-30 11:39:21 +03:00
Mikhail Tergoev 0590170cd2 Merge branch 'minergenon-crpr' 2026-09-30 11:38:34 +03:00
Sergey Palcheh ae4eed8be9 fixing a typo in AGENTS.md 2026-09-30 10:47:38 +06:00
Sergey Palcheh 0231f7fe3f fix(gui): always show Russian button texts and startup log line 2026-09-30 10:39:53 +06:00
Sergey Palcheh e4e7828420 feat(gui): support PyQt6 alongside PyQt5 via qt_compat layer 2026-09-29 12:19:53 +06:00
Mikhail Tergoev 41ec383343 docs: add CLAUDE.md with CLI usage and podman repro recipe 2026-09-11 20:03:23 +05:00
Mikhail Tergoev 3591f657f9 scada-krug-2000: install from MSI, move to autoinstall
Root setup.exe in the vendor archive is an AutoRunHTML menu that ignores
/quiet and waits for clicks, so the install hung forever (bug #59578).
Download 01_scada_v4_4_release.zip, unpack it and run
DeveloperStudio/SCADA KRUG 2000.msi with /qn directly. Kill leftover
KrugTaskManager/SMTListener/KrugTaskStarter via wineserver -k instead of
the racy background kill_autostart.

Closes: https://bugzilla.altlinux.org/59578
2026-09-11 20:03:23 +05:00
Sergey Palcheh db88a6ebd0 cryptopro_warning module has been added 2026-09-03 09:06:10 +06:00
Mikhail Tergoev 691f13b046 Merge branch 'minergenon-test' 2026-09-02 20:57:02 +03:00
Sergey Palcheh b865533a4b refactor(gui): drop unused package-level re-exports from __init__ modules 2026-09-02 17:01:05 +06:00
Sergey Palcheh df22c2b1db refactor(gui): use DEFAULT_WINEDEBUG constant instead of literal value 2026-09-02 17:01:04 +06:00
Sergey Palcheh 3bf33340b9 refactor(gui): extract shared QProcess setup and widget-deleted guard into utils 2026-09-02 17:00:35 +06:00
Sergey Palcheh 1c221ed1cc refactor(gui): remove dead code 2026-09-02 16:19:57 +06:00
Sergey Palcheh 5fa4514665 feat(gui): distinguish user-aborted commands from errors in log 2026-09-02 16:04:14 +06:00
Sergey Palcheh a5e339b112 refactor(gui): replace closeEvent monkey-patch with InstallDialog subclass 2026-09-02 16:04:01 +06:00
Sergey Palcheh b33f476fc9 refactor(gui): two-phase UI construction in version selection dialogs 2026-09-02 13:43:32 +06:00
Sergey Palcheh 22d76d4bea refactor(gui): unify prefix command execution in prefix tab 2026-09-02 13:14:22 +06:00
Sergey Palcheh 6a5aeed900 refactor(gui): unify winetricks.log parsing in prefix info panel 2026-09-02 12:39:27 +06:00
Sergey Palcheh b462a4729c feat(gui): show empty-state hints for app grids and prefix list 2026-09-01 13:43:02 +06:00
Sergey Palcheh fb79aeaddb refactor(gui): expose cross-object APIs and move wine-util processes to prefix tab 2026-09-01 13:12:00 +06:00
Sergey Palcheh 47fbff3a0f fix(gui): delete finished QProcess objects to stop memory leaks 2026-08-31 18:59:54 +06:00
Sergey Palcheh e6051aa04b refactor(gui): unify winedebug dialogs, separate remembered channels per mode 2026-08-31 17:41:33 +06:00
Sergey Palcheh 552c118595 fix(gui): ignore repeat close clicks during shutdown, add killall safety net 2026-08-31 17:01:34 +06:00
Sergey Palcheh 906ad8cae6 fix: per-second timestamps for prefix backups and wine logs 2026-08-31 16:27:45 +06:00
Sergey Palcheh cb6255454a fix(gui): async wine --version cache and instant command dialog close 2026-08-31 15:17:37 +06:00
Sergey Palcheh daf0758d75 fix(gui): hold waiting stop state until app actually stops 2026-08-31 14:18:05 +06:00
Sergey Palcheh c8deeb1b8a fix(gui): dialog progress, console mirror, command exit codes 2026-08-29 19:34:19 +06:00
Sergey Palcheh 9a8c185846 refactor(gui): decouple InstalledTab info-panel state from main window 2026-08-28 17:33:41 +06:00
Mikhail Tergoev 66f11f384d Merge branch 'minergenon-gui-improvements' 2026-08-25 12:53:16 +03:00
Sergey Palcheh e1701fbac9 refactor(gui): remove dead code across services, utils and dialogs 2026-08-25 10:37:07 +06:00
Sergey Palcheh e6717908ac refactor(gui): extract command dialogs into services/command_runner.py 2026-08-24 19:08:38 +06:00
Sergey Palcheh 68c665fb8e refactor(gui): extract install tabs into widgets/install_tab.py 2026-08-24 18:54:09 +06:00
Sergey Palcheh d83ef56e0e refactor(gui): extract installed apps tab into widgets/installed_tab.py 2026-08-24 16:50:16 +06:00
Sergey Palcheh 261b70fbe0 refactor(gui): extract prefix manager tab into widgets/prefix_tab.py 2026-08-24 16:36:03 +06:00
Sergey Palcheh b76c6f25f5 refactor(gui): extract install process controller into services/install_runner.py 2026-08-24 15:38:00 +06:00
Sergey Palcheh 5e22af5b4b refactor(gui): extract help tab into widgets/help_tab.py 2026-08-24 15:24:24 +06:00
Sergey Palcheh f84e8a9de3 fix(gui): handle missing system wine in version selection dialog 2026-08-24 14:49:57 +06:00
Sergey Palcheh 422a22562f refactor(gui): drop unused desktop file getters 2026-08-24 13:59:26 +06:00
Sergey Palcheh 1caa6e2fc1 refactor(gui): unify theme icon lookup via IconManager.theme_icon() 2026-08-24 13:35:16 +06:00
Mikhail Tergoev 547a705e37 Merge branch 'minergenon-devel' 2026-08-17 12:28:00 +03:00
Sergey Palcheh 8d69318f11 fix(gui): drain process output to prevent pipe buffer deadlock 2026-08-02 13:29:28 +06:00
Sergey Palcheh e68636758c feat(gui): colorize log levels in terminal output 2026-08-02 12:58:49 +06:00
Sergey Palcheh 1f94dfd0e2 feat(gui): add WINEDEBUG logging toggle to prefix manager tab 2026-07-31 18:41:36 +06:00
Sergey Palcheh 50bd638ad7 feat(gui): show settings tab only in development mode 2026-07-31 17:18:35 +06:00
Mikhail Tergoev b07c35e879 scadoffice: update wine for fix hasplmd 2026-07-29 16:04:24 +03:00
Mikhail Tergoev 0baa84dbed drop wait_wineserver from debug_/wine_run 2026-07-29 16:03:40 +03:00
Mikhail Tergoev f1821b40ff Merge branch 'minergenon-indicator' 2026-07-28 16:36:10 +03:00
Mikhail Tergoev 608faea597 emias: update and use wine-11.0-alt-emias-wow64 2026-07-28 16:34:07 +03:00
Sergey Palcheh 827395a0ed feat(gui): support dll/ocx registration in prefix file runner 2026-07-27 13:55:23 +06:00
Sergey Palcheh 3fdfe0fffc feat(gui): add working directory selection to settings tab 2026-07-27 12:55:14 +06:00
Sergey Palcheh f9be296cac feat: add global settings tab for log level 2026-07-24 14:29:02 +06:00
Sergey Palcheh b2e79a3571 refactor: replace print statements and extend logging across modules 2026-07-24 14:28:21 +06:00
Sergey Palcheh 2d808ae555 feat: add unified logger and console output for GUI 2026-07-24 14:26:33 +06:00
Sergey Palcheh 732c688e3a fix(gui): name of the DEBUG channel configuration file has been changed 2026-07-23 18:36:59 +06:00
Sergey Palcheh 7611dc16c6 fix(gui): launch button is raised closer to the app name 2026-07-23 18:29:05 +06:00
Sergey Palcheh 010f757589 fix(gui): prevent duplicate GUI instances by not removing active local socket 2026-07-23 16:41:52 +06:00
Sergey Palcheh 55d591ac4f refactor(gui): Hide duplicate header in the dashboard 2026-07-23 16:12:55 +06:00
Sergey Palcheh 95ad8bdedc added an indication of creating a debug log 2026-07-23 14:44:35 +06:00
Mikhail Tergoev 7fe5208333 Merge branch 'minergenon-wintr' 2026-07-22 14:49:53 +03:00
Sergey Palcheh c2e24ff327 fixup(gui): fixing errors in CommandDialog 2026-07-22 14:57:25 +06:00
Sergey Palcheh 1b63f2dd0c fix(gui): added protection against accidental closing of command execution dialogs 2026-07-22 14:31:03 +06:00
Sergey Palcheh 7cf76aaf97 feat(gui): add WINEDEBUG channel input to log creation dialog 2026-07-22 12:33:31 +06:00
Sergey Palcheh b126910229 fix(gui): winetricks cache naming, text truncation, clear crash 2026-07-22 10:31:00 +06:00
Mikhail Tergoev 96e29eb528 drop --debug, and always use WINEDEBUG for create log files 2026-07-21 14:46:24 +03:00
Mikhail Tergoev 3ec7b756de Merge branch 'minergenon-fix/last-conf-quotes-consistency' 2026-07-21 13:58:39 +03:00
Mikhail Tergoev f640aaf6af use cpcsp-proxy command with args 2026-07-21 13:55:12 +03:00
Mikhail Tergoev 93b9e2f638 Merge branch 'minergenon-cr-pro' 2026-07-21 13:37:38 +03:00
Sergey Palcheh 2030cea983 fix: eliminating the quotation mark difference in the last.conf between Python and bash 2026-07-21 15:28:50 +06:00
Sergey Palcheh 793d960b65 added enable/disable-cryptopro commands to the cli and updated in the gui 2026-07-20 22:38:15 +06:00
Sergey Palcheh ef74003283 fix: resetting WH_USE_CPCSP_PROXY to last.conf in case of a cpcsp_proxy configuration error 2026-07-20 21:17:18 +06:00
Sergey Palcheh 81a43ca9bd chore: suppress shellcheck SC2068, SC2145, SC2199 in pre-commit config 2026-07-20 21:16:01 +06:00
Mikhail Tergoev f10921840e check last.conf in update_last_conf_var 2026-07-20 14:56:36 +03:00
Mikhail Tergoev 425268ade2 Merge branch 'minergenon-devel' 2026-07-20 14:42:45 +03:00
Sergey Palcheh 743ab69455 docs: sync AGENTS.md with the current project structure 2026-07-18 22:58:31 +06:00
Sergey Palcheh 502c5e4b1b refactor(gui): public reset_state/ensure_cache_dir, output_handler for command dialogs 2026-07-18 22:46:24 +06:00
Sergey Palcheh 44881267cf refactor(gui): extract prefix info HTML builder to utils/prefix_info.py 2026-07-18 22:10:02 +06:00
Sergey Palcheh 0433ff6cee refactor(gui): single source for selection frame styles in IconManager 2026-07-18 21:49:32 +06:00
Sergey Palcheh 8c984f1b15 refactor(gui): single exe path parser for desktop Exec lines 2026-07-18 21:39:24 +06:00
Sergey Palcheh 319c4453fd refactor(gui): unify command_dialog, state is set inside the method 2026-07-18 21:10:40 +06:00
Sergey Palcheh 9ec692833e refactor(gui): remove dead code 2026-07-18 20:06:08 +06:00
Sergey Palcheh 1a9297e3e5 fix(cli): killall missed wow64 processes running as bin/wine 2026-07-18 18:42:34 +06:00
Sergey Palcheh 625ee0a223 feat(gui): added External install tab for third-party scripts 2026-07-18 16:44:22 +06:00
Sergey Palcheh 991a03b829 feat(gui): toggle for description search on install tabs 2026-07-18 15:24:08 +06:00
Sergey Palcheh 8eba7d21d0 feat(gui): green border on running app buttons 2026-07-18 14:43:40 +06:00
Sergey Palcheh eed297a226 fix(gui): terminate background processes on winetricks manager close 2026-07-18 13:51:01 +06:00
Sergey Palcheh 9017aaac25 fix(gui): check installer file existence on manual install 2026-07-18 13:37:07 +06:00
Sergey Palcheh 18f35ed08b fix(gui): clear error instead of traceback on direct winehelper_gui.py launch 2026-07-18 13:13:51 +06:00
Sergey Palcheh f5b62e9ec2 perf(gui): non-blocking process termination instead of waitForFinished 2026-07-17 18:50:38 +06:00
Sergey Palcheh ae55e69700 perf(gui): skip icon animations for hidden buttons 2026-07-17 18:34:08 +06:00
Sergey Palcheh 723cfdcc4c perf(gui): cache system Wine version for the session 2026-07-17 18:23:25 +06:00
Sergey Palcheh fb2f2cf020 fix(gui): escape HTML in script descriptions and command output 2026-07-17 17:54:10 +06:00
Sergey Palcheh a7d2ea994e fix(gui): 5s timeout for process start waits 2026-07-17 17:35:41 +06:00
Sergey Palcheh 572ce2a08a fix(gui): forbid prefix/data removal while apps are running 2026-07-17 17:24:39 +06:00
Sergey Palcheh d48e50a0b4 feat(gui): add kill all Wine processes button to the prefix manager 2026-07-17 17:10:55 +06:00
Sergey Palcheh 7321bde951 fix(gui): stable alphabetical sorting of the installed apps list 2026-07-17 16:46:52 +06:00
Sergey Palcheh f5e10a8b0d gui: explicit indication of the installation result 2026-07-17 16:29:27 +06:00
Mikhail Tergoev 61e659ea2e Merge branch 'minergenon-win-pfx' 2026-07-15 17:47:00 +03:00
Sergey Palcheh 728266cdb3 gui: added Windows version selection when creating a prefix 2026-07-15 20:33:00 +06:00
Mikhail Tergoev 055dc04bb1 apply pre-commit fixes 2026-07-15 16:45:43 +03:00
Mikhail Tergoev 3b5432ecdd Merge branch 'minergenon-feature/pre-commit' 2026-07-15 16:24:11 +03:00
Sergey Palcheh 415ed948ad add pre-commit configuration 2026-07-11 14:43:57 +06:00
Mikhail Tergoev b6e99d01af add check version for dxvk and vkd3d 2026-07-10 14:07:51 +03:00
Mikhail Tergoev 6b53afa238 Merge branch 'minergenon-win' 2026-07-10 12:30:27 +03:00
Sergey Palcheh b27d5f31ae added Windows version selection for the prefix 2026-07-10 09:07:06 +06:00
Mikhail Tergoev e62cd1463a Merge branch 'minergenon-dx-vk' 2026-07-09 11:23:08 +03:00
Sergey Palcheh 3bab2d12c7 added a shortened version input for install-dxvk and install-vkd3d 2026-07-09 11:16:08 +06:00
Mikhail Tergoev a14ad4ff70 Revert "t-flex: update wine to wine-11.0-alt-tflex-wow64"
This reverts commit ebabc7ebf7.
2026-07-08 13:28:18 +03:00
Mikhail Tergoev 39fd02d358 Revert "update wine-11.0-alt-tflex-wow64 with fix name"
This reverts commit e76f49a3bd.
2026-07-08 13:27:03 +03:00
Mikhail Tergoev d7d0ef4516 Merge branch 'minergenon-devel' 2026-07-08 13:22:52 +03:00
Sergey Palcheh 4b540302dc style: minor linter comments have been fixed 2026-07-06 13:10:07 +06:00
Sergey Palcheh ff7c36208d fixed duplicate prefix deletion between CLI and GUI 2026-07-06 12:22:25 +06:00
Mikhail Tergoev 4f3355e633 fix for registering .ocx files 2026-07-03 13:49:32 +03:00
Mikhail Tergoev bb637f48f6 add wine-11.12-alt-tkg and use by default 2026-07-01 17:41:29 +03:00
Mikhail Tergoev e76f49a3bd update wine-11.0-alt-tflex-wow64 with fix name 2026-07-01 17:14:26 +03:00
Mikhail Tergoev 0ed10c72f9 Merge branch 'minergenon-fix-gui' 2026-07-01 15:18:12 +03:00
Mikhail Tergoev 34849c9490 Merge branch 'minergenon-auto-completion' 2026-07-01 15:17:09 +03:00
Mikhail Tergoev ebabc7ebf7 t-flex: update wine to wine-11.0-alt-tflex-wow64 2026-07-01 15:03:35 +03:00
Sergey Palcheh 726f2f4b43 refactor(gui): deduplicate PyQt code and extract shared utilities 2026-06-30 10:16:25 +06:00
Sergey Palcheh 8a94f519a3 fix(gui): eliminated clumping and duplication of lines in the command log 2026-06-29 18:16:58 +06:00
Sergey Palcheh 468ea73aa6 fix(gui): fixed multiple duplication of prefix parameters 2026-06-29 14:00:11 +06:00
Sergey Palcheh a860cacf2c refactor(gui): add script and installation management services
- add ScriptManager for flat script structure support

- add ApplicationInstaller and InstallationManager

- refactor ProcessHandler for safer subprocess handling

- improve script, sha256 and winetricks parsers

- update version selection dialog and icon manager
2026-06-26 16:51:00 +06:00
Sergey Palcheh 416c4e5336 refactor(gui): add desktop entry and settings management
- add DesktopEntry reader/writer with section and escape support

- refactor DesktopFileManager to use DesktopEntry

- add SettingsManager for prefix and application settings

- update settings and prefix dialogs to use new managers
2026-06-26 16:50:47 +06:00
Sergey Palcheh 48f694322a fix(gui): security hardening and stability fixes
- pass CLI arguments as list instead of string to prevent injection

- validate .whdb and .whpack paths inside USER_WORK_PATH

- create cache files with 0o600 permissions

- restrict prefix names to alphanumeric, dash and underscore

- remove QApplication.processEvents() calls

- atomic single-instance check via QLocalServer

- guard against deleted QObject/QProcess before deleteLater()

- queue async winetricks installations
2026-06-26 16:50:33 +06:00
Sergey Palcheh b58d768ca6 added auto-completion commands for bash and zsh 2026-06-26 11:32:55 +06:00
Mikhail Tergoev 28062ff5bb use noexec_warning dialog if need for gui 2026-06-23 14:57:34 +03:00
Mikhail Tergoev 4bd3028c4a add clarifications on the solution 2026-06-23 14:33:10 +03:00
Mikhail Tergoev 8edceb667c Merge branch 'minergenon-info' 2026-06-23 14:25:08 +03:00
Mikhail Tergoev 45b495eff0 add new command 2026-06-23 14:23:28 +03:00
Sergey Palcheh 0469197d77 added the noexec_warning dialog 2026-06-16 11:09:12 +06:00
Mikhail Tergoev 4f067d6850 update README.md 2026-06-05 16:45:15 +03:00
Mikhail Tergoev d0c6bdfdb6 update help 2026-06-05 16:44:52 +03:00
Mikhail Tergoev 41bde9d985 updated changelog to 0.13.0 2026-06-02 12:43:24 +03:00
Mikhail Tergoev ee9bb767a7 replace data-path if need 2026-06-01 23:03:55 +03:00
Mikhail Tergoev 1e0f8255ea revert check noexec and add custom data-path 2026-06-01 14:12:20 +03:00
Mikhail Tergoev af8471ea40 drop prepare_wine_noexec 2026-05-27 15:50:41 +03:00
Mikhail Tergoev 4bfeae6e42 Merge branch 'minergenon-devel' 2026-05-26 13:01:45 +03:00
Sergey Palcheh ccd3176d2a gui: prefix manager layout fixes 2026-05-25 11:29:38 +06:00
Sergey Palcheh 0d23121a14 gui: fixed case-insensitivity in the definition .exe files 2026-05-24 00:31:38 +06:00
Sergey Palcheh 4fbb250a34 gui: added an indicator icon to the buttons of the DXVK/VKD3D versions 2026-05-23 23:27:47 +06:00
Sergey Palcheh 6091bf2cf9 gui: added an indicator icon on the buttons of wine versions 2026-05-23 23:07:44 +06:00
Sergey Palcheh dbe5226d29 gui: added sorting of wine versions in descending order 2026-05-23 21:05:19 +06:00
Sergey Palcheh 08cc66c533 gui: added forced reinstallation of cpcsp_proxy when enabled 2026-05-23 20:26:55 +06:00
Mikhail Tergoev c457dc0d31 Merge branch 'minergenon-devel' 2026-05-22 15:45:35 +03:00
Sergey Palcheh b3e4ae719d gui: added the correct activation of CPCSP_PROXY in the additional prefix settings 2026-05-22 18:29:24 +06:00
Sergey Palcheh dc75805a31 fixed the creation of a minimum .whdb file 2026-05-22 17:30:45 +06:00
Mikhail Tergoev 3aebe55162 replace wine-10.18.1-alt1-wow64 to wine-10.0-alt-tkg-wow64 2026-05-21 20:04:38 +03:00
Mikhail Tergoev 9a7a2d5f50 fix winereg for ocx files 2026-05-21 19:51:27 +03:00
Mikhail Tergoev b4ccebc65d fix replace CService32.exe for daobit and ctm 2026-05-21 17:44:31 +03:00
Mikhail Tergoev 36b009ecd4 daobit-cservice: fix setup name 2026-05-21 17:21:17 +03:00
Mikhail Tergoev 372b4c6a6a replace wine-11.6.1-scadoffice to wine-10.0-alt-scadoffice 2026-05-21 16:35:02 +03:00
Mikhail Tergoev 15d5db87ad replace wine-10.12.1-alt1 to wine-10.0-alt-tkg 2026-05-21 16:00:18 +03:00
Mikhail Tergoev a8a35d5bd8 replace wine-10.0-tkg-amd64 to wine-10.0-alt-tkg-amd64 2026-05-21 15:52:17 +03:00
Mikhail Tergoev 87ad9e12b6 add wine-9.0-alt-tkg (i586, amd64 and wow64) 2026-05-21 15:46:42 +03:00
Mikhail Tergoev 45987c9941 emias: WH_MAIN_DECORATED=0 by default 2026-05-20 15:44:09 +03:00
130 changed files with 8967 additions and 6549 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
seh
testser
+16 -5
View File
@@ -40,9 +40,20 @@ env/
.DS_Store
Thumbs.db
# Logs
*.log
.ruff_cache/
.mypy_cache/
.pytest_cache/
.tox/
# Temporary files
*.tmp
*.bak
# Test and coverage
.coverage
htmlcov/
# RPM / package build artifacts
*.rpm
*.src.rpm
RPMS/
SRPMS/
BUILD/
BUILDROOT/
*.spec~
+111
View File
@@ -0,0 +1,111 @@
default_language_version:
python: python3
exclude: |
(?x)(
^\.git/|
^__pycache__/|
.*\.pyc$
)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-added-large-files
args: ["--maxkb=1024"]
- id: check-merge-conflict
- id: check-symlinks
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: mixed-line-ending
args: ["--fix=lf"]
- id: check-case-conflict
- id: check-ast
- id: debug-statements
- id: detect-private-key
- repo: local
hooks:
- id: bash-syntax-check
name: bash syntax check
entry: bash -n
language: system
files: |
(?x)(
^winehelper$|
^dependencies\.sh$|
^autoinstall/|
^manualinstall/|
^testinstall/|
^hiden_scripts/|
^database/.*\.whdb$
)
types: [shell]
- id: py-compile
name: python compile
entry: python3 -m py_compile
language: system
types: [python]
- id: shellcheck
name: shellcheck
entry: shellcheck
args: ["--severity=error", "--exclude=SC2068,SC2145,SC2199"]
language: system
files: |
(?x)(
^winehelper$|
^dependencies\.sh$|
^autoinstall/|
^manualinstall/|
^testinstall/|
^hiden_scripts/|
^database/.*\.whdb$
)
types: [shell]
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.10.0-1
hooks:
- id: shfmt
args: ["--write", "--indent", "4", "--case-indent", "--space-redirects"]
files: |
(?x)(
^winehelper$|
^dependencies\.sh$|
^autoinstall/|
^manualinstall/|
^testinstall/|
^hiden_scripts/|
^database/.*\.whdb$
)
types: [shell]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.4
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args: ["--ignore-words", ".codespell-ignorewords"]
- repo: local
hooks:
- id: sha256sum-list-check
name: sha256sum.list format check
entry: scripts/check_sha256sum_list.sh
language: script
files: ^sha256sum\.list$
pass_filenames: false
+211 -18
View File
@@ -15,8 +15,8 @@ WineHelper — инструмент для упрощения установки
## Технологический стек
### Основные технологии
- **Bash** — основной язык для CLI-интерфейса (~2692 строк в главном скрипте)
- **Python 3** — графический интерфейс с использованием PyQt5
- **Bash** — основной язык для CLI-интерфейса (~3075 строк в главном скрипте)
- **Python 3** — графический интерфейс с использованием PyQt5 (~3413 строк в `winehelper_gui.py` и модули `winehelperqt/`)
- **Wine** — запуск Windows-приложений (поддерживаются различные версии: vanilla, staging, Proton, tkg, crossover)
- **DXVK/VKD3D** — трансляция DirectX → Vulkan
@@ -39,12 +39,16 @@ winehelper/
├── winehelper_gui.py # GUI на PyQt5 (точка входа)
├── winehelperqt/ # Python-модуль GUI
│ ├── __init__.py # Экспорты основных классов
│ ├── logger.py # Единый логгер (get_logger/setup_logger), вывод в консоль
│ ├── dialogs/ # Диалоговые окна
│ │ ├── base_version_dialog.py
│ │ ├── component_version.py
│ │ ├── create_prefix.py # Создание префикса
│ │ ├── file_associations.py
│ │ ├── settings.py # Настройки
│ │ ├── noexec_warning.py # Предупреждение о noexec
│ │ ├── prefix_logging.py # Включение/выключение логирования на вкладке префиксов
│ │ ├── settings.py # Настройки префикса
│ │ ├── windows_version.py # Выбор версии Windows
│ │ ├── wine_version.py # Выбор версии Wine
│ │ └── winetricks_manager.py
│ ├── parsers/ # Парсеры
@@ -52,14 +56,35 @@ winehelper/
│ │ ├── sha256_parser.py # Парсинг sha256sum.list
│ │ └── winetricks_parser.py
│ ├── services/ # Бизнес-логика
│ │ ├── application_installer.py
│ │ ├── app_launcher.py # Запуск приложений
│ │ ├── command_runner.py # Диалоги выполнения команд winehelper (бэкап, удаление и т.п.)
│ │ ├── desktop_file_manager.py
│ │ ├── install_runner.py # Диалог и процесс установки приложений
│ │ ├── prefix_manager.py # Управление префиксами
│ │ ├── process_handler.py
│ │ ├── script_manager.py
│ │ ├── settings_manager.py
│ │ └── winetricks_service.py
│ ├── utils/ # Утилиты
│ │ └── log_handler.py # Обработка логов
│ └── widgets/ # Кастомные виджеты
│ │ ├── cache_service.py
│ │ ├── desktop_entry.py
│ │ ├── dialog_utils.py
│ │ ├── environment.py
│ │ ├── export_utils.py
│ │ ├── font_utils.py # Системные шрифты: app_font()/fixed_font()
│ │ ├── grid_utils.py
│ │ ├── log_handler.py # Обработка логов
│ │ ├── prefix_info.py # Построение HTML с информацией о префиксе
│ │ ├── process_utils.py
│ │ └── version_utils.py
│ └── widgets/ # Вкладки главного окна и кастомные виджеты
│ ├── help_tab.py # Вкладка «Справка»
│ ├── icon_manager.py
│ ├── info_panel.py # Контроллер правой информационной панели
│ ├── install_tab.py # Вкладки установки (авто/ручная/сторонняя)
│ ├── installed_tab.py # Вкладка «Установленные»
│ ├── prefix_tab.py # Вкладка «Менеджер префиксов»
│ ├── tab_names.py # Константы имён вкладок (TabNames)
│ └── __init__.py
├── autoinstall/ # Скрипты автоматической установки (43 приложения)
├── manualinstall/ # Скрипты ручной установки (9 приложений)
├── testinstall/ # Тестовые скрипты установки
@@ -68,9 +93,14 @@ winehelper/
│ ├── gui/ # Иконки приложения (SVG)
│ └── handbook/ # Скриншоты для документации
├── hiden_scripts/ # Скрытые/вспомогательные скрипты
├── scripts/ # Вспомогательные скрипты
│ └── check_sha256sum_list.sh # Проверка формата sha256sum.list
├── auto_completion/ # Автодополнение для bash
├── dependencies.sh # Установка системных зависимостей
├── sha256sum.list # SHA256 хеши загружаемых файлов
├── .pre-commit-config.yaml # Настройки pre-commit
├── pyproject.toml # Настройки ruff
├── .codespell-ignorewords # Исключения для codespell
└── winehelper.desktop # Desktop-файл для запуска GUI
```
@@ -92,11 +122,23 @@ winehelper/
Модульная архитектура PyQt5:
- **WineHelperGUI** — главное окно, управление вкладками
- **PrefixManager** — CRUD операции с префиксами
- **ApplicationInstaller** — установка приложений
- **WineHelperGUI** — главное окно приложения (`winehelper_gui.py`)
- **ScriptManager** — загрузка и обработка скриптов установки
- **ScriptParser** — парсинг информации из скриптов установки
- **Диалоги** — модальные окна для настроек и выбора
- **PrefixManager** — CRUD операции с префиксами
- **DesktopFileManager** — работа с .desktop файлами
- **AppLauncher** — запуск и мониторинг установленных приложений
- **SettingsManager** — чтение/запись настроек префиксов
- **WinetricksService** — работа с компонентами Winetricks
- **Диалоги** — модальные окна для настроек и выбора (Wine, Windows, компоненты, ассоциации файлов, предупреждение noexec)
- **Утилиты** — логирование, работа с окружением, экспорт, сеточные макеты, управление иконками
#### Как CLI и GUI общаются
- **CLI → GUI.** `./winehelper gui` экспортирует окружение (`DATA_PATH`, `RUN_SCRIPT`, `USER_WORK_PATH`, `WH_DIST_DIR`, `WH_TMP_DIR`, `WH_DEVEL`, `WH_ICON_*`, `LICENSE_FILE`, `AGREEMENT`, `CHANGELOG_FILE`, …) и затем `exec`-ает `python3 winehelper_gui.py`. GUI читает его через `winehelperqt/utils/environment.py` (`EnvironmentConfig`). **Запустить GUI напрямую через `python3 winehelper_gui.py` нельзя** — нужно это окружение.
- **GUI → CLI.** Каждое действие GUI (установка, удаление/бэкап/восстановление префикса, winetricks, запуск exe, killall…) собирает argv `winehelper <команда>` из `EnvironmentConfig.RUN_SCRIPT` и выполняет в `QProcess`, стримя stdout в диалог лога. Общая настройка `QProcess` — `winehelperqt/utils/process_utils.py`; диалоги долгих команд — `services/command_runner.py`, `services/install_runner.py`.
- CLI определяет запуск из GUI (`WH_USE_GUI=1` + проверка родительского процесса) и пропускает интерактивный лицензионный запрос.
- Завершённые объекты `QProcess` нужно удалять (см. `utils/process_utils.py`) — утечки были реальным багом.
### Скрипты установки
@@ -121,6 +163,27 @@ wine_run_install "${SETUP_EXE}" /S
create_desktop "Имя ярлыка" "${EXE_PATH}" "icon_name"
```
**Важно: скрипты не исполняются, а подключаются (`source`)** внутри функции `run_autoinstall`. Поэтому они вызывают функции CLI напрямую и видят его переменные. Любая синтаксическая ошибка в скрипте ломает сам CLI — перед коммитом проверяйте `bash -n` / shellcheck.
- `$1` — имя скрипта; `$2` — путь к дистрибутиву от пользователя для скриптов `manualinstall/` (при отсутствии `$2` скрипт обязан вызвать `fatal`).
- `PROG_ICON="icon_name"` указывает на `image/<icon_name>.png`.
- Режим определяется каталогом:
- `autoinstall/` — полностью автоматическая установка, показывается в GUI;
- `manualinstall/` — требуется дистрибутив пользователя;
- `testinstall/` — экспериментальные, в GUI скрыты за галочкой;
- `hiden_scripts/` — не перечисляются нигде, запускаются только по явному пути.
- Каждый скачиваемый архив (сборки Wine, базовые префиксы, DXVK/VKD3D) должен иметь строку в `sha256sum.list` — `check_file_sum` отвергает незарегистрированные.
### Файлы `.whdb` и `last.conf`
Пофайловые настройки запуска — это тоже bash-сниппеты, которые подключаются (`source`):
- `create_desktop` создаёт `<exe>.whdb` рядом с установленным exe, копируя туда строки `export` из скрипта. Скрипты могут дописывать в этот файл дополнительный bash (хуки, фоновые задачи) — пример: `autoinstall/ksamu`.
- `database/*.whdb` — общие профили, сопоставляемые по строке-комментарию `#name.exe`, когда своего `<exe>.whdb` нет (например, T-FLEX).
- Перед каждым `wine_run` функция `init_database` подключает подходящий `.whdb`, затем `$WINEPREFIX/last.conf` (последние использованные настройки этого префикса). `last.conf` — это то, что правит диалог «настройки префикса» в GUI (`WH_WINDOWS_VER`, `WH_MAIN_DECORATED`, `WH_VIRTUAL_DESKTOP`, `WH_XDG_OPEN`, `WH_USE_CPCSP_PROXY` и т. д.).
Всё это подключается через `source`, поэтому синтаксическая ошибка в скрипте или `.whdb` ломает CLI — проверяйте `bash -n` / shellcheck перед коммитом.
## Режимы работы
### Системный режим
@@ -133,15 +196,25 @@ create_desktop "Имя ярлыка" "${EXE_PATH}" "icon_name"
При запуске из репозитория:
- Рабочая директория: `~/test-winehelper/`
- Данные берутся из текущей директории
- Включается проверка синтаксиса скриптов
- Устанавливается `WH_DEVEL=1` — в GUI появляется вкладка «Настройки» и dev-иконка
- Префиксы, скачанные сборки Wine (`dist/`) и временные файлы лежат под `~/test-winehelper/`, поэтому запуски в режиме разработки не трогают реальную установку пользователя
- Включается проверка синтаксиса: CLI прогоняет `bash -n` по самому себе, `dependencies.sh` и каждому скрипту в `autoinstall/`, `manualinstall/`, `testinstall/`, `database/`; при первой же синтаксической ошибке работа прерывается
| | системный режим | режим разработки |
|---|---|---|
| данные (`DATA_PATH`) | `/usr/share/winehelper/` | корень репозитория |
| рабочая директория (`USER_WORK_PATH`) | `~/.local/share/winehelper/` | `~/test-winehelper/` |
| `WH_DEVEL` | не задана | `1` |
## Команды CLI
```bash
# Установка
winehelper install list # Список доступных программ
winehelper -i <script_name> # Установить программу
winehelper -i <script_name> /path # Установить из дистрибутива
winehelper -i <script_name> # Установить программу (autoinstall/)
winehelper -i <script_name> /path # Установить из дистрибутива (manualinstall/, $2 = путь к дистрибутиву)
winehelper -i "/path/to/script" # Установить из стороннего скрипта (режим "another")
winehelper -i <script_name> --clear-pfx # Игнорировать BASE_PFX, строить префикс с нуля
# Управление префиксами
winehelper create-prefix # Создать префикс
@@ -150,23 +223,45 @@ winehelper backup-prefix [name] # Создать бэкап (.whpack)
winehelper restore-prefix <path> # Восстановить из бэкапа
# Wine tools
winehelper winefile # Файловый менеджер
winehelper winecfg # Настройки Wine
winehelper winereg # Редактор реестра
winehelper winefile # Файловый менеджер
winehelper wineconsole # Терминал Wine (cmd.exe)
winehelper winetricks [component] # Winetricks
winehelper <file.exe> # Запустить exe в префиксе
winehelper run <exe_name> # Запустить установленную программу (префикс берётся из .whdb)
winehelper debug [флаги] [программа] # Запуск программы в отладчике winedbg
winehelper killall # Остановить все процессы Wine, запущенные из USER_WORK_PATH
winehelper run-in-prefix <префикс> <файл> # Запустить произвольный exe/msi в указанном префиксе
winehelper --help # Полный список подкоманд (функция wh_info)
# Управление компонентами
winehelper install-dxvk [version] # Установить DXVK
winehelper install-vkd3d [version] # Установить VKD3D
winehelper change-wine [version] # Сменить версию Wine
winehelper clear-winetricks-cache # Очистить кэш Winetricks
# Другое
winehelper --debug <command> # Режим отладки
winehelper --changelog # История изменений
winehelper gui # Запустить GUI
```
### Переменные окружения и неинтерактивный режим
```bash
WH_LOG_LEVEL=debug ./winehelper gui # Уровень логов GUI: debug/info/warning/error
WH_SKIP_DEPS=1 ./winehelper ... # Пропустить проверку i586-зависимостей (rpm -q 32-битных библиотек)
yes | ./winehelper -i <script> # Неинтерактивно: лицензионный запрос — это `read -p`;
# когда stdin не tty, проверка зависимостей тоже пропускается
```
## Как работает выполнение (runtime)
- `wine_run` сопоставляет тип файла способу запуска: `.msi` → `msiexec /i` (для тихой установки передавайте `/qn`), `.exe` → `start /wait /high /unix` (переменная `WINE_WIN_START`).
- Вывод Wine каждого запуска дублируется (`tee`) в `~/winehelper_backup_log/<префикс>_<exe>_<дата>.log`.
- По `sha256sum.list` проверяются **только** сборки Wine, базовые префиксы и DXVK/VKD3D. Скачанные дистрибутивы приложений **не** проверяются (`check_file_sum` отвергает лишь незарегистрированные архивы из первого списка).
- Внутренний конвейер CLI: инициализация окружения/путей → помощники (`fatal`, `print_*`, `try_download`, `unpack`, `check_file_sum`) → управление Wine/DXVK/префиксами (`init_wine_ver`, `init_wineprefix`, `prepair_wine`) → `wine_run` / `wine_run_install` → интеграция с рабочим столом (`create_desktop`) → большой диспетчер `case "$arg1"` внизу файла. Новые подкоманды добавляются в этот `case` и в текст `--help` (функция `wh_info`).
## Разработка
### Запуск в режиме разработки
@@ -180,6 +275,78 @@ winehelper gui # Запустить GUI
./winehelper -i alta-expert
```
### Стиль коммитов
Коммиты оформляются по Conventional Commits с указанием области: `feat(gui): ...`, `refactor(gui): ...`, `fix(gui): ...` и т. п. Документация, комментарии, сообщения лога и строки интерфейса — на русском; так же оформляется и новый код.
### Pre-commit
Проект использует [pre-commit](https://pre-commit.com/) для автоматической проверки кода перед коммитом.
#### Установка
```bash
# Установить pre-commit
pip install --user pre-commit
# Установить git-хуки
pre-commit install
```
#### Использование
Проверка запускается автоматически при обычном `git commit`:
```bash
git add some-file.py
git commit -m "feat: описание изменения"
```
Если хуки проходят — коммит создаётся. Если падают — коммит отменяется, а в терминале выводится список ошибок.
Проверить все файлы вручную:
```bash
pre-commit run --all-files
```
Проверить только staged-файлы:
```bash
pre-commit run
```
Обновить версии хуков:
```bash
pre-commit autoupdate
```
#### Включённые проверки
- `trailing-whitespace`, `end-of-file-fixer`, `mixed-line-ending` — форматирование файлов
- `check-ast` — синтаксис Python
- `debug-statements` — отладочные вызовы (`breakpoint()`, `pdb`)
- `detect-private-key` — случайно закоммиченные ключи
- `bash-syntax-check` — синтаксис bash-скриптов
- `py-compile` — компиляция Python-файлов (`python3 -m py_compile`)
- `shellcheck` — линтинг bash-скриптов
- `shfmt` — форматирование bash-скриптов
- `ruff` — линтинг Python (импорты, отступы, line-length=120)
- `ruff-format` — автоформатирование Python
- `codespell` — проверка орфографии
- `sha256sum.list format check` — валидация формата `sha256sum.list`
#### Важные нюансы
- Хук проверяет только файлы из индекса (`git add`).
- Чтобы пропустить проверку (не рекомендуется):
```bash
git commit --no-verify
```
- `pyproject.toml` содержит настройки `ruff` (line-length=120, select правил).
- Слова, которые `codespell` считает опечатками, но которые являются корректными для проекта, перечислены в `.codespell-ignorewords`.
### Добавление нового скрипта установки
1. Создать файл в `autoinstall/` (для автоматической установки) или `manualinstall/` (требуется дистрибутив)
@@ -195,12 +362,38 @@ winehelper gui # Запустить GUI
- Проверять наличие переменных перед использованием
- Использовать функции обработки ошибок (`fatal`, `print_error`)
- Экранировать все пути с помощью кавычек
- Предпочитать готовые помощники (`try_remove_file`, `try_copy_dir`, `create_new_dir`, `try_download`) сырым `rm`/`cp`/`wget`
**Python:**
- Импортировать из `winehelperqt` относительно
- Использовать type hints где возможно
- Импорты — только абсолютные (`from winehelperqt.x import y`). Относительные (`from .`, `from ..`) запрещены
- Qt импортируется ТОЛЬКО через слой совместимости: `from winehelperqt.qt_compat import Qt, QWidget, ...`. Прямые импорты `PyQt5`/`PyQt6` в коде запрещены (единственное исключение — сам `winehelperqt/qt_compat.py`). Это нужно для работы на PyQt6 (p11+) и PyQt5 (p10) из единой кодовой базы:
- enum'ы только в scoped-форме: `Qt.AlignmentFlag.AlignCenter`, `QFont.Weight.Bold`, `QMessageBox.StandardButton.Yes` (работает в PyQt5 ≥5.11 и PyQt6);
- `exec()` вместо `exec_()`;
- сравнение int из сигналов (например `QCheckBox.stateChanged`) с enum — через `Qt.CheckState(state) == Qt.CheckState.Checked`;
- для теста конкретного binding'а: `WH_QT_BINDING=pyqt5 ./winehelper gui` (или `pyqt6`);
- выбор binding'а также сохраняется во вкладке «Настройки» (ключ `qt_binding` в `~/.config/winehelper/logging.conf`: auto/pyqt6/pyqt5; приоритет: `WH_QT_BINDING` > конфиг > auto); применяется при перезапуске GUI — переключение «на лету» в одном процессе невозможно (PyQt5 и PyQt6 нельзя загружать вместе)
- Документация на русском языке
- Классы должны наследоваться от соответствующих QWidget
- Код проходит проверки `ruff` и `ruff-format` (см. раздел Pre-commit)
- Желательно добавлять type hints для новых функций и методов
- Для вывода сообщений использовать логгер, а не `print()`:
```python
from winehelperqt.logger import get_logger
logger = get_logger(__name__)
```
Логгер настраивается один раз в `main()` (`winehelper_gui.py`) через `setup_logger()`.
Уровень задаётся переменной окружения `WH_LOG_LEVEL` (debug/info/warning/error, по умолчанию info)
либо во вкладке «Настройки» GUI (вкладка отображается только в режиме разработки — при тестовом запуске из репозитория, когда `WH_DEVEL=1`).
Вывод идёт в stdout — виден при запуске GUI из терминала (`./winehelper gui`).
При выводе в терминал (tty) уровни подсвечиваются цветом (`ColoredFormatter`): DEBUG — голубой, INFO — зелёный, WARNING — жёлтый, ERROR — красный, CRITICAL — жирный красный.
## Воспроизведение в headless-окружении (podman)
Использовалось для бага #59578. Образ `registry.altlinux.org/sisyphus/base` уже содержит репозиторий `x86_64-i586`. От root:
`apt-get install apt-repo xorg-xvfb xdotool ImageMagick winetricks 7-zip icoutils cabextract procps psmisc which`,
затем `bash dependencies.sh` (нужен `apt-repo`), `useradd -m test`, `mkdir -m 1777 /tmp/.X11-unix`.
CLI запускать от непривилегированного пользователя (от root он отказывается), репозиторий монтировать read-only (режим разработки → `~/test-winehelper/`), поднять `Xvfb :99` + `DISPLAY=:99` и обязательно задать **`LANG=ru_RU.UTF-8`** — без UTF-8 локали установка MSI с кириллическими путями откатывается (`cabinet_copy_file ... error 3`), а интерфейсы приложений рисуют `?`. Скриншоты: `import -window root out.png`; список окон: `xdotool search --name ""`.
## Безопасность
+31
View File
@@ -1,5 +1,36 @@
История изменений:
0.13.0
* обновлен графический режим Qt5:
- исправлены/улучшены основные функции
- запуск утилит Wine выполняется через winehelper (CLI) вместо прямого вызова wine
- добавлены индикаторы-иконки на кнопках версий Wine и DXVK/VKD3D
- добавлена сортировка версий Wine по убыванию
- удалена вкладка "Загрузка" в окне выбора версии Wine
- исправлена автопрокрутка списка в менеджере компонентов при поиске
- исправлено определение .exe файлов без учета регистра
- скрытие ярлыка установленного приложения, если .exe файл отсутствует
- добавлена корректная активация CPCSP_PROXY в дополнительных настройках префикса
- добавлена принудительная переустановка cpcsp_proxy при включении
* добавлены версии wine:
- wine-11.0-alt-tkg
- wine-9.0-alt-tkg (i586, amd64, wow64)
- wine-10.0-alt-tkg (i586, amd64, wow64)
- wine-10.0-alt-scadoffice-wow64
* удалены неиспользуемые версии wine
* обновлены скрипты установок:
- t-flex: единый префикс для всех версий, используется wine с исправлением всплывающих окон и 3D на Wayland
- emias: используется wine с исправлением сворачивания окон
- scadoffice: используется специальная сборка wine (демо версия программы)
- olimp: добавлен itss в префикс, исправлен фокус окна и окна справки
- ctm-service: исправлено имя exe для desktop файла
- daobit-cservice: исправлено имя установщика и замена CService32.exe
* добавлена поддержка запуска сторонних скриптов установки
* добавлена минимальная генерация .whdb файла при ручном создании ярлыка
* возвращено уведомление о монтировании home с флагом noexec
* добавлена возможность выбрать рабочий каталог для WineHelper:
winehelper data-path "/путь/к/каталогу"
0.12.0
* исправлена работа cpcsp_proxy при монтировании home с флагом noexec
* обновлены все скрипты установки ВЭД и СТМ
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+9 -9
View File
@@ -55,7 +55,7 @@ modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
@@ -111,7 +111,7 @@ modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
@@ -158,7 +158,7 @@ Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
@@ -216,7 +216,7 @@ instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
@@ -267,7 +267,7 @@ Library will still fall under Section 6.)
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
@@ -329,7 +329,7 @@ restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
@@ -370,7 +370,7 @@ subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
@@ -422,7 +422,7 @@ conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
@@ -456,7 +456,7 @@ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
+25 -4
View File
@@ -72,6 +72,12 @@
$ winehelper -i is-record-station "/путь/до/установочного_файла"
```
3. **Установка из стороннего скрипта** — можно передать путь к собственному скрипту установки, подготовленному по шаблону WineHelper.
Пример:
```
$ winehelper -i "/путь/до/скрипта_установки"
```
### Процесс установки приложений из списка скриптов
При первом запуске **WineHelper** проверяются и при необходимости устанавливаются дополнительные зависимости (запрашиваются права root).
@@ -142,7 +148,7 @@ $ winehelper "/путь/до/установочного_файла.exe"
2 - Префикс2
Выберите префикс (0-2):
```
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**.
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**.
После выбора префикса автоматически:
- Создаётся копия префикса.
- Внутри префикса сохраняется используемая версия WINE.
@@ -171,7 +177,6 @@ WineHelper предоставляет доступ к основным инст
winehelper winecfg # запуск wine конфигуратора для префикса
winehelper winereg # запуск редактора реестра для префикса
winehelper wineconsole # запуск терминала wine (cmd.exe)
winehelper winetricks # запуск графического интерфейса winetricks
winehelper winetricks [компонент] # автоматическая установка дополнительного компонента в префикс
```
После выполнения любой из вышеперечисленных команд отображается список существующих префиксов:
@@ -219,24 +224,40 @@ WineHelper предоставляет доступ к основным инст
install list список возможных установочных скриптов
install [скрипт] запустить скрипт установки программы
install [скрипт] --clear-pfx не использовать готовый префикс для установки ПО
install "путь/до/скрипта" запустить сторонний скрипт установки
install-dxvk [версия|none|list] установить, удалить или показать версии DXVK
install-vkd3d [версия|none|list] установить, удалить или показать версии VKD3D
change-wine [версия] изменить версию Wine для текущего префикса
installed список установленных программ
run [программа] запуск программы (отладка)
run [программа] запуск программы
debug [флаги] [программа] запуск программы в отладчике winedbg
Флаги:
--auto, -a автоматический backtrace
--gdb, -g режим gdb
--interactive, -i интерактивный режим
remove-all удалить WineHelper и все связанные данные
create-prefix создать префикс
remove-prefix [имя_префикса] удалить префикс и все связанные данные
backup-prefix [имя_префикса] создать резервную копию префикса
restore-prefix \"путь/до/whpack\" восстановить префикс из резервной копии
clear-winetricks-cache очистить кэш Winetricks
winecfg запуск wine конфигуратора для префикса
winereg запуск редактора реестра для префикса
winefile запуск файлового менеджера wine
wineconsole запуск терминала wine (cmd.exe)
control запуск панели управления wine
uninstaller запуск удаления программ wine
winetricks [компонент] автоматическая установка дополнительного компонента в префикс
killall остановить все процессы Wine
run-in-prefix <префикс> <файл> запустить файл в указанном префиксе
Параметры:
--help показать эту справку и выйти
--version показать информацию о пакете и его версии
--changelog показать историю изменений
--debug [команда] включить режим логирования работы WINE
</pre>
</details>
+23 -9
View File
@@ -4,25 +4,21 @@ _winehelper_completions() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="--help --version --debug install installed install-dxvk install-vkd3d -r -i remove-all --clear-pfx killall remove-prefix backup-prefix restore-prefix create-prefix --changelog changelog change-wine clear-winetricks-cache"
wine_cmd="winecfg winereg winefile wineconsole winetricks desktop regedit explorer cmd run"
opts="--help --version install installed install-dxvk install-vkd3d -r -i remove-all killall remove-prefix backup-prefix restore-prefix create-prefix change-wine change-windows-ver clear-winetricks-cache gui data-path version help --changelog changelog create-base-pfx init-prefix run-in-prefix debug"
wine_cmd="winecfg winereg winefile wineconsole winetricks desktop regedit explorer cmd run uninstaller control"
case "${prev}" in
winehelper)
COMPREPLY=( $(compgen -W "${opts} ${wine_cmd}" -- "${cur}") )
return 0
;;
--debug)
COMPREPLY=( $(compgen -W "${wine_cmd}" -- "${cur}") )
return 0
;;
install|-i)
local scripts="$(ls -1 /usr/share/winehelper/autoinstall) $(ls -1 /usr/share/winehelper/manualinstall)"
COMPREPLY=( $(compgen -W "list ${scripts}" -- "${cur}") )
local scripts="$(ls -1 /usr/share/winehelper/autoinstall 2>/dev/null) $(ls -1 /usr/share/winehelper/manualinstall 2>/dev/null) $(ls -1 /usr/share/winehelper/testinstall 2>/dev/null)"
COMPREPLY=( $(compgen -W "list --clear-pfx ${scripts}" -- "${cur}") )
return 0
;;
run|installed)
local installed=$(ls -1 ~/.local/share/winehelper/ | grep ".desktop" | sed 's/.desktop//')
local installed=$(ls -1 ~/.local/share/winehelper/ 2>/dev/null | grep ".desktop" | sed 's/.desktop//')
COMPREPLY=( $(compgen -W "${installed}" -- "${cur}") )
return 0
;;
@@ -31,7 +27,17 @@ _winehelper_completions() {
COMPREPLY=( $(compgen -W "${prefixes}" -- "${cur}") )
return 0
;;
run-in-prefix)
local prefixes=$(ls -1 ~/.local/share/winehelper/prefixes 2>/dev/null)
COMPREPLY=( $(compgen -W "${prefixes}" -- "${cur}") )
return 0
;;
restore-prefix)
COMPREPLY=( $(compgen -f -X '!*.whpack' -- "${cur}") )
return 0
;;
data-path)
COMPREPLY=( $(compgen -d -- "${cur}") )
return 0
;;
install-dxvk|install-vkd3d)
@@ -48,7 +54,15 @@ _winehelper_completions() {
COMPREPLY=( $(compgen -W "system ${wine_versions}" -- "${cur}") )
return 0
;;
change-windows-ver)
COMPREPLY=( $(compgen -W "2k xp xp64 2k3 vista 7 8 81 10 11" -- "${cur}") )
return 0
;;
*)
if [[ "${COMP_WORDS[COMP_CWORD-2]}" == "run-in-prefix" ]] ; then
COMPREPLY=( $(compgen -f -- "${cur}") )
return 0
fi
;;
esac
}
+38 -20
View File
@@ -5,8 +5,9 @@ _winehelper() {
opts=(
'--help[Вывести справку]'
'help[Вывести справку]'
'--version[Показать информацию о пакете и его версии]'
'--debug[Режим отладки]'
'version[Показать информацию о пакете и его версии]'
'install[Запустить скрипт установки программы]'
'install-dxvk[Установить/удалить DXVK]'
'install-vkd3d[Установить/удалить VKD3D]'
@@ -14,14 +15,22 @@ _winehelper() {
'-r[Запуск программы (отладка)]'
'-i[Запустить скрипт установки программы]'
'remove-all[Удалить WineHelper и все связанные данные]'
'--clear-pfx[Очистить префикс \[имя_префикса\]]'
'killall[Убить все процессы]'
'create-prefix[Создать новый префикс]'
'remove-prefix[Удалить префикс и все связанные данные]'
'backup-prefix[Создать резерную копию префикса]'
'restore-prefix[восстановить префикс из резервной копии "путь/до/whpack"]'
'backup-prefix[Создать резервную копию префикса]'
'restore-prefix[Восстановить префикс из резервной копии "путь/до/whpack"]'
'change-wine[Изменить версию Wine для префикса]'
'change-windows-ver[Изменить версию Windows для префикса]'
'clear-winetricks-cache[Очистить кэш Winetricks]'
'gui[Запустить графический интерфейс]'
'data-path[Изменить рабочий каталог WineHelper]'
'create-base-pfx[Создать базовый шаблонный префикс]'
'init-prefix[Инициализировать префикс]'
'run-in-prefix[Запустить файл в указанном префиксе]'
'debug[Запуск программы в отладчике]'
'--changelog[Показать историю изменений]'
'changelog[Показать историю изменений]'
)
wine_cmd=(
@@ -34,6 +43,8 @@ _winehelper() {
regedit
explorer
cmd
uninstaller
control
'run[Запуск программы (отладка)]'
)
@@ -47,23 +58,28 @@ _winehelper() {
_values 'winehelper options' "${opts[@]}" "${wine_cmd[@]}"
;;
args)
if [[ "$words[2]" == "run-in-prefix" ]] && [[ ${#words[@]} -gt 3 ]] ; then
_files
return
fi
case $words[2] in
--debug)
_values 'wine commands' "${wine_cmd[@]}"
;;
install|-i)
_get_list_for_install
;;
run|installed)
run|installed|debug)
_get_installed_list
;;
remove-prefix|backup-prefix)
remove-prefix|backup-prefix|run-in-prefix)
_get_prefixes
;;
create-prefix)
create-prefix|init-prefix|create-base-pfx|gui|killall|clear-winetricks-cache|remove-all|version|--version|help|--help|changelog|--changelog)
;;
restore-prefix)
_files
_files -g '*.whpack'
;;
data-path)
_path_files -/
;;
install-dxvk)
_get_component_versions 'install-dxvk'
@@ -74,6 +90,11 @@ _winehelper() {
change-wine)
_get_wine_versions
;;
change-windows-ver)
_values 'windows versions' 2k xp xp64 2k3 vista 7 8 81 10 11
;;
winecfg|winereg|winefile|wineconsole|winetricks|desktop|regedit|explorer|cmd|uninstaller|control)
;;
*)
_values 'winehelper options' "${opts[@]}" "${wine_cmd[@]}"
;;
@@ -117,8 +138,11 @@ _get_prefixes () {
}
_get_list_for_install () {
packages=(${(f)"$(winehelper install list 2>/dev/null |
local -a packages
packages=(--clear-pfx)
packages+=(${(f)"$(winehelper install list 2>/dev/null |
awk -F ' - ' '
/^[^ ]+ - \("/ {next}
/^[[:space:]]*$/ {next}
@@ -131,14 +155,8 @@ _get_list_for_install () {
if (name && desc) print name ":" desc
}
')"})
packages+=(
scadoffice:"SCAD Office"
t-flex-cad17-applications:"Приложения для T-FLEX CAD 17"
t-flex-cad17-resources:"Обучающие материалы T-FLEX CAD 17"
)
_describe 'packages' packages
_describe 'packages' packages
}
_get_installed_list () {
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Пакет программ для таможенного оформления. Включает программы Такса и ТамДок в комплектациях Эксперт, а также программу Заполнитель.
########################################################################
export PROG_URL="https://www.alta.ru"
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="alta"
export PROG_NAME="Заполнитель"
export PROG_ICON="altazap"
+1 -3
View File
@@ -2,7 +2,7 @@
# info_ru: Система “A'PROPOS-Z - Графит ТМ” для проектирования этапов механообработки, синтеза, расчета и верефикация операционных размерных текстур. Графическое моделирование технологии иформирование карт эскизов.
########################################################################
export PROG_URL="https://www.tezis.pro"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="apropos-z"
export PROG_NAME="A'PROPOS-Z - Графит ТМ"
export PROG_ICON="apropos-z"
@@ -24,5 +24,3 @@ if try_download "$AUTOINSTALL_URL" "$AUTOINSTALL_EXE" ; then
create_desktop "$PROG_NAME" "$DRIVE_C/Program Files/A'PROPOS-Z/Apropos-Z.exe" "$PROG_ICON"
fi
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ АТТЕСТАТ” для документов об основном общем, среднем общем образовании.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-4"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ АТТЕСТАТ"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ ДПП” для заполнения бланков документов о повышении квалификации и профессиональной переподготовке.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ ДПП"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ НШ” для заполнения бланков документов об окончании начальной школы.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-1"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ НШ"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ МЕД” для заполнения бланков документов о послевузовском профессиональном образовании и сертификатов специалиста.
######################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-3"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ МЕД"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ ПРОФЕССИЯ” для заполнения бланков документов профессионального обучения.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-5"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ ПРОФЕССИЯ"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ СПО” для заполнения бланков документов о среднем профессиональном образовании.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-2"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ СПО"
export PROG_ICON="armkt"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа “АРМ КТ ПРОФ ВУЗ” для заполнения бланков документов о высшем образовании.
########################################################################
export PROG_URL="https://armregistr.ru/programmnoe-obespechenie/licenziya-na-programmnoe-obespechenie-dlya-zapolneniya-blank-6"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="arm-kt"
export PROG_NAME="АРМ КТ ПРОФ ВУЗ"
export PROG_ICON="armkt"
+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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+4 -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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
@@ -28,7 +28,9 @@ if try_download "https://files.ctm.ru/CSERVICE/SFX/setup_cs.exe" "${AUTOINSTALL_
try_remove_dir "$AUTOINSTALL_UNPACK"
WIN_FILE_EXEC="$DRIVE_C/Program Files/CTM/CSERVICE/CService32.exe"
create_desktop "$PROG_NAME" "$WIN_FILE_EXEC" "$PROG_ICON"
WIN_FAKE_EXEC="$DRIVE_C/Program Files/CTM/CSERVICE/ctm-service.exe"
try_force_link_file "$WIN_FILE_EXEC" "$WIN_FAKE_EXEC"
create_desktop "$PROG_NAME" "$WIN_FAKE_EXEC" "$PROG_ICON"
if [[ "$USE_CTMBAR" == "1" ]] ; then
if [[ -f "$DRIVE_C/Program Files/CTM/COMMON/CTMBAR.EXE" ]]
+5 -3
View File
@@ -2,7 +2,7 @@
# info_ru: Сервис обновления ПО: "R-Атлас", "R-Инфо" и "R-Тариф".
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="daobit"
export PROG_NAME="Сервис обновления ПО"
export PROG_ICON="daobitcservice"
@@ -13,7 +13,7 @@ export INSTALL_DLL="dotnet7 dotnetdesktop7 arial renderer=gdi"
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"
prepair_wine
if try_download "https://files.daobit.ru/CSERVICE/SFX/setup_cs.exe" "${AUTOINSTALL_EXE}" ; then
@@ -25,7 +25,9 @@ if try_download "https://files.daobit.ru/CSERVICE/SFX/setup_cs.exe" "${AUTOINSTA
wine_run_install "${AUTOINSTALL_SETUP}" /cd /targetdir="C:\\\\Program Files\\\\DAO\\\\CSERVICE" /auto /hide
WIN_FILE_EXEC="$DRIVE_C/Program Files/DAO/CSERVICE/CService32.exe"
create_desktop "$PROG_NAME" "$WIN_FILE_EXEC" "$PROG_ICON"
WIN_FAKE_EXEC="$DRIVE_C/Program Files/DAO/CSERVICE/daobit-service.exe"
try_force_link_file "$WIN_FILE_EXEC" "$WIN_FAKE_EXEC"
create_desktop "$PROG_NAME" "$WIN_FAKE_EXEC" "$PROG_ICON"
try_remove_dir "$AUTOINSTALL_UNPACK"
try_remove_file "$AUTOINSTALL_EXE"
fi
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Построение маршрутов железнодорожных грузоперевозок
########################################################################
export PROG_URL="https://daobit.ru"
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="keepsoft_similator4"
export BASE_PFX="defpfx_x86_v01"
export WINEARCH="win32"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: “КСАМУ” - Комплексная система автоматизации медицинского учреждения.
########################################################################
export PROG_URL="https://docs.medicine-it.ru/"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="ksamu"
export PROG_NAME="КСАМУ"
export PROG_ICON="ksamu"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# info_ru: Это мощный, современный инструмент быстрой и качественной разработки АСУТП
########################################################################
export PROG_URL="https://www.krug2000.ru"
export WH_WINE_USE="wine-11.12-alt-sk2000-i586"
export WINEPREFIX="scada-krug-2000"
export PROG_NAME="SCADA КРУГ 2000"
export PROG_ICON="krugtaskmanager"
export BASE_PFX="scadakrug2k_pfx_v01"
export WH_WINDOWS_VER="10"
export WINEARCH="win32"
export INSTALL_DLL="dotnet48 vcrun6 vcrun2008 vcrun2022 d3dcompiler_47 mdac28 corefonts riched20 wsh57"
KRUG_VERSION="4_4"
AUTOINSTALL_EXE="${WH_TMP_DIR}/01_scada_v${KRUG_VERSION}_release.zip"
AUTOINSTALL_UNPACK="${WH_TMP_DIR}/scada_krug_${KRUG_VERSION}"
prepair_wine
if try_download "https://www.krug2000.ru/sp/01_scada_v${KRUG_VERSION}_release.zip" "${AUTOINSTALL_EXE}"; then
unpack "${AUTOINSTALL_EXE}" "${AUTOINSTALL_UNPACK}"
try_remove_file "$AUTOINSTALL_EXE"
# Корневой setup.exe в архиве — HTML-меню автозапуска, тихую установку не поддерживает (bug #59578).
# Настоящий инсталлятор — DeveloperStudio/SCADA KRUG 2000.msi, ставим его напрямую.
AUTOINSTALL_SETUP="$(find "$AUTOINSTALL_UNPACK" -iname "SCADA KRUG 2000.msi" -print -quit)"
[[ -f "$AUTOINSTALL_SETUP" ]] || fatal "В архиве дистрибутива не найден файл SCADA KRUG 2000.msi."
wine_run_install "${AUTOINSTALL_SETUP}" /qn
# Инсталлятор оставляет запущенными KrugTaskManager.exe, SMTListener.exe, KrugTaskStarter.exe
"$WINESERVER" -k
try_remove_dir "$AUTOINSTALL_UNPACK"
create_desktop "Менеджер задач" "$DRIVE_C/Program Files/Krug2000/Bin/KrugTaskManager.exe" "$PROG_ICON"
fi
+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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="ved_ctm"
export BASE_PFX="ved_ctm_pfx_x86_v05"
export WINEARCH="win32"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# info_ru: ZET 9 Lite — бесплатная астрологическая программа: натальные карты, транзиты, прогнозы, расчёты по швейцарским эфемеридам.
########################################################################
export PROG_URL="https://astrozet.net"
export WH_WINE_USE="wine-11.12-alt-tkg-wow64"
export WINEPREFIX="zet9"
export PROG_NAME="ZET 9 Lite"
export PROG_ICON="zet9"
export BASE_PFX="none"
export WINEARCH="win64"
export WH_WINDOWS_VER="10"
AUTOINSTALL_URL="https://astrozet.net/upload/ZET9SETUP229-RU.exe"
AUTOINSTALL_EXE="${WH_TMP_DIR}/ZET9SETUP229-RU.exe"
prepair_wine
if try_download "$AUTOINSTALL_URL" "$AUTOINSTALL_EXE" ; then
wine_run_install "$AUTOINSTALL_EXE" /S
try_remove_file "$AUTOINSTALL_EXE"
create_desktop "$PROG_NAME" "$DRIVE_C/ZET 9/zet.exe" "$PROG_ICON"
fi
+6 -9
View File
@@ -1,19 +1,19 @@
#!/usr/bin/env bash
##### CHECK ROOT #####
if [[ $(id -u) -ne 0 ]] ; then
if [[ $(id -u) -ne 0 ]]; then
echo "Перезапустите скрипт $0 от root пользователя!"
exit 1
fi
##### MESSAGES FUNCTIONS #####
if [[ -t 0 ]] ; then
print_error () { printf "\E[31m%s Ошибка: $* %s\e[0m\n" ;}
if [[ -t 0 ]]; then
print_error() { printf "\E[31m%s Ошибка: $* %s\e[0m\n"; }
else
print_error () { echo -e "Ошибка: $*" ;}
print_error() { echo -e "Ошибка: $*"; }
fi
fatal () {
fatal() {
print_error "$@ Аварийное завершение работы WineHelper!"
exit 1
}
@@ -29,7 +29,4 @@ apt-get update || fatal "Не удалось обновить список до
##### INSTALL DEPENDENCIES #####
# fonts-ttf-ms
apt-get install -y i586-{wine,glibc-core,libstdc++6,glibc-pthread,glibc-nss,\
libnm,libnss,libnss-mdns,libnsl1,libunwind,libunixODBC2,ocl-icd,libfreetype,\
libcups,libfontconfig1,libgnutls30,libGL,libEGL,libvulkan1,xorg-dri-swrast,\
xorg-dri-intel,xorg-dri-radeon} || fatal "Не удалось установить зависимости."
apt-get install -y i586-{wine,glibc-core,libstdc++6,glibc-pthread,glibc-nss,libnm,libnss,libnss-mdns,libnsl1,libunwind,libunixODBC2,ocl-icd,libfreetype,libcups,libfontconfig1,libgnutls30,libGL,libEGL,libvulkan1,xorg-dri-swrast,xorg-dri-intel,xorg-dri-radeon} || fatal "Не удалось установить зависимости."
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Автоматизация установки МИС Витакарта (AIS LPU Client).
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="ais-lpu-client"
export PROG_NAME="AIS LPU Client"
export PROG_ICON="ais_lpu_client"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 250 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# info_ru: ADEM 9.0 — интегрированная система CAD/CAM/CAPP/PDM для конструкторско-технологической подготовки производства. Ручная установка из дистрибутива CADCAMCAPPAdem90st.exe.
########################################################################
export PROG_URL="https://adem.ru"
export WH_WINE_USE="wine-11.18-alt-adem-wow64"
export WINEPREFIX="adem"
export PROG_NAME="ADEM 9.0"
export PROG_ICON="adem"
export BASE_PFX="adem_pfx_x64_v02"
export WINEARCH="win64"
export WH_WINDOWS_VER="10"
export INSTALL_DLL="dotnet48 vcrun2008 vcrun2010 vcrun2012 vcrun2022 corefonts"
if [[ -f "$2" ]] \
&& [[ $(basename "$2") =~ ^CADCAMCAPPAdem.*\.exe$ ]]
then
prepair_wine
# Inno Setup. Файлы CADCAMCAPPAdem90st-N.bin должны лежать рядом с exe.
# Установщик сам вызывает SwitchLang.exe (xcopy русских ресурсов) и regsvr32.
wine_run_install "$2" # /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
ADEM_PATH="$DRIVE_C/Program Files (x86)/Adem Group/Adem90"
create_desktop "$PROG_NAME" "$ADEM_PATH/2-d/admsrv90r.exe" "$PROG_ICON"
else
fatal "Не найден файл установки CADCAMCAPPAdem90st.exe. Перезапустите по примеру:
winehelper install $1 \"/путь/до/CADCAMCAPPAdem90st.exe\""
fi
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Специализированное программное обеспечение «Автономная станция прослушивания» предназначено для загрузки и прослушивания электронных материалов итогового собеседования.
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="desktopaudiostation"
export PROG_NAME="АС Прослушивания"
export PROG_ICON="desktopaudiostation"
+2 -1
View File
@@ -2,12 +2,13 @@
# info_ru: Установщик "ЕМИАС" - Единая медицинская информационно-аналитическая система
########################################################################
export PROG_URL="https://emias.info/"
export WH_WINE_USE="wine-11.0-alt-tkg-wow64"
export WH_WINE_USE="wine-11.0-alt-emias-wow64"
export WINEPREFIX="emias"
export PROG_NAME="КИС ЕМИАС"
export BASE_PFX="none"
export WINEARCH="win64"
export WH_WINDOWS_VER="7"
export WH_MAIN_DECORATED="0"
export WH_USE_CPCSP_PROXY="1"
export CPCSP_PROXY_WOW64_VER="0.7.7-alt1.1-em-wow64"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Специализированное программное обеспечение «Автономная станция записи» предназначено для записи ответов участников итогового собеседования.
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="isrecordingstation"
export PROG_NAME="АС Записи"
export PROG_ICON="isrecordingstation"
+1 -5
View File
@@ -2,7 +2,7 @@
# info_ru: Установщик программного комплекса NetTest (демо-версия)
########################################################################
export PROG_URL="https://www.kpolyakov.spb.ru/prog/nettest/nettget.htm"
export WH_WINE_USE="wine-10.0-tkg-amd64"
export WH_WINE_USE="wine-10.0-alt-tkg-amd64"
export WINEPREFIX="nettest"
export PROG_NAME="NetTest"
export BASE_PFX="none"
@@ -33,7 +33,3 @@ else
fatal "Не найден файл архива для $PROG_NAME. Перезапустите по примеру:
winehelper install $1 \"/путь/до/архива\""
fi
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: "КИС Олимп" - программно-аналитический комплекс, состоящий из самостоятельных подключаемых модулей.
########################################################################
export PROG_URL="https://olimp.best/"
export WH_WINE_USE="wine-10.0-tkg-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="olimp"
export PROG_NAME="КИС Олимп"
export BASE_PFX="olimp_pfx_x86_v04"
-25
View File
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# info_ru: Это мощный, современный инструмент быстрой и качественной разработки АСУТП
########################################################################
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"
export BASE_PFX="scadakrug2k_pfx_v01"
export WH_WINDOWS_VER="10"
export WINEARCH="win32"
export INSTALL_DLL="dotnet48 vcrun6 vcrun2008 vcrun2022 d3dcompiler_47 mdac28 corefonts riched20 wsh57"
export USE_RENDERER="proton"
if [[ -f "$2" ]] ; then
prepair_wine
kill_autostart "KrugTaskManager.exe" &
wine_run_install "$2" /quiet
WIN_FILE_EXEC="$DRIVE_C/Program Files/Krug2000/Bin/KrugTaskManager.exe"
create_desktop "Менеджер задач" "$WIN_FILE_EXEC" "$PROG_ICON"
else
fatal "Не найден файл установки для $PROG_NAME. Перезапустите по примеру:
winehelper install $1 \"/путь/до/установочного_файла\""
fi
+9
View File
@@ -0,0 +1,9 @@
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = ["E", "W", "F", "I", "ISC"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
file="sha256sum.list"
if [[ ! -f "$file" ]]; then
exit 0
fi
awk '
/^#/ { next }
NF == 0 { next }
$1 !~ /^[a-f0-9]{64}$/ {
print "Invalid hash line: " NR ": " $0
exit 1
}
' "$file"
+20 -10
View File
@@ -1,21 +1,26 @@
##### WINE WOW64 #####
7f3b22aeb72b96f1c1c33c5d4cf108a56b8aacea461e23d511e1064bcf448caa wine-11.9-tkg-wow64.tar.xz
8b9a6548eb5df921514cd569607e7f845acd18a3a6cf56f1f724b38bc9a94e43 wine-11.6.1-scadoffice-alt5-wow64.tar.xz
496e8436a57eb6617d7d90f06b9d7d80d12a5604fb676eddedb2eab346d4f71a wine-11.19-alt-tkg-wow64.tar.xz
da8a6b1ced50e4563e00494f2dceddd11b8dbeebd80979172a176242a63a06b8 wine-11.18-alt-adem-wow64.tar.xz
e99a44d9f302869287cbe699c2800e3ab4b3e1f9501f20fa3b0491c9cfaae1e7 wine-11.12-alt-tkg-wow64.tar.xz
90676579fc97b0bdd0cc265b5c728683f8ce8d8998b020ac39d7c26ebf8783d9 wine-11.0-alt-tkg-wow64.tar.xz
349c707148a23e667970309248bbbf97b4e2a0db59c548e73d9fe9c3f585872c wine-10.18.1-alt1-wow64.tar.xz
d8275a94f7538e600b4c82b0693a9ab1ebf0d5f141ea7ec3bc2a0e230b15676b wine-11.0-alt-emias-wow64.tar.xz
d86724bd1b3a7a798f221a6a84430d37a3f531014756b7de49c0d8444d748bd7 wine-10.9-alt-tflex-wow64.tar.xz
eab43cb4ef6c69ef8c4c8af9e1afadb83141d4d6b0d51afb69fc47596776ecc2 wine-10.0-tkg-wow64.tar.xz
1f5214b5070066e6868da4f8961506e1d80a00470e5a135945386421a4ca02df wine-10.0.2-alt-scadoffice-wow64.tar.xz
714687110724f4772d4028fb340ac1c8f9a32f0454c76822c967af9ef06844eb wine-10.0-alt-tkg-wow64.tar.xz
4826028ddd49cc6ec2760be8dbb8ff2353a58323a87c846a8667e41e90851ea1 wine-9.0-alt-tkg-wow64.tar.xz
##### WINE AMD64 #####
bb1e67282255eb1a927834082d0ef8f6900c76d598e7d6453250000cf2f1218a wine-11.9-tkg-amd64.tar.xz
d1bfda024138f6e83ee531df513e6ebebb7aee1e40ce3f5cd3a4d0631107e482 wine-11.12-alt-tkg-amd64.tar.xz
7a735f1e00173d8d3e3727aab927c4eb86aca018f39a4e8dfc1936909b8b9654 wine-11.0-alt-tkg-amd64.tar.xz
bcd2f4f5d5f61da73352c6f4ca900e86eb93d80cdd28668be07d7c80c34e3501 wine-10.0-tkg-amd64.tar.xz
fd209c95a3aa507002374417365b3d1119dbe3a0a6d223034171221a2b187b66 wine-9.0-cx-amd64.tar.xz
2f3aa24fcb52a12bac35a77bf088aa8308e2e0ac3bf89f9b3f9e8e3c7090bbe8 wine-10.0-alt-tkg-amd64.tar.xz
4251e75b410268fca9dabc168f838a56f5f02b970c6d4250763da8e791e8880d wine-9.0-alt-tkg-amd64.tar.xz
##### WINE I586 #####
d9210f9be6ccc8be38d5d201aa06a924eeb6079f062dd4bad22da5e3eedeb630 wine-11.12-alt-sk2000-i586.tar.xz
b660de1ce16fc90f54997e6cf30d9cf92daa932f5ef14ba8bd7cd5d7a4fa25af wine-11.12-alt-tkg-i586.tar.xz
b082dc92c0ed96366353b8a9f62b126276d2476be85d2b37d47a886771a808b6 wine-11.0-alt-tkg-i586.tar.xz
49ce3e1152f01c1f24ca148092a66f6eccd68cf4ee35d0cb04900120f45dd864 wine-10.12.1-alt1-i586.tar.xz
6e4346f7d2631c1f6859eb46c428fba84c6e61063c5f622e11a83ad59e5b00db wine-10.0-tkg-i586.tar.xz
d63ae109e2495d3817172146107d9ddfd0b63a0f76b4d575279ae6b642522795 wine-10.0-alt-tkg-i586.tar.xz
9a3160ae815b2dd008f67bf230b4e440c6b341d770b7c2a4b1a2c50283c7a895 wine-9.0-alt-tkg-i586.tar.xz
##### DXVK #####
98411effbc1b3611b11c4ca3af2ad29400b08e1c8c380791d6351600a06fb3c7 dxvk-1.10.3-28.tar.xz
@@ -189,7 +194,7 @@ ef7e8f1ba785d48e4ea287feed5b79bd630d423e59efadb43da9653adefef218 ais-lpu-client
70a4cbce1b600434b4d5f241ee90dafb15f7b5f821bc1594e728493febaf017b ksamu_pfx_x64_v03.tar.xz
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
# winetricks corefonts tahoma msxml6 msxml4 msxml3 riched30 msls31 riched20 msftedit richtx32 fontsmooth=gray
# + manuall installed riched32
# + manually installed riched32
5971788003a5337c23003ad0944c59d549e025c5dc10c19d2e0ec9e7dc04819f ppdgr2_pfx_x64_v01.tar.xz
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
@@ -220,6 +225,11 @@ d16c300b5451e4fad6eeebeca0949367a7bdf1cb1878fc51f0f9dee33744d3a7 emias_pfx_x64_
# 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
9b7a241374c27ea341294053d817722e16a5e799acce08f285e62b0f60b6547f adem_pfx_x64_v02.tar.xz
# create with wine-11.18-alt-adem-wow64 (universal user: xuser)
# winetricks dotnet40 dotnet48 vcrun2008 vcrun2010 vcrun2012 vcrun2022 corefonts
# + jet40
##### ADDONS #####
# addons with ODBC, SSH, *.reg
0f4ef434df07bc338ae308af44330590eaa1d9c94b64850514e55b960642d0eb scadoffice_addons_v02.tar.xz
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа «Декларация» позволяет автоматически формировать налоговые декларации по форме 3-НДФЛ. В процессе заполнения Вами данных программа автоматически проверяет их корректность, что уменьшает вероятность появления ошибки.
########################################################################
export PROG_URL="https://www.nalog.gov.ru"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="declaration"
export PROG_NAME="Декларация"
export PROG_ICON="declarac-2"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Программное обеспечение для 3D-графики, предназначенное для проектирования освещения. (Версия 4.4.0.2)
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="easymnemo"
export PROG_NAME="Easymnemo"
export PROG_ICON="easymnemo"
+3 -3
View File
@@ -2,7 +2,7 @@
# info_ru: Программа автоматизации процесса подготовки юридическими и физическими лицами документов для представления в налоговые органы в электронной форме и на бумажном носителе.
########################################################################
export PROG_URL="https://www.nalog.gov.ru"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="nalog"
export PROG_NAME="Налогоплательщик ЮЛ"
export PROG_ICON="npul"
@@ -95,7 +95,7 @@ if [[ -n "$UPDATE_VER" ]]; 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
@@ -106,7 +106,7 @@ if [[ -n "$UPDATE_VER" ]]; then
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
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Подготовка документов для государственной регистрации (ППДГР)
########################################################################
export PROG_URL="https://www.nalog.gov.ru"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="ppdgr2"
export PROG_NAME="ППДГР-2"
export PROG_ICON="ppdgr"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Цифровая платформа для обучения и оценки специалистов нефтегазовой отрасли.
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export PROG_NAME="РТСИМ.Карьера"
export WINEPREFIX="rtsim"
export PROG_ICON="rtsim"
+10 -8
View File
@@ -2,7 +2,7 @@
# info_ru: SCAD Office — это программный комплекс для расчёта строительных конструкций.
########################################################################
export PROG_URL="https://scadoffice.ru"
export WH_WINE_USE="wine-11.6.1-scadoffice-alt5-wow64"
export WH_WINE_USE="wine-10.0.2-alt-scadoffice-wow64"
export WINEPREFIX="scadoffice"
export PROG_NAME="SCAD Office"
export PROG_ICON="scadoffice"
@@ -33,13 +33,6 @@ if [[ -n $2 ]] ; then
fi
fi
if try_download "https://scadhelp.ru/files/10/download" "${AUTOINSTALL_EXE}" ; then
create_new_dir "$DRIVE_C/SDATA"
create_new_dir "$DRIVE_C/SWORK"
wine_run_install "$AUTOINSTALL_EXE"
# try_remove_file "$AUTOINSTALL_EXE"
fi
print_info "Установка дополнительных компонентов..."
ADDONS_PACK="${WH_TMP_DIR}/$(basename "$SCADOFFICE_ADDONS_URL")"
@@ -65,6 +58,15 @@ if try_download "$SCADOFFICE_ADDONS_URL" "$ADDONS_PACK" ; then
try_remove_dir "$ADDONS_PATH"
fi
if try_download "https://scadhelp.ru/files/10/download" "${AUTOINSTALL_EXE}" ; then
create_new_dir "$DRIVE_C/SDATA"
create_new_dir "$DRIVE_C/SWORK"
wine_run_install "$AUTOINSTALL_EXE"
"$WINESERVER" -k
# try_remove_file "$AUTOINSTALL_EXE"
fi
# Определение всех программ, значков и исполняемых файлов
declare -a PROG_NAME_All=("Арбат" "Вест" "Глобальные настройки" "Декор" "Дискретная арматура" "Запрос" "Камин" "КоКон" "Комета" "Конструктор сечений" "Консул" "Конфигуратор лицензий" "Кристалл" "Кросс" "Куст" "Магнум" "Монолит" "Откос" "Пастернак" "Преобразование ед. измерений" "Cортамент металлопроката" "Расчет по формуле" "Редактор акселерограмм" "Редактор динамичности" "Редактор материалов" "Тонус" "Эквивалентное сечение" "SCAD++")
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Справочник кодов обозначения налоговых органов для целей учета налогоплательщиков
########################################################################
export PROG_URL="https://www.nalog.gov.ru"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="soun"
export PROG_NAME="СОУН"
export PROG_ICON="soun"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Программный комплекс для энергетики.
########################################################################
export WH_WINE_USE="wine-10.0-tkg-amd64"
export WH_WINE_USE="wine-10.0-alt-tkg-amd64"
export WINEPREFIX="swmanmodus"
export BASE_PFX="none"
export WH_WINDOWS_VER="10"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# info_ru: Программный комплекс СуперОкна для фабрик по изготовлению окон и дверей.
########################################################################
export WH_WINE_USE="wine-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-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-10.12.1-alt1-i586"
export WH_WINE_USE="wine-10.0-alt-tkg-i586"
export WINEPREFIX="swnd"
export PROG_NAME="СуперОкна 8 бесплатная"
export PROG_ICON="swnd"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: Программа проверки файлов на соответствие форматам представления в электронном виде налоговых деклараций, бухгалтерской отчетности.
########################################################################
export PROG_URL="https://www.nalog.gov.ru"
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
export WH_WINE_USE="wine-10.0-alt-tkg-wow64"
export WINEPREFIX="nalog"
export PROG_NAME="Тестер"
export PROG_ICON="tester"
+1 -1
View File
@@ -2,7 +2,7 @@
# info_ru: ZuluGIS 2021 Инженерные расчёты (Полный) 64-бит. Включает ZuluGIS, пакеты расчетов ZuluThermo, ZuluHydro, ZuluDrain, ZuluGaz, ZuluSteam, библиотеки ZuluXTools и ZuluNetTools. Дополнительно в состав пакета входит Microsoft SQL Server Express LocalDB 2012.
########################################################################
export PROG_URL="https://www.politerm.com"
export WH_WINE_USE="wine-10.0-tkg-amd64"
export WH_WINE_USE="wine-10.0-alt-tkg-amd64"
export PROG_NAME="ZuluGIS 2021"
export WINEPREFIX="zulugis"
export PROG_ICON="zulugis"
+1027 -812
View File
File diff suppressed because it is too large. Load diff
+556 -3090
View File
File diff suppressed because it is too large. Load diff
+3 -62
View File
@@ -7,66 +7,7 @@ GUI пакет для winehelper.
- services: Бизнес-логика (кэширование, установка и т.д.)
- parsers: Парсеры (sha256sum.list, winetricks output)
- utils: Утилиты (обработчики логов и т.д.)
Реэкспортов нет: импорты — только абсолютные из конкретных модулей
(например, from winehelperqt.services.prefix_manager import PrefixManager).
"""
# 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',
]
-27
View File
@@ -1,30 +1,3 @@
"""
Диалоги для 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',
]
+120 -125
View File
@@ -2,19 +2,32 @@
Базовые классы для диалогов выбора версий.
Устраняет дублирование кода между 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.logger import get_logger
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.qt_compat import (
QDialog,
QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QProcess,
QPushButton,
QScrollArea,
Qt,
QTabWidget,
QTimer,
QVBoxLayout,
QWidget,
)
from winehelperqt.utils.environment import EnvironmentConfig
from winehelperqt.utils.grid_utils import GridUtils
logger = get_logger(__name__)
class BaseVersionSelectionDialog(QDialog):
@@ -25,15 +38,37 @@ class BaseVersionSelectionDialog(QDialog):
- Поле поиска с фильтрацией
- Сетка кнопок с версиями
- Базовая логика выбора версии
Построение UI двухфазное: конечный наследник обязан вызвать
self._setup_ui() в конце своего __init__, когда установлены все поля,
необходимые виртуальным методам (_create_version_container, populate_ui).
"""
NUM_COLUMNS = 3
version_selected = pyqtSignal(str)
# Кэш версии системного Wine на время сессии.
# None — версия ещё не определена; "" — определить не удалось
# (кэшируем и неудачу, чтобы не запускать wine --version при каждом открытии диалога);
# непустая строка — определённая версия.
_system_wine_version_cache = None
@classmethod
def get_system_wine_version(cls):
"""Возвращает версию системного Wine (None — ещё не определена)."""
return cls._system_wine_version_cache
@classmethod
def set_system_wine_version(cls, version):
"""Запоминает результат определения версии системного Wine (включая неудачу)."""
cls._system_wine_version_cache = version or ""
def __init__(self, title, parent=None, min_width=600, min_height=400):
"""
Инициализирует базовый диалог выбора версий.
Не строит UI: виртуальные методы (_create_version_container,
load_versions, populate_ui) зависят от полей наследников, поэтому
_setup_ui() вызывается конечным классом в конце своего __init__.
"""
super().__init__(parent)
@@ -46,15 +81,13 @@ class BaseVersionSelectionDialog(QDialog):
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')
# Фолбэк: явно разрешаем путь из расположения пакета
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sha256sum.list"))
def _setup_ui(self):
"""Создает базовый UI диалога."""
@@ -77,71 +110,14 @@ class BaseVersionSelectionDialog(QDialog):
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)
grid_layout = getattr(self, "grid_layout", None)
GridUtils.filter_buttons(self.buttons, self.search_edit.text(), grid_layout, self.NUM_COLUMNS)
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):
"""
@@ -155,13 +131,11 @@ class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
self.architecture = architecture
self.wine_versions_data = {}
self.system_wine_display_name = "Системная версия"
self.system_wine_available = True
self.selected_display_text = None
super().__init__(
title=f"Выбор версии Wine для {architecture} префикса",
parent=parent,
min_width=900,
min_height=500
title=f"Выбор версии Wine для {architecture} префикса", parent=parent, min_width=900, min_height=500
)
def _create_version_container(self):
@@ -177,7 +151,7 @@ class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
loading_widget = QWidget()
loading_layout = QVBoxLayout(loading_widget)
status_label = QLabel("Загрузка, пожалуйста, подождите...")
status_label.setAlignment(Qt.AlignCenter)
status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_layout.addWidget(status_label)
self.main_tabs.addTab(loading_widget, "Загрузка...")
self.main_tabs.setEnabled(False)
@@ -193,11 +167,7 @@ class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
self.wine_versions_data = Sha256Parser.parse_wine_versions(sha256_path)
if not self.wine_versions_data:
QMessageBox.warning(
self,
"Ошибка",
f"Файл с версиями не найден или пуст:\n{sha256_path}"
)
QMessageBox.warning(self, "Ошибка", f"Файл с версиями не найден или пуст:\n{sha256_path}")
def _get_installed_versions(self):
"""Возвращает список локально установленных версий Wine."""
@@ -207,35 +177,74 @@ class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
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}")
"""
Получает версию системного Wine (с кэшированием на время сессии).
Определение выполняется асинхронно через QProcess, чтобы открытие
диалога не блокировало GUI на время работы wine --version; по готовности
текст кнопки системной версии обновляется на вкладке «Установленные версии Wine».
Returns:
Строка с версией; "Системная версия", если версию определить
не удалось; None, если Wine не установлен в системе.
"""
cached = BaseVersionSelectionDialog.get_system_wine_version()
if cached is not None:
return cached or "Системная версия"
if not shutil.which("wine"):
logger.debug("Системный wine не найден в системе")
return None
self._probe_system_wine_version()
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')
def _probe_system_wine_version(self):
"""Запускает асинхронное определение версии системного Wine (без блокировки GUI)."""
if BaseVersionSelectionDialog.get_system_wine_version() is not None:
# Версию уже определил другой диалог или прогрев при старте GUI:
# обновим кнопку из кэша на следующем тике событий
QTimer.singleShot(0, self._update_system_wine_button_text)
return
if is_win64_prefix and re_32bit_version.search(version_name):
return False
proc = QProcess(self)
proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
if is_win32_prefix:
if version_name == "system" or 'wow64' in version_name:
return False
def on_finished(exit_code, _exit_status):
version = ""
try:
if exit_code == 0:
output = proc.readAllStandardOutput().data().decode("utf-8", errors="ignore").strip()
version = output.splitlines()[0].strip() if output else ""
except RuntimeError:
pass
# Кэшируем и неудачу (""), чтобы повторные открытия диалога
# не запускали новые процессы wine --version
BaseVersionSelectionDialog.set_system_wine_version(version)
if version:
logger.debug("Системный wine: %s", version)
self._update_system_wine_button_text()
else:
logger.warning("Не удалось определить версию системного wine")
return True
def on_error(_error):
# Не перезаписываем версию, если её уже определил параллельный запрос
if BaseVersionSelectionDialog.get_system_wine_version() is None:
BaseVersionSelectionDialog.set_system_wine_version("")
proc.finished.connect(on_finished)
proc.errorOccurred.connect(on_error)
proc.start("wine", ["--version"])
def _update_system_wine_button_text(self):
"""Обновляет текст кнопки системной версии после асинхронного определения версии."""
version = BaseVersionSelectionDialog.get_system_wine_version()
if not version:
return
for btn in getattr(self, "installed_buttons", []):
if getattr(btn, "value_name", None) == "system":
btn.setText(f"{version} (Системный)")
break
def populate_ui(self):
"""Заполняет UI версиями Wine."""
@@ -247,13 +256,7 @@ class ComponentVersionSelectionDialogBase(BaseVersionSelectionDialog):
Базовый класс для диалога выбора версии компонента (DXVK, VKD3D).
"""
def __init__(
self,
component_group,
title,
parent=None,
add_extra_options=True
):
def __init__(self, component_group, title, parent=None, add_extra_options=True):
"""
Инициализирует диалог выбора версии компонента.
"""
@@ -261,25 +264,20 @@ class ComponentVersionSelectionDialogBase(BaseVersionSelectionDialog):
self.versions_data = []
self.add_extra_options = add_extra_options
super().__init__(
title=title,
parent=parent,
min_width=600,
min_height=400
)
super().__init__(title=title, parent=parent, min_width=700, min_height=400)
def _create_version_container(self):
"""Создает контейнер для версий компонента."""
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.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.grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.load_versions(start_row=0)
@@ -311,10 +309,7 @@ class ComponentVersionSelectionDialogBase(BaseVersionSelectionDialog):
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
)
self.versions_data = Sha256Parser.parse_component_versions(sha256_path, self.component_group)
def populate_ui(self, start_row):
"""Заполняет UI версиями компонента."""
+55
View File
@@ -0,0 +1,55 @@
from winehelperqt.qt_compat import QDialog, QProcess, QTimer
from winehelperqt.utils.dialog_utils import DialogUtils
class CommandDialog(QDialog):
"""Модальный диалог для выполнения команд winehelper с защитой от случайного закрытия."""
TERMINATE_GRACE_MS = 10000
def __init__(self, parent=None):
super().__init__(parent)
self._process = None
# True, если пользователь сам подтвердил прерывание через закрытие окна
self.aborted_by_user = False
def set_process(self, process):
self._process = process
@staticmethod
def _process_is_running(process) -> bool:
"""Жив ли процесс (False если объект уже удалён)."""
try:
return process.state() == QProcess.ProcessState.Running
except RuntimeError:
return False
def closeEvent(self, event):
if self._process is None or not self._process_is_running(self._process):
event.accept()
return
if not DialogUtils.confirm(
self,
"Прервать выполнение?",
"Процесс ещё выполняется.\nВы действительно хотите прервать его?",
yes_text="Да, прервать",
no_text="Нет",
):
event.ignore()
return
self.aborted_by_user = True
proc = self._process
self._process = None
try:
proc.terminate()
# Окно закрываем сразу, не дожидаясь смерти процесса:
# если процесс игнорирует SIGTERM, через TERMINATE_GRACE_MS его убьём
QTimer.singleShot(
self.TERMINATE_GRACE_MS,
lambda: proc.kill() if self._process_is_running(proc) else None,
)
except RuntimeError:
pass
event.accept()
+13 -28
View File
@@ -2,19 +2,18 @@
Диалог для выбора версии компонента (DXVK, VKD3D).
"""
from functools import partial
from PyQt5.QtWidgets import (
QPushButton
)
from winehelperqt.dialogs.base_version_dialog import ComponentVersionSelectionDialogBase
from winehelperqt.dialogs.wine_version import VersionButtonWidget
from winehelperqt.utils.version_utils import sort_versions
class ComponentVersionSelectionDialog(ComponentVersionSelectionDialogBase):
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
def __init__(self, component_group, title, parent=None, add_extra_options=True):
def __init__(self, component_group, title, parent=None, add_extra_options=True, installed_version=None):
"""
Инициализирует диалог выбора версии компонента.
@@ -23,20 +22,23 @@ class ComponentVersionSelectionDialog(ComponentVersionSelectionDialogBase):
title: Заголовок диалога
parent: Родительский виджет
add_extra_options: Добавлять ли кнопки действий
installed_version: Текущая установленная версия компонента в префиксе
"""
self.installed_version = installed_version
super().__init__(
component_group=component_group,
title=title,
parent=parent,
add_extra_options=add_extra_options
component_group=component_group, title=title, parent=parent, add_extra_options=add_extra_options
)
# UI строим в конце: к этому моменту установлены поля всех уровней
self._setup_ui()
def populate_ui(self, start_row):
"""Заполняет UI кнопками версий."""
versions = sorted(self.versions_data, reverse=True)
versions = sort_versions(self.versions_data, reverse=True)
row, col = start_row, 0
for version_name in versions:
btn = QPushButton(version_name)
is_installed = version_name == self.installed_version
btn = VersionButtonWidget(version_name, version_name, is_installed=is_installed, show_icon=True)
btn.clicked.connect(partial(self.on_version_selected, version_name))
self.grid_layout.addWidget(btn, row, col)
self.buttons.append(btn)
@@ -45,23 +47,6 @@ class ComponentVersionSelectionDialog(ComponentVersionSelectionDialogBase):
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
+23
View File
@@ -0,0 +1,23 @@
"""
Диалог создания лога запуска программы с выбором каналов WINEDEBUG.
"""
from winehelperqt.dialogs.winedebug_base import CREATE_LOG_CHANNELS_KEY, WinedebugChannelsDialog
class CreateLogDialog(WinedebugChannelsDialog):
"""Диалог для запуска приложения с созданием лога."""
def __init__(self, parent=None):
super().__init__(
title="Создание лога",
description=(
"Приложение будет запущено в режиме отладки.\n\n"
"После закрытия приложения лог будет сохранен в папке 'winehelper_backup_log' "
"в вашем домашнем каталоге под именем (пример: prefix_program.log).\n\n"
"Укажите каналы отладки WINEDEBUG:"
),
primary_button_text="Запустить",
channels_key=CREATE_LOG_CHANNELS_KEY,
parent=parent,
)
+58 -29
View File
@@ -1,26 +1,39 @@
"""
Диалог для создания нового префикса.
"""
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.windows_version import WindowsVersionSelectionDialog
from winehelperqt.dialogs.wine_version import WineVersionSelectionDialog
from winehelperqt.parsers.sha256_parser import Sha256Parser
from winehelperqt.qt_compat import (
QComboBox,
QDialog,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QRadioButton,
QVBoxLayout,
QWidget,
)
from winehelperqt.utils.environment import EnvironmentConfig
from winehelperqt.utils.font_utils import app_font
class CreatePrefixDialog(QDialog):
"""Диалог для создания нового префикса."""
PREFIX_NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
PREFIX_NAME_CHARS = r"[^a-zA-Z0-9_-]"
def __init__(self, parent=None):
super().__init__(parent)
self.parent_gui = parent
self.parent_tab = parent
self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(680, 250)
self.setModal(True)
@@ -31,6 +44,7 @@ class CreatePrefixDialog(QDialog):
self.prepared_prefixes = {}
self.selected_wine_version_value = None
self.selected_wine_version_display = None
self.selected_windows_version = "10"
layout = QVBoxLayout(self)
form_layout = QFormLayout()
@@ -65,6 +79,7 @@ class CreatePrefixDialog(QDialog):
form_layout.addRow("<b>Разрядность:</b>", arch_widget)
self.prefix_template_selector = QComboBox()
self.prefix_template_selector.setFixedWidth(170)
self.prefix_template_selector.setToolTip("Выберите шаблон для создания префикса.")
form_layout.addRow("<b>Шаблон префикса:</b>", self.prefix_template_selector)
@@ -80,11 +95,18 @@ class CreatePrefixDialog(QDialog):
version_layout.addWidget(select_version_button)
form_layout.addRow("<b>Версия Wine:</b>", version_layout)
self.windows_version_combo = QComboBox()
self.windows_version_combo.setFixedWidth(170)
for display_name, value in WindowsVersionSelectionDialog.WINDOWS_VERSIONS:
self.windows_version_combo.addItem(display_name, value)
self.windows_version_combo.setCurrentIndex(self.windows_version_combo.findData(self.selected_windows_version))
form_layout.addRow("<b>Версия Windows:</b>", self.windows_version_combo)
layout.addLayout(form_layout)
button_layout = QHBoxLayout()
self.create_button = QPushButton("Создать")
self.create_button.setFont(QFont('Arial', 11, QFont.Bold))
self.create_button.setFont(app_font(11, bold=True))
self.create_button.setStyleSheet("background-color: #0078d7; color: white;")
self.create_button.setEnabled(False)
self.create_button.clicked.connect(self.accept_creation)
@@ -100,13 +122,16 @@ class CreatePrefixDialog(QDialog):
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)
# Подключаем один раз здесь: при подключении внутри on_architecture_changed
# обработчики накапливались бы при каждом переключении радио 32/64-bit
self.prefix_template_selector.currentIndexChanged.connect(self._update_template_tooltip)
self._load_prepared_prefixes()
self.on_architecture_changed()
def _load_prepared_prefixes(self):
"""Загружает и парсит шаблоны префиксов из sha256sum.list."""
self.prepared_prefixes = {'win32': [], 'win64': []}
self.prepared_prefixes = {"win32": [], "win64": []}
data_path = EnvironmentConfig.DATA_PATH
if not data_path:
QMessageBox.warning(self, "Ошибка", "Переменная окружения DATA_PATH не установлена.")
@@ -123,7 +148,7 @@ class CreatePrefixDialog(QDialog):
"""Открывает диалог выбора версии Wine."""
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
dialog = WineVersionSelectionDialog(architecture, self)
if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
if dialog.exec() == QDialog.DialogCode.Accepted and dialog.selected_version:
self.wine_version_edit.setText(dialog.selected_display_text)
self.selected_wine_version_value = dialog.selected_version
@@ -140,23 +165,26 @@ class CreatePrefixDialog(QDialog):
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.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'])
# Сигнал был заблокирован на время заполнения списка — обновляем подсказку вручную
self._update_template_tooltip(self.prefix_template_selector.currentIndex())
def _update_template_tooltip(self, index):
"""Обновляет подсказку выпадающего списка под выбранный шаблон."""
data = self.prefix_template_selector.itemData(index)
if data:
self.prefix_template_selector.setToolTip(data["tooltip"])
else:
self.prefix_template_selector.setToolTip("Выберите шаблон для создания префикса.")
def validate_prefix_name(self, text):
"""Проверяет имя префикса в реальном времени."""
valid_pattern = r'^[a-zA-Z0-9_-]*$'
if re.match(valid_pattern, text):
if re.match(self.PREFIX_NAME_PATTERN, text):
self.name_warning_label.setVisible(False)
else:
cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
cleaned_text = re.sub(self.PREFIX_NAME_CHARS, "", text)
self.prefix_name_edit.blockSignals(True)
self.prefix_name_edit.setText(cleaned_text)
self.prefix_name_edit.blockSignals(False)
@@ -168,9 +196,7 @@ class CreatePrefixDialog(QDialog):
"""Включает или выключает кнопку 'Создать'."""
name_ok = bool(self.prefix_name_edit.text().strip())
version_ok = bool(self.wine_version_edit.text().strip())
self.create_button.setEnabled(
name_ok and version_ok and not self.name_warning_label.isVisible()
)
self.create_button.setEnabled(name_ok and version_ok and not self.name_warning_label.isVisible())
def accept_creation(self):
"""Валидирует данные, сохраняет их и закрывает диалог с успехом."""
@@ -180,10 +206,11 @@ class CreatePrefixDialog(QDialog):
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
return
if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
if not re.match(self.PREFIX_NAME_PATTERN, prefix_name):
QMessageBox.warning(
self, "Ошибка",
"Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания."
self,
"Ошибка",
"Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания.",
)
return
@@ -195,8 +222,10 @@ class CreatePrefixDialog(QDialog):
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']
template_data = self.prefix_template_selector.itemData(self.prefix_template_selector.currentIndex())
# Список шаблонов может быть пуст (секция PREFIX отсутствует в sha256sum.list)
self.base_pfx = template_data["value"] if template_data else "none"
self.selected_wine_version_display = self.wine_version_edit.text()
self.selected_windows_version = self.windows_version_combo.currentData()
self.accept()
+96
View File
@@ -0,0 +1,96 @@
"""
Модуль вывода предупреждения о проверке установки КриптоПро в системе.
Выводит информационное окно со ссылками на официальный сайт КриптоПро
и инструкцию по установке КриптоПро в систему.
"""
import sys
from winehelperqt.qt_compat import (
QApplication,
QDialog,
QTextBrowser,
QVBoxLayout,
)
class CryptoProWarningDialog(QDialog):
"""Диалог предупреждения о проверке установки КриптоПро."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("WineHelper — Предупреждение!")
self.setMinimumSize(600, 400)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(12)
self.info_browser = QTextBrowser()
self.info_browser.setOpenExternalLinks(True)
self.info_browser.setStyleSheet("""
QTextBrowser {
background-color: #1e1e1e;
color: #e4e4e4;
border: none;
}
""")
self.info_browser.setHtml(self._build_html_content())
layout.addWidget(self.info_browser)
def _build_html_content(self) -> str:
"""Формирует HTML-содержимое окна."""
body_style = (
"font-family: 'Segoe UI', 'Noto Sans', sans-serif; font-size: 14px; color: #e4e4e4; line-height: 1.55;"
)
warning_style = "background-color: #3d351f; border-left: 4px solid #ffcc00; padding: 12px 14px; margin: 0;"
warning_text_style = "color: #ffd700; font-weight: bold; text-align: center; margin: 0 0 6px 0;"
card_style = "padding: 14px; margin: 0;"
title_style = "font-size: 15px; font-weight: bold; color: #ffffff; margin: 0 0 10px 0;"
paragraph_style = "margin: 0 0 10px 0;"
link_style = "color: #7ec7ff;"
return f"""
<html>
<body style="{body_style}">
<p style="text-align: center; font-size: 20px; font-weight: bold; color: #ffd700; margin: 0 0 8px 0;">
Предупреждение!
</p>
<div style="{warning_style}">
<p style="{warning_text_style} margin-bottom: 0;">
⚠️ Проверьте правильность установки КриптоПро в системе!
</p>
</div>
<div style="line-height: 18px; font-size: 1px;">&nbsp;</div>
<div style="{card_style}">
<p style="{title_style}">Официальный сайт КриптоПро</p>
<p style="{paragraph_style}">
<a style="{link_style}" href="https://cryptopro.ru/products/csp">
https://cryptopro.ru/products/csp
</a>
</p>
<p style="{title_style} margin-top: 14px;">Инструкция установки КриптоПро в систему</p>
<p style="{paragraph_style} margin-bottom: 0;">
<a style="{link_style}" href="https://www.altlinux.org/CryptoPro">
https://www.altlinux.org/CryptoPro
</a>
</p>
</div>
</body>
</html>
"""
def main() -> int:
"""Точка входа для автономного запуска модуля."""
app = QApplication(sys.argv)
dialog = CryptoProWarningDialog()
dialog.show()
return app.exec()
if __name__ == "__main__":
sys.exit(main())
+10 -16
View File
@@ -1,10 +1,8 @@
"""
Диалог для управления ассоциациями файлов (WH_XDG_OPEN).
"""
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QMessageBox
)
from PyQt5.QtCore import Qt
from winehelperqt.qt_compat import QDialog, QDialogButtonBox, QLabel, QLineEdit, QMessageBox, Qt, QVBoxLayout
class FileAssociationsDialog(QDialog):
@@ -27,7 +25,7 @@ class FileAssociationsDialog(QDialog):
"<b>Пример:</b> <code>pdf docx txt</code>"
)
info_label.setWordWrap(True)
info_label.setTextFormat(Qt.RichText)
info_label.setTextFormat(Qt.TextFormat.RichText)
layout.addWidget(info_label)
self.associations_edit = QLineEdit()
@@ -37,13 +35,11 @@ class FileAssociationsDialog(QDialog):
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)
forbidden_label = QLabel("<small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small>")
forbidden_label.setTextFormat(Qt.TextFormat.RichText)
layout.addWidget(forbidden_label)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.validate_and_accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
@@ -53,22 +49,20 @@ class FileAssociationsDialog(QDialog):
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()
}
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.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("Недопустимые расширения")
msg_box.setTextFormat(Qt.RichText)
msg_box.setTextFormat(Qt.TextFormat.RichText)
msg_box.setText(
"Следующие расширения запрещены и не могут быть использованы:<br><br>"
f"<b>{', '.join(sorted(list(found_forbidden)))}</b>"
)
msg_box.exec_()
msg_box.exec()
return
self.new_associations = " ".join(sorted(list(entered_extensions)))
+205
View File
@@ -0,0 +1,205 @@
"""
Диалог/виджет глобальных настроек WineHelper GUI.
"""
from winehelperqt.logger import get_logger, setup_logger
from winehelperqt.qt_compat import (
CURRENT_BINDING,
QApplication,
QComboBox,
QFormLayout,
QHBoxLayout,
QLabel,
QMessageBox,
QProcess,
QPushButton,
Qt,
QVBoxLayout,
QWidget,
available_bindings,
)
from winehelperqt.services.global_settings_service import GlobalSettingsService
from winehelperqt.utils.dialog_utils import DialogUtils
from winehelperqt.utils.environment import EnvironmentConfig
logger = get_logger(__name__)
class GlobalSettingsWidget(QWidget):
"""Виджет с глобальными настройками приложения."""
def __init__(self, parent=None):
super().__init__(parent)
self.settings_service = GlobalSettingsService()
self._setup_ui()
self._load_settings()
def _setup_ui(self):
"""Создаёт элементы управления."""
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
dev_notice_label = QLabel(
"Эта вкладка отображается только в режиме разработки (тестовый запуск из репозитория)."
)
dev_notice_label.setWordWrap(True)
dev_notice_label.setStyleSheet("color: #e05000; font-weight: bold; font-size: 13pt;")
layout.addWidget(dev_notice_label)
form_layout = QFormLayout()
form_layout.setSpacing(10)
self.log_level_label = QLabel("Уровень логирования:")
self.log_level_label.setToolTip(
"debug — максимально подробный вывод\n"
"info — обычный вывод\n"
"warning — только предупреждения и ошибки\n"
"error — только ошибки"
)
self.log_level_combo = QComboBox()
self.log_level_combo.setMinimumWidth(200)
self.log_level_combo.addItem("Отладка (debug)", "debug")
self.log_level_combo.addItem("Информация (info)", "info")
self.log_level_combo.addItem("Предупреждения (warning)", "warning")
self.log_level_combo.addItem("Ошибки (error)", "error")
form_layout.addRow(self.log_level_label, self.log_level_combo)
self.qt_binding_label = QLabel("Qt binding:")
self.qt_binding_label.setToolTip(
"Библиотека, через которую работает графический интерфейс.\n"
"auto — PyQt6, если он установлен, иначе PyQt5 (рекомендуется).\n"
"PyQt6/PyQt5 — строго выбранный binding; если он не установлен,\n"
"GUI не запустится (вернуться можно переменной WH_QT_BINDING).\n"
"Изменение применяется после перезапуска WineHelper."
)
self.qt_binding_combo = QComboBox()
self.qt_binding_combo.setMinimumWidth(200)
self.qt_binding_combo.addItem("Авто (рекомендуется)", "auto")
self.qt_binding_combo.addItem("PyQt6", "pyqt6")
self.qt_binding_combo.addItem("PyQt5", "pyqt5")
installed = available_bindings()
combo_model = self.qt_binding_combo.model()
for index in range(self.qt_binding_combo.count()):
data = self.qt_binding_combo.itemData(index)
if data in ("pyqt5", "pyqt6") and data not in installed:
item = combo_model.item(index)
item.setEnabled(False)
self.qt_binding_combo.setItemText(index, f"{self.qt_binding_combo.itemText(index)} (не установлен)")
form_layout.addRow(self.qt_binding_label, self.qt_binding_combo)
layout.addLayout(form_layout)
layout.addStretch()
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.FocusPolicy.NoFocus)
button_layout.addWidget(self.save_button)
layout.addLayout(button_layout)
def _load_settings(self):
"""Загружает текущие настройки в интерфейс."""
current_level = self.settings_service.get_log_level()
index = self.log_level_combo.findData(current_level)
if index >= 0:
self.log_level_combo.setCurrentIndex(index)
current_binding = self.settings_service.get_qt_binding()
index = self.qt_binding_combo.findData(current_binding)
if index >= 0:
self.qt_binding_combo.setCurrentIndex(index)
def _save_qt_binding(self):
"""
Сохраняет выбранный Qt binding; при смене фактического binding'а
предлагает перезапустить приложение.
Returns:
bool: False если сохранение не удалось (сообщение уже показано).
"""
new_binding = self.qt_binding_combo.currentData()
if new_binding == self.settings_service.get_qt_binding():
return True
if not self.settings_service.set_qt_binding(new_binding):
QMessageBox.warning(self, "Ошибка", "Не удалось сохранить Qt binding.")
return False
# Какой binding фактически загрузится при следующем запуске
effective = new_binding
if effective == "auto":
effective = "pyqt6" if "pyqt6" in available_bindings() else "pyqt5"
if effective == CURRENT_BINDING:
return True
logger.info("Qt binding изменён: %s -> %s (требуется перезапуск)", CURRENT_BINDING, effective)
if DialogUtils.confirm(
self,
"Изменение Qt binding",
f"При следующем запуске будет использован {effective}.\n\nПерезапустить WineHelper сейчас?",
default_no=False,
):
self._restart()
else:
QMessageBox.information(
self, "Сохранено", "Новый Qt binding вступит в силу при следующем запуске WineHelper."
)
return True
def _restart(self):
"""Перезапускает GUI через скрипт-обёртку winehelper.
Новый процесс стартует с небольшой задержкой, чтобы текущий успел
завершиться и освободить сокет single-instance (QLocalServer).
"""
run_script = EnvironmentConfig.RUN_SCRIPT
if not run_script:
QMessageBox.warning(
self,
"Ошибка",
"Не удалось определить путь к скрипту winehelper.\nПерезапустите WineHelper вручную.",
)
return
QProcess.startDetached("sh", ["-c", f'sleep 1; exec "{run_script}" gui'])
app = QApplication.instance()
if app:
app.quit()
def _save_log_level(self):
"""Сохраняет и применяет уровень логирования, только если он изменился."""
new_level = self.log_level_combo.currentData()
if new_level == self.settings_service.get_log_level():
return
if self.settings_service.set_log_level(new_level):
setup_logger(new_level)
# Сообщение отправляем на новом уровне: info-сообщение было бы
# отфильтровано, если новый уровень warning/error.
log_func = {
"debug": logger.debug,
"info": logger.info,
"warning": logger.warning,
"error": logger.error,
}[new_level]
log_func("Уровень логирования изменён на '%s'", new_level)
QMessageBox.information(self, "Сохранено", f"Уровень логирования изменён на '{new_level}'.")
else:
QMessageBox.warning(self, "Ошибка", "Не удалось сохранить уровень логирования.")
def _save_settings(self):
"""Сохраняет настройки; Qt binding — последним, т.к. может инициировать перезапуск."""
self._save_log_level()
if not self._save_qt_binding():
return
+42
View File
@@ -0,0 +1,42 @@
"""
Диалог установки приложения с защитой от случайного закрытия во время процесса.
В отличие от CommandDialog (который при прерывании закрывается сразу),
окно установки остаётся открытым до фактического завершения процесса,
чтобы пользователь увидел финальное состояние лога.
"""
from winehelperqt.dialogs.command_dialog import CommandDialog
from winehelperqt.utils.dialog_utils import DialogUtils
class InstallDialog(CommandDialog):
"""Модальный диалог установки приложения с защитой от случайного закрытия."""
def __init__(self, parent=None):
super().__init__(parent)
self._abort_callback = None
def set_abort_callback(self, callback):
"""Задаёт callback, вызываемый при подтверждённом прерывании через закрытие окна."""
self._abort_callback = callback
def closeEvent(self, event):
if self._process is None or not self._process_is_running(self._process):
event.accept()
return
if not DialogUtils.confirm(
self,
"Прервать установку?",
"Установка еще не завершена.\nВы действительно хотите прервать процесс?",
yes_text="Да, прервать",
no_text="Нет",
):
event.ignore()
return
if self._abort_callback:
self._abort_callback()
# Запрещаем закрытие: окно закроется кнопкой «Закрыть» после завершения процесса
event.ignore()
+119
View File
@@ -0,0 +1,119 @@
"""
Модуль вывода предупреждения о noexec для /home.
Выводит информационное окно с инструкциями по устранению проблемы,
когда /home примонтирован с флагом noexec и запуск Wine невозможен.
"""
import getpass
import sys
from winehelperqt.qt_compat import (
QApplication,
QDialog,
QTextBrowser,
QVBoxLayout,
)
class NoExecWarningDialog(QDialog):
"""Диалог предупреждения о флаге noexec для /home."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("WineHelper — Предупреждение!")
self.setMinimumSize(600, 620)
self.setModal(True)
layout = QVBoxLayout(self)
layout.setSpacing(12)
self.info_browser = QTextBrowser()
self.info_browser.setOpenExternalLinks(True)
self.info_browser.setStyleSheet("""
QTextBrowser {
background-color: #1e1e1e;
color: #e4e4e4;
border: none;
}
""")
self.info_browser.setHtml(self._build_html_content())
layout.addWidget(self.info_browser)
def _build_html_content(self) -> str:
"""Формирует HTML-содержимое окна с подстановкой имени пользователя."""
username = getpass.getuser()
body_style = (
"font-family: 'Segoe UI', 'Noto Sans', sans-serif; font-size: 14px; color: #e4e4e4; line-height: 1.55;"
)
warning_style = "background-color: #3d351f; border-left: 4px solid #ffcc00; padding: 12px 14px; margin: 0;"
warning_text_style = "color: #ffd700; font-weight: bold; text-align: center; margin: 0 0 6px 0;"
card_style = "padding: 14px; margin: 0;"
title_style = "font-size: 15px; font-weight: bold; color: #ffffff; margin: 0 0 10px 0;"
paragraph_style = "margin: 0 0 10px 0;"
pre_style = (
"color: #7ee787; font-family: 'JetBrains Mono', 'Consolas', 'Courier New', monospace; "
"font-size: 13px; padding: 4px 0; margin: 0 0 12px 0;"
)
return f"""
<html>
<body style="{body_style}">
<p style="text-align: center; font-size: 20px; font-weight: bold; color: #ffd700; margin: 0 0 8px 0;">
Предупреждение!
</p>
<div style="{warning_style}">
<p style="{warning_text_style} margin-bottom: 0;">
⚠️ /home примонтирован в /etc/fstab с аргументом noexec.<br>
Запуск портативной версии wine невозможен из домашнего каталога!
Используйте один из двух вариантов решения проблемы.
</p>
</div>
<div style="line-height: 18px; font-size: 1px;">&nbsp;</div>
<div style="{card_style}">
<p style="{title_style}">1. Снять флаг noexec в /etc/fstab</p>
<pre style="{pre_style}">$ su -
# apt-get install nano
# nano /etc/fstab</pre>
<p style="{paragraph_style}">
Удаляем флаг <b>noexec</b> напротив <b>/home</b>.<br>
<b>Ctrl+O</b> — сохранить изменения,<br>
<b>Ctrl+X</b> — выход.<br>
Можно использовать любой редактор; nano приведён как пример.
</p>
<p style="{title_style} margin-top: 14px;">Обновляем загрузчик:</p>
<pre style="{pre_style}"># update-grub
# exit</pre>
<p style="{paragraph_style} margin-bottom: 0;">
После перезагрузки можно приступать к работе с WineHelper.
</p>
</div>
<div style="line-height: 18px; font-size: 1px;">&nbsp;</div>
<div style="{card_style}">
<p style="{title_style}">2. Или изменить рабочий каталог для WineHelper</p>
<p style="{paragraph_style}">
Каталог должен находиться <b>вне /home</b> и иметь права на чтение-запись
для пользователя <b>{username}</b>.
</p>
<p style="{paragraph_style}">Используйте команду:</p>
<pre style="{pre_style} margin-bottom: 0;">$ winehelper data-path /путь/к/каталогу</pre>
</div>
</body>
</html>
"""
def main() -> int:
"""Точка входа для автономного запуска модуля."""
app = QApplication(sys.argv)
dialog = NoExecWarningDialog()
dialog.show()
return app.exec()
if __name__ == "__main__":
sys.exit(main())
+34
View File
@@ -0,0 +1,34 @@
"""
Диалог управления логированием действий на вкладке «Менеджер префиксов».
"""
from winehelperqt.dialogs.winedebug_base import PREFIX_LOG_CHANNELS_KEY, WinedebugChannelsDialog
class PrefixLoggingDialog(WinedebugChannelsDialog):
"""Диалог включения/выключения логирования (каналы WINEDEBUG) для вкладки 'Менеджер префиксов'."""
def __init__(self, logging_enabled=False, parent=None):
self._logging_enabled = logging_enabled
state_text = "включено" if logging_enabled else "выключено"
super().__init__(
title="Логирование действий с префиксом",
description=(
f"Сейчас логирование <b>{state_text}</b>.<br><br>"
"При включённом логировании все действия на вкладке 'Менеджер префиксов' "
"(запуск файлов и утилит Wine) будут выполняться с указанными каналами WINEDEBUG, "
"а лог будет сохраняться в папке 'winehelper_backup_log' в вашем домашнем каталоге."
),
primary_button_text="Выключить" if logging_enabled else "Включить",
channels_key=PREFIX_LOG_CHANNELS_KEY,
channels_label="Укажите каналы отладки WINEDEBUG:",
parent=parent,
)
def _on_primary_clicked(self):
self._logging_enabled = not self._logging_enabled
self.accept()
@property
def logging_enabled(self) -> bool:
return self._logging_enabled
+142 -122
View File
@@ -1,22 +1,34 @@
"""
Диалоги для дополнительных настроек префикса и приложений.
"""
import os
import re
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton,
QLabel, QCheckBox, QComboBox, QMessageBox, QSizePolicy
import os
from winehelperqt.logger import get_logger
from winehelperqt.qt_compat import (
QCheckBox,
QComboBox,
QDialog,
QGridLayout,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSizePolicy,
Qt,
QVBoxLayout,
)
from PyQt5.QtCore import Qt
from winehelperqt.utils.export_utils import ExportUtils
logger = get_logger(__name__)
class AdditionalSettingsDialog(QDialog):
"""Диалог для дополнительных настроек префикса (ESync, FSync, DXVK, VKD3D)."""
"""Диалог для дополнительных настроек префикса (ESync, FSync, DXVK, VKD3D, Windows)."""
def __init__(self, prefix_name, parent=None):
super().__init__(parent)
self.parent_gui = parent
self.parent_tab = parent
self.prefix_name = prefix_name
self.setWindowTitle("Дополнительные настройки")
@@ -32,60 +44,56 @@ class AdditionalSettingsDialog(QDialog):
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.clicked.connect(lambda checked: self._toggle_sync_option("WINEESYNC", checked))
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; }")
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.clicked.connect(lambda checked: self._toggle_sync_option("WINEFSYNC", checked))
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; }")
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)
self.esync_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.fsync_button.setFocusPolicy(Qt.FocusPolicy.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.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.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.dxvk_manage_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.vkd3d_manage_button.setFocusPolicy(Qt.FocusPolicy.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.clicked.connect(self.parent_tab.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)
self.file_associations_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
# CPCSP_PROXY
self.cpcsp_proxy_button = QPushButton("CPCSP_PROXY")
@@ -93,28 +101,70 @@ class AdditionalSettingsDialog(QDialog):
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)
lambda checked: self._toggle_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; }")
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)
self.cpcsp_proxy_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
# Версия Windows
self.windows_version_button = QPushButton("Версия Windows")
self.windows_version_button.setMinimumHeight(32)
self.windows_version_button.clicked.connect(self.parent_tab.open_windows_version_manager)
self.windows_version_button.setToolTip("Изменить версию Windows, которую Wine сообщает приложениям в префиксе.")
grid_layout.addWidget(self.windows_version_button, 3, 0)
self.windows_version_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
# Ключ защиты Guardant (grdwine)
self.grdwine_button = QPushButton("Ключ Guardant")
self.grdwine_button.setMinimumHeight(32)
self.grdwine_button.setCheckable(True)
self.grdwine_button.setToolTip(
"Подкидывать в Wine библиотеки grdwine из системного пакета wine-grdwine (ключ защиты Guardant)."
)
self.grdwine_button.clicked.connect(lambda checked: self._toggle_sync_option("WH_USE_GRDWINE", checked))
self.grdwine_button.toggled.connect(
lambda checked, btn=self.grdwine_button: self._update_button_text(btn, "Ключ Guardant", checked)
)
self.grdwine_button.setStyleSheet(
"QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }"
)
grid_layout.addWidget(self.grdwine_button, 3, 1)
self.grdwine_button.setFocusPolicy(Qt.FocusPolicy.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.close_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
layout.addWidget(self.close_button, alignment=Qt.AlignmentFlag.AlignRight)
self.update_button_states()
def _toggle_sync_option(self, option_name, is_checked, button):
def _toggle_sync_option(self, option_name, is_checked):
"""Обновляет опцию синхронизации."""
self.parent_gui.update_sync_option(option_name, is_checked)
self.parent_tab.update_sync_option(option_name, is_checked)
def _toggle_cpcsp_proxy(self, is_checked, button):
"""Включает/выключает CPCSP_PROXY через winehelper cpcsp-proxy enable/disable."""
if is_checked:
def on_install_finished(success):
if not success:
button.blockSignals(True)
button.setChecked(False)
self._update_button_text(button, "CPCSP_PROXY", False)
button.blockSignals(False)
self.parent_tab.run_cpcsp_proxy_install(self.prefix_name, on_install_finished)
else:
self.parent_tab.run_cpcsp_proxy_disable(self.prefix_name)
def _update_button_text(self, button, base_text, is_checked):
"""Обновляет текст кнопки, добавляя или убирая галочку."""
@@ -125,35 +175,36 @@ class AdditionalSettingsDialog(QDialog):
def _open_component_manager(self, component_name):
"""Открывает менеджер компонентов."""
self.parent_gui.open_component_version_manager(component_name)
self.parent_tab.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"
esync_enabled = self.parent_tab.get_prefix_component_version(self.prefix_name, "WINEESYNC") == "1"
fsync_enabled = self.parent_tab.get_prefix_component_version(self.prefix_name, "WINEFSYNC") == "1"
cpcsp_proxy_enabled = (
self.parent_tab.get_prefix_component_version(self.prefix_name, "WH_USE_CPCSP_PROXY") == "1"
)
grdwine_enabled = self.parent_tab.get_prefix_component_version(self.prefix_name, "WH_USE_GRDWINE") == "1"
self.esync_button.blockSignals(True)
self.fsync_button.blockSignals(True)
self.cpcsp_proxy_button.blockSignals(True)
self.grdwine_button.blockSignals(True)
self.esync_button.setChecked(esync_enabled)
self.fsync_button.setChecked(fsync_enabled)
self.cpcsp_proxy_button.setChecked(cpcsp_proxy_enabled)
self.grdwine_button.setChecked(grdwine_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._update_button_text(self.grdwine_button, "Ключ Guardant", grdwine_enabled)
self.esync_button.blockSignals(False)
self.fsync_button.blockSignals(False)
self.cpcsp_proxy_button.blockSignals(False)
self.grdwine_button.blockSignals(False)
class AppAdditionalSettingsDialog(QDialog):
@@ -161,16 +212,16 @@ class AppAdditionalSettingsDialog(QDialog):
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.parent_tab = parent
self.exe_path = os.path.abspath(exe_path)
self.whdb_path = self.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': ''
"WH_MAIN_DECORATED": "default",
"WH_VIRTUAL_DESKTOP": "default",
"WH_FONT_SMOOTHING": "0",
"WH_FONT_MSS_REPLACE": "0",
"WINE_D3D_CONFIG": "",
}
self.setWindowTitle("Дополнительные настройки приложения")
@@ -215,9 +266,7 @@ class AppAdditionalSettingsDialog(QDialog):
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.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.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")
@@ -235,13 +284,13 @@ class AppAdditionalSettingsDialog(QDialog):
self.save_button = QPushButton("Сохранить")
self.save_button.setMinimumHeight(32)
self.save_button.clicked.connect(self._save_settings)
self.save_button.setFocusPolicy(Qt.NoFocus)
self.save_button.setFocusPolicy(Qt.FocusPolicy.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)
self.close_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
@@ -255,48 +304,32 @@ class AppAdditionalSettingsDialog(QDialog):
return
try:
with open(self.whdb_path, 'r', encoding='utf-8') as f:
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
for line in content.split("\n"):
var_name, var_value = self._parse_export_line(line)
if var_name and 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}"
)
logger.warning("Не удалось прочитать файл настроек %s: %s", self.whdb_path, 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_main_decorated_checkbox.setChecked(self.settings.get("WH_MAIN_DECORATED") == "1")
wh_virtual_desktop = self.settings.get("WH_VIRTUAL_DESKTOP", "")
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'
wh_virtual_desktop == "1" or 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', '')
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)
@@ -308,49 +341,36 @@ class AppAdditionalSettingsDialog(QDialog):
try:
existing_lines = []
if os.path.exists(self.whdb_path):
with open(self.whdb_path, 'r', encoding='utf-8') as f:
with open(self.whdb_path, "r", encoding="utf-8") as f:
existing_lines = f.readlines()
updated_vars = set()
new_lines = []
settings_values = {var_name: self._get_setting_value(var_name) for var_name in self.settings}
new_lines = ExportUtils.update_lines(existing_lines, settings_values)
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:
with open(self.whdb_path, "w", encoding="utf-8") as f:
f.writelines(new_lines)
self.accept()
except Exception as e:
logger.error("Не удалось сохранить файл настроек %s: %s", self.whdb_path, 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 = ''
def _get_setting_value(self, var_name):
"""Возвращает текущее значение переменной из UI."""
if var_name == "WINE_D3D_CONFIG":
return self.wine_d3d_config_combo.currentData()
if var_name == "WH_MAIN_DECORATED":
return "1" if self.wh_main_decorated_checkbox.isChecked() else "default"
if var_name == "WH_VIRTUAL_DESKTOP":
return "1" if self.wh_virtual_desktop_checkbox.isChecked() else "default"
if var_name == "WH_FONT_SMOOTHING":
return "1" if self.wh_font_smoothing_checkbox.isChecked() else "0"
if var_name == "WH_FONT_MSS_REPLACE":
return "1" if self.wh_font_mss_replace_checkbox.isChecked() else "0"
return ""
return f'export {var_name}="{value}"\n'
@staticmethod
def _parse_export_line(line):
"""Парсит строку export VAR=value, игнорируя комментарии и секции."""
return ExportUtils.parse_line(line)
+76
View File
@@ -0,0 +1,76 @@
"""
Диалог для выбора версии Windows в префиксе Wine.
"""
from winehelperqt.qt_compat import QComboBox, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
class WindowsVersionSelectionDialog(QDialog):
"""Диалог выбора версии Windows, сообщаемой приложениям в префиксе."""
WINDOWS_VERSIONS = [
("Windows 2000", "2k"),
("Windows XP", "xp"),
("Windows XP 64-bit", "xp64"),
("Windows Server 2003", "2k3"),
("Windows Vista", "vista"),
("Windows 7", "7"),
("Windows 8", "8"),
("Windows 8.1", "81"),
("Windows 10", "10"),
("Windows 11", "11"),
]
def __init__(self, current_version="10", parent=None):
"""
Инициализирует диалог выбора версии Windows.
Args:
current_version: Текущая версия Windows из last.conf (по умолчанию "10")
parent: Родительский виджет
"""
super().__init__(parent)
self.setWindowTitle("Выбор версии Windows")
self.setMinimumWidth(380)
self.setModal(True)
self.selected_version = None
layout = QVBoxLayout(self)
layout.setSpacing(10)
info_label = QLabel("Выберите версию Windows, которую Wine будет сообщать приложениям в префиксе.")
info_label.setWordWrap(True)
layout.addWidget(info_label)
self.version_combo = QComboBox()
self.version_combo.setMinimumHeight(32)
for display_name, value in self.WINDOWS_VERSIONS:
self.version_combo.addItem(display_name, value)
index = self.version_combo.findData(current_version)
if index >= 0:
self.version_combo.setCurrentIndex(index)
layout.addWidget(self.version_combo)
button_layout = QHBoxLayout()
button_layout.addStretch()
self.ok_button = QPushButton("Применить")
self.ok_button.setMinimumHeight(32)
self.ok_button.clicked.connect(self._accept)
self.ok_button.setDefault(True)
button_layout.addWidget(self.ok_button)
self.cancel_button = QPushButton("Отмена")
self.cancel_button.setMinimumHeight(32)
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
def _accept(self):
"""Сохраняет выбранную версию и закрывает диалог."""
self.selected_version = self.version_combo.currentData()
self.accept()
+171 -58
View File
@@ -1,15 +1,106 @@
"""
Диалог для выбора версии 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
from winehelperqt.qt_compat import (
QGridLayout,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
Qt,
QTabWidget,
QVBoxLayout,
QWidget,
)
from winehelperqt.utils.grid_utils import GridUtils
from winehelperqt.utils.version_utils import sort_versions
class VersionButtonWidget(QPushButton):
"""Кастомная кнопка версии с текстом слева и галочкой справа."""
def __init__(self, display_name, value_name, is_installed=False, show_icon=True, parent=None):
super().__init__(parent)
self.value_name = value_name
self.display_name = display_name
self.is_installed = is_installed
self.show_icon = show_icon
self.setMinimumHeight(40)
self.setCursor(Qt.CursorShape.PointingHandCursor)
# Создаем внутренний layout для размещения текста и галочки
self._setup_layout()
self._apply_style()
def _setup_layout(self):
"""Настраивает внутренний layout кнопки с текстом слева и галочкой справа."""
# Создаем контейнер для содержимого кнопки
container = QWidget(self)
layout = QHBoxLayout(container)
layout.setContentsMargins(12, 0, 12, 0)
layout.setSpacing(0)
# Если show_icon=False (вкладка "Установленные версии Wine"), текст по центру
if not self.show_icon:
layout.addStretch(1)
# Метка с названием версии (по центру)
self.name_label = QLabel(self.display_name)
self.name_label.setStyleSheet("background: transparent;")
self.name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.name_label)
layout.addStretch(1)
else:
# Метка с названием версии (слева)
self.name_label = QLabel(self.display_name)
self.name_label.setStyleSheet("background: transparent;")
layout.addWidget(self.name_label)
# Растягивающийся spacer (занимает всё доступное пространство между текстом и галочкой)
layout.addStretch(1)
# Метка со значком статуса (справа)
if self.is_installed:
self.check_label = QLabel("✓")
self.check_label.setStyleSheet(
"background: transparent; color: #7fff7f; font-weight: bold; font-size: 22px;"
)
layout.addWidget(self.check_label)
else:
self.check_label = QLabel("⭳")
self.check_label.setStyleSheet("background: transparent; color: #888888; font-size: 22px;")
layout.addWidget(self.check_label)
# Устанавливаем layout для контейнера
container.setLayout(layout)
# Устанавливаем layout кнопки (Qt поддерживает это)
button_layout = QHBoxLayout(self)
button_layout.setContentsMargins(0, 0, 0, 0)
button_layout.setSpacing(0)
button_layout.addWidget(container)
self.setLayout(button_layout)
def _apply_style(self):
"""Применяет стили в зависимости от статуса установки."""
# Не применяем никаких стилей — используем стандартные цвета системы
if self.is_installed:
self.setToolTip("Эта версия уже скачана и установлена. Нажмите, чтобы выбрать её.")
def text(self):
"""Возвращает текст кнопки для фильтрации."""
return self.display_name
def setText(self, text):
"""Устанавливает текст метки названия версии."""
self.display_name = text
if hasattr(self, "name_label"):
self.name_label.setText(text)
class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
@@ -27,9 +118,13 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
self.installed_grid_layout = None
self.installed_buttons = []
self.download_buttons = []
self._installed_version_names = set()
super().__init__(architecture=architecture, parent=parent)
# UI строим в конце: к этому моменту установлены поля всех уровней
self._setup_ui()
def _create_version_container(self):
"""Создает контейнер с вкладками для версий Wine."""
self.main_tabs = QTabWidget()
@@ -39,6 +134,7 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
def populate_ui(self):
"""Заполняет UI версиями Wine."""
self.main_tabs.clear()
self._installed_version_names = set()
# Вкладка "Установленные версии Wine"
installed_tab = QWidget()
@@ -46,31 +142,43 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
installed_layout = QVBoxLayout(installed_tab)
installed_scroll_area = QScrollArea()
installed_scroll_area.setWidgetResizable(True)
installed_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
installed_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.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)
self.installed_grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
installed_versions_for_grid = []
# Системная версия
self.system_wine_display_name = self._get_system_wine_version()
# Системная версия (может отсутствовать, если Wine не установлен в системе)
system_wine_version = self._get_system_wine_version()
self.system_wine_available = system_wine_version is not None
self.system_wine_display_name = system_wine_version or "Системная версия"
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)
# Запоминаем имена установленных версий для пометки на вкладках загрузки
for version_data in installed_versions_for_grid:
if isinstance(version_data, tuple):
_, value_name = version_data
else:
value_name = version_data
self._installed_version_names.add(value_name)
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_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
error_layout.addWidget(error_label)
self.main_tabs.addTab(error_widget, "Ошибка")
self.filter_versions()
@@ -82,30 +190,21 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
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"
}
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"
}
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))
available_keys = [key for key in self.wine_versions_data if key in allowed_groups_for_arch]
group_keys = sorted(available_keys, key=tab_order.index)
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._create_version_tab_directly(display_name, sort_versions(versions, reverse=True))
self.filter_versions()
@@ -117,14 +216,14 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
tab_layout.addWidget(scroll_area)
scroll_content = QWidget()
scroll_area.setWidget(scroll_content)
grid_layout = QGridLayout(scroll_content)
grid_layout.setAlignment(Qt.AlignTop)
grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self._create_grid_buttons_for_wine(grid_layout, versions_list, is_download_tab=True)
self.main_tabs.addTab(tab_page, title)
@@ -133,7 +232,7 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
"""Создает кнопки для версий в сетке."""
is_win64_prefix = self.architecture == "win64"
is_win32_prefix = self.architecture == "win32"
re_32bit_version = re.compile(r'i[3-6]86|i586')
re_32bit_version = re.compile(r"i[3-6]86|i586")
row, col = 0, 0
for version_data in versions_list:
@@ -142,12 +241,30 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
else:
display_name = value_name = version_data
btn = QPushButton(display_name)
# Определяем, установлена ли версия
# На вкладке установленных все версии установлены по определению
if is_download_tab:
is_installed = value_name in self._installed_version_names
else:
is_installed = True
# Создаем кастомную кнопку
# На вкладке "Установленные версии Wine" значки не показываем
show_icon = is_download_tab
btn = VersionButtonWidget(display_name, value_name, is_installed=is_installed, show_icon=show_icon)
btn.clicked.connect(partial(self.on_version_selected, value_name, display_name))
# Подсказка для неустановленных версий на вкладках загрузки
if is_download_tab and not is_installed:
btn.setToolTip("Эта версия доступна для загрузки. Нажмите, чтобы скачать и выбрать её.")
if value_name == "system":
btn.setText(f"{display_name} (Системный)")
btn.setToolTip("Системная версия Wine, установленная в вашей ОС.")
if self.system_wine_available:
btn.setToolTip("Системная версия Wine, установленная в вашей ОС.")
else:
btn.setEnabled(False)
btn.setToolTip("Wine не установлен в системе.")
# Отключение 32-битных версий для 64-битного префикса
if not is_download_tab and is_win64_prefix:
@@ -156,8 +273,8 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
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:
if not is_download_tab and is_win32_prefix and btn.isEnabled():
if value_name == "system" or "wow64" in value_name:
btn.setEnabled(False)
btn.setToolTip("Эта 64-битная версия Wine несовместима с 32-битным префиксом.")
@@ -174,43 +291,39 @@ class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
def filter_versions(self):
"""Фильтрует видимость кнопок версий на основе текста поиска."""
search_text = self.search_edit.text().lower()
search_text = self.search_edit.text()
# Фильтр для вкладки "Установленные"
if hasattr(self, 'installed_grid_layout'):
self._filter_grid(self.installed_grid_layout, search_text)
if self.installed_grid_layout:
GridUtils.filter_buttons(self.installed_buttons, search_text, self.installed_grid_layout, self.NUM_COLUMNS)
# Фильтр для вкладок с версиями (теперь напрямую в 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)
grid_layout, buttons = self._get_grid_layout_and_buttons(tab_widget)
if grid_layout:
visible = GridUtils.filter_buttons(buttons, search_text, grid_layout, self.NUM_COLUMNS)
self.main_tabs.setTabEnabled(i, bool(visible))
def _filter_grid(self, grid_layout, search_text):
"""Helper-функция для фильтрации кнопок в одной сетке."""
any_visible_in_grid = False
if not grid_layout:
return False
def _get_grid_layout_and_buttons(self, tab_widget):
"""Возвращает QGridLayout и список кнопок из вкладки."""
scroll_area = tab_widget.findChild(QScrollArea)
if not scroll_area:
return None, []
scroll_content = scroll_area.widget()
if not scroll_content:
return None, []
grid_layout = scroll_content.layout()
if not isinstance(grid_layout, QGridLayout):
return None, []
buttons = []
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
if item and item.widget():
buttons.append(item.widget())
return grid_layout, buttons
def on_version_selected(self, version_name, display_name=None):
"""Обрабатывает выбор версии Wine."""
+118
View File
@@ -0,0 +1,118 @@
"""
Общий модуль для диалогов с вводом каналов WINEDEBUG и их настроек.
Единственный источник хранения (QSettings) и констант, которым пользуются
CreateLogDialog, PrefixLoggingDialog и вкладка «Менеджер префиксов».
"""
from winehelperqt.qt_compat import (
QCheckBox,
QDialog,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QSettings,
QVBoxLayout,
)
# Хранение настроек логирования WINEDEBUG (один источник)
WINEDEBUG_SETTINGS_ORG = "winehelper"
WINEDEBUG_SETTINGS_APP = "winedebug_run"
# Значения по умолчанию и имена ключей.
# Ключи каналов раздельные: два режима пишут в разные лог-файлы,
# поэтому «Запомнить каналы» в каждом режиме работает независимо
DEFAULT_WINEDEBUG = "+loaddll,+seh"
CREATE_LOG_CHANNELS_KEY = "winedebug_channels_run"
PREFIX_LOG_CHANNELS_KEY = "prefix_tab_winedebug_channels"
PREFIX_LOGGING_ENABLED_KEY = "prefix_tab_logging_enabled"
def winedebug_settings() -> QSettings:
"""QSettings для каналов WINEDEBUG (общее хранилище диалогов и вкладки префиксов)."""
return QSettings(WINEDEBUG_SETTINGS_ORG, WINEDEBUG_SETTINGS_APP)
class WinedebugChannelsDialog(QDialog):
"""
Базовый диалог с полем ввода каналов WINEDEBUG и чекбоксом «Запомнить каналы».
Подклассы предоставляют только заголовок, текст описания, текст главной
кнопки и ключ каналов (а при необходимости переопределяют _on_primary_clicked).
"""
def __init__(self, title, description, primary_button_text, channels_key, channels_label=None, parent=None):
super().__init__(parent)
self.setWindowTitle(title)
self.setMinimumSize(520, 240)
self.setModal(True)
self._settings = winedebug_settings()
self._channels_key = channels_key
layout = QVBoxLayout(self)
desc_label = QLabel(description)
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
if channels_label:
layout.addSpacing(8)
channels_label_widget = QLabel(channels_label)
layout.addWidget(channels_label_widget)
winedebug_layout = QHBoxLayout()
winedebug_layout.addWidget(QLabel("<b>WINEDEBUG:</b>"))
self.winedebug_edit = QLineEdit()
self.winedebug_edit.setPlaceholderText("Например: +loaddll,+seh")
winedebug_layout.addWidget(self.winedebug_edit)
layout.addLayout(winedebug_layout)
hint_label = QLabel("Пример: +loaddll,-seh,fixme-all,+tid")
hint_label.setStyleSheet("color: gray; font-size: 13px;")
layout.addWidget(hint_label)
self.remember_cb = QCheckBox("Запомнить каналы")
layout.addWidget(self.remember_cb)
self._restore_state()
layout.addStretch()
button_layout = QHBoxLayout()
primary_button = QPushButton(primary_button_text)
primary_button.clicked.connect(self._on_primary_clicked)
cancel_button = QPushButton("Отмена")
cancel_button.clicked.connect(self.reject)
button_layout.addStretch()
button_layout.addWidget(primary_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
def _on_primary_clicked(self):
"""Действие главной кнопки (подклассы могут переопределить)."""
self.accept()
def _restore_state(self):
saved = self._settings.value(self._channels_key, "")
if saved:
self.winedebug_edit.setText(saved)
self.remember_cb.setChecked(True)
else:
self.winedebug_edit.setText(DEFAULT_WINEDEBUG)
self.remember_cb.setChecked(False)
def accept(self):
if self.remember_cb.isChecked():
value = self.winedebug_edit.text().strip()
if value:
self._settings.setValue(self._channels_key, value)
else:
self._settings.remove(self._channels_key)
super().accept()
@property
def winedebug_value(self) -> str:
value = self.winedebug_edit.text().strip()
return value or DEFAULT_WINEDEBUG
+205 -176
View File
@@ -1,23 +1,41 @@
"""
Диалог для управления компонентами 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.logger import get_logger
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
from winehelperqt.services.winetricks_service import WinetricksLogReader
from winehelperqt.qt_compat import (
QDialog,
QHBoxLayout,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QProcess,
QPushButton,
Qt,
QTabWidget,
QTextCharFormat,
QTextCursor,
QTextEdit,
QTimer,
QVBoxLayout,
QWidget,
pyqtSignal,
)
from winehelperqt.services.winetricks_service import WinetricksCacheService, WinetricksLogReader
from winehelperqt.utils.dialog_utils import DialogUtils
from winehelperqt.utils.environment import EnvironmentConfig
from winehelperqt.utils.font_utils import fixed_font
from winehelperqt.utils.process_utils import ProcessUtils
logger = get_logger(__name__)
# Отдельный логгер для вывода внешних процессов (bash, winetricks),
# чтобы в консоли отличать его от событий самого GUI
process_logger = get_logger("process")
class NoAutoScrollListWidget(QListWidget):
@@ -27,7 +45,7 @@ class NoAutoScrollListWidget(QListWidget):
super().__init__(*args, **kwargs)
self._scroll_enabled = False # Флаг для ручной прокрутки
def scrollTo(self, index, hint=QListWidget.EnsureVisible):
def scrollTo(self, index, hint=QListWidget.ScrollHint.EnsureVisible):
"""Переопределяем, чтобы игнорировать автоматическую прокрутку."""
if self._scroll_enabled:
super().scrollTo(index, hint)
@@ -57,8 +75,8 @@ class WinetricksManagerDialog(QDialog):
super().__init__(parent)
self.prefix_path = prefix_path
self.winetricks_path = winetricks_path
self.wine_executable = wine_executable or 'wine'
self.winehelper_path = winehelper_path or 'winehelper'
self.wine_executable = wine_executable or "wine"
self.winehelper_path = winehelper_path or "winehelper"
self.initial_states = {}
self.apply_process = None
self.installation_finished = False
@@ -66,10 +84,10 @@ class WinetricksManagerDialog(QDialog):
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.cache_service = WinetricksCacheService()
self.is_reloading_after_cache_clear = False
self.cache_clear_process = None
self._install_queue = []
self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
self.setMinimumSize(800, 500)
@@ -79,11 +97,7 @@ class WinetricksManagerDialog(QDialog):
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
self.categories = {
"Библиотеки": "dlls",
"Шрифты": "fonts",
"Настройки": "settings"
}
self.categories = {"Библиотеки": "dlls", "Шрифты": "fonts", "Настройки": "settings"}
self.list_widgets = {}
self.search_edits = {}
for display_name, internal_name in self.categories.items():
@@ -93,7 +107,7 @@ class WinetricksManagerDialog(QDialog):
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setFont(QFont('DejaVu Sans Mono', 10))
self.log_output.setFont(fixed_font(10))
self.log_output.setMaximumHeight(150)
self.log_output.setText(self.INFO_TEXT)
main_layout.addWidget(self.log_output)
@@ -130,9 +144,12 @@ class WinetricksManagerDialog(QDialog):
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()
previous_index = self.tabs.indexOf(self.previous_tab_widget)
if previous_index >= 0:
category = list(self.categories.values())[previous_index]
search_edit = self.search_edits.get(category)
if search_edit:
search_edit.clear()
if self.installation_finished:
self.log_output.setText(self.INFO_TEXT)
@@ -150,7 +167,8 @@ class WinetricksManagerDialog(QDialog):
layout.addWidget(search_edit)
list_widget = NoAutoScrollListWidget()
list_widget.setAutoScroll(False) # Отключаем автопрокрутку
list_widget.setAutoScroll(False)
list_widget.setTextElideMode(Qt.TextElideMode.ElideNone)
list_widget.itemChanged.connect(self._on_item_changed)
list_widget.currentItemChanged.connect(self._update_ui_state)
layout.addWidget(list_widget)
@@ -176,7 +194,7 @@ class WinetricksManagerDialog(QDialog):
for i in range(list_widget.count()):
item = list_widget.item(i)
if not item.isHidden():
list_widget.scrollToItem(item, QListWidget.PositionAtTop)
list_widget.scrollToItem(item, QListWidget.ScrollHint.PositionAtTop)
break
list_widget.blockSignals(False)
@@ -190,49 +208,27 @@ class WinetricksManagerDialog(QDialog):
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}' поврежден, будет выполнена перезагрузка. ---")
cached_output = self.cache_service.get_cached_data(category, self.winetricks_path)
if cached_output is not None:
QTimer.singleShot(
0, lambda: self._on_load_finished(category, 0, QProcess.ExitStatus.NormalExit, from_cache=cached_output)
)
return
process = QProcess(self)
self.processes[category] = process
process.setProcessChannelMode(QProcess.MergedChannels)
ProcessUtils.prepare_process(process, {"WINEPREFIX": self.prefix_path})
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
process.setProcessEnvironment(env)
process.finished.connect(partial(self._on_load_finished, category))
process.finished.connect(
lambda exit_code, exit_status: self._on_load_finished(category, exit_code, exit_status, process=process)
)
logger.debug(
"Запуск загрузки списка winetricks: %s %s",
shlex.quote(self.winehelper_path),
["winetricks", category, "list"],
)
process.start(self.winehelper_path, ["winetricks", category, "list"])
def _parse_winetricks_log(self):
@@ -247,62 +243,64 @@ class WinetricksManagerDialog(QDialog):
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)
item.setData(Qt.ItemDataRole.UserRole, item_data.name)
item.setFont(fixed_font(10))
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(Qt.CheckState.Checked if item_data.is_installed else Qt.CheckState.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):
def _on_load_finished(self, category, exit_code, exit_status, from_cache=None, process=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')
if process is None:
process = self.processes.get(category)
if process is None:
return
output = process.readAllStandardOutput().data().decode("utf-8", "ignore")
# Убираем завершённый процесс из словаря и удаляем его,
# чтобы QProcess не копился в диалоге до его закрытия
if self.processes.get(category) is process:
self.processes.pop(category, None)
process.deleteLater()
list_widget = self.list_widgets[category]
category_display_name = next(k for k, v in self.categories.items() if v == category)
list_widget = self.list_widgets.get(category)
if list_widget is None:
return
category_display_name = next((k for k, v in self.categories.items() if v == category), category)
list_widget.clear()
if exit_code != 0 or exit_status != QProcess.NormalExit:
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
self.category_statuses[category] = "error"
error_string = process.errorString() if process else "N/A"
self._log(f"--- Ошибка загрузки категории '{category_display_name}' (код: {exit_code}) ---", "red")
if exit_status == QProcess.CrashExit:
if exit_status == QProcess.ExitStatus.CrashExit:
self._log("--- Процесс winetricks завершился аварийно. ---", "red")
if error_string != "Неизвестная ошибка":
self._log(f"--- Системная ошибка: {error_string} ---", "red")
self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.")
self._log("--------------------------------------------------", "red")
else:
self.category_statuses[category] = "ok"
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 self.cache_service.save_to_cache(category, output, self.winetricks_path):
self._log(f"--- Не удалось сохранить кэш для '{category}' ---")
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())
has_errors = any(status == "error" for status in self.category_statuses.values())
self.loading_count -= 1
if self.loading_count == 0:
@@ -316,13 +314,13 @@ class WinetricksManagerDialog(QDialog):
def _on_item_changed(self, item):
"""Обрабатывает изменение состояния чекбокса."""
name = item.data(Qt.UserRole)
name = item.data(Qt.ItemDataRole.UserRole)
if name in self.initial_states and self.initial_states.get(name) is True:
if item.checkState() == Qt.Unchecked:
if item.checkState() == Qt.CheckState.Unchecked:
list_widget = item.listWidget()
if list_widget:
list_widget.blockSignals(True)
item.setCheckState(Qt.Checked)
item.setCheckState(Qt.CheckState.Checked)
if list_widget:
list_widget.blockSignals(False)
self._update_ui_state()
@@ -333,10 +331,10 @@ class WinetricksManagerDialog(QDialog):
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)
name = item.data(Qt.ItemDataRole.UserRole)
if name in self.initial_states:
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
current_state = item.checkState() == Qt.CheckState.Checked
if current_state != initial_state:
has_changes = True
break
@@ -347,19 +345,26 @@ class WinetricksManagerDialog(QDialog):
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
current_index = self.tabs.currentIndex()
if current_index >= 0:
category = list(self.categories.values())[current_index]
current_list_widget = self.list_widgets.get(category)
if current_list_widget:
current_item = current_list_widget.currentItem()
if current_item:
name = current_item.data(Qt.ItemDataRole.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)
current_index = self.tabs.currentIndex()
if current_index < 0:
return
category = list(self.categories.values())[current_index]
current_list_widget = self.list_widgets.get(category)
if not current_list_widget:
return
@@ -367,7 +372,7 @@ class WinetricksManagerDialog(QDialog):
if not current_item:
return
name = current_item.data(Qt.UserRole)
name = current_item.data(Qt.ItemDataRole.UserRole)
if not name:
return
@@ -387,12 +392,12 @@ class WinetricksManagerDialog(QDialog):
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)
name = item.data(Qt.ItemDataRole.UserRole)
if name not in self.initial_states:
continue
initial_state = self.initial_states[name]
current_state = item.checkState() == Qt.Checked
current_state = item.checkState() == Qt.CheckState.Checked
if current_state != initial_state:
if initial_state:
@@ -423,16 +428,27 @@ class WinetricksManagerDialog(QDialog):
# Устанавливает новые компоненты (без --force)
if verbs_to_install:
self._log(f"Выполнение установки: winehelper winetricks {' '.join(verbs_to_install)}")
self._run_winetricks_process(verbs_to_install, use_force=False)
self._install_queue.append((verbs_to_install, False))
# Переустанавливает компоненты (с --force)
if verbs_to_reinstall:
if verbs_to_install:
# Если уже что-то устанавливается, добавляем небольшую задержку
self._log("\n---\n")
self._log(f"Выполнение переустановки: winehelper winetricks --force {' '.join(verbs_to_reinstall)}")
self._run_winetricks_process(verbs_to_reinstall, use_force=True)
self._install_queue.append((verbs_to_reinstall, True))
# Запускаем первую задачу, если сейчас ничего не выполняется
if not self.apply_process or self.apply_process.state() != QProcess.ProcessState.Running:
self._process_next_install_task()
def _process_next_install_task(self):
"""Берёт следующую задачу из очереди и запускает процесс."""
if not self._install_queue:
return
verbs, use_force = self._install_queue.pop(0)
if use_force:
self._log(f"Выполнение переустановки: winehelper winetricks --force {' '.join(verbs)}")
else:
self._log(f"Выполнение установки: winehelper winetricks {' '.join(verbs)}")
self._run_winetricks_process(verbs, use_force)
def _run_winetricks_process(self, verbs, use_force=False):
"""Запускает процесс winetricks с указанными компонентами.
@@ -446,42 +462,50 @@ class WinetricksManagerDialog(QDialog):
args.append("--force")
args.extend(verbs)
if not self.apply_process:
self.apply_process = QProcess(self)
self.apply_process.setProcessChannelMode(QProcess.MergedChannels)
# Всегда создаём новый QProcess, чтобы избежать повторного вызова start()
# и дублирования обработчиков сигналов.
if self.apply_process:
try:
self.apply_process.readyReadStandardOutput.disconnect()
self.apply_process.finished.disconnect(self.on_apply_finished)
except TypeError as e:
logger.debug("Отсутствуют подключенные обработчики процесса winetricks: %s", e)
# Удаляем завершённый процесс, чтобы он не копился
# в диалоге по числу выполненных операций
self.apply_process.deleteLater()
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
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 = QProcess(self)
ProcessUtils.prepare_process(self.apply_process, {"WINEPREFIX": self.prefix_path})
self.apply_process.readyReadStandardOutput.connect(
lambda: self._log(self.apply_process.readAllStandardOutput().data().decode("utf-8", "ignore").strip())
)
self.apply_process.finished.connect(self.on_apply_finished)
self.apply_process.start(self.winehelper_path, ["winetricks"] + args)
def on_apply_finished(self, exit_code, exit_status):
"""Обрабатывает завершение применения изменений."""
if self.user_cancelled:
self._install_queue.clear()
self._log("\n=== Установка прервана пользователем. ===")
self.close()
return
if exit_code != 0 or exit_status != QProcess.NormalExit:
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
self._install_queue.clear()
self._log(f"\n=== Ошибка во время выполнения операций (код: {exit_code}). ===", "red")
self._show_message_box(
"Ошибка",
"Произошла ошибка во время выполнения операций.\nПодробности смотрите в логе.",
QMessageBox.Warning,
{"buttons": {"OK": QMessageBox.AcceptRole}}
QMessageBox.warning(
self, "Ошибка", "Произошла ошибка во время выполнения операций.\nПодробности смотрите в логе."
)
self.log_output.setCurrentCharFormat(QTextCharFormat())
self.apply_button.setEnabled(True)
self.close_button.setEnabled(True)
return
if self._install_queue:
self._process_next_install_task()
return
self._log("\n=== Все операции успешно завершены ===")
self.apply_button.setEnabled(True)
self.reinstall_button.setEnabled(False)
@@ -497,16 +521,14 @@ class WinetricksManagerDialog(QDialog):
def clear_winetricks_cache(self):
"""Запускает очистку кэша Winetricks."""
reply = self._show_message_box(
if not DialogUtils.confirm(
self,
"Очистка кэша Winetricks",
"Вы собираетесь удалить все скачанные установщики и списки компонентов Winetricks.\n\n"
"Это действие может потребоваться, если у вас возникают проблемы со скачиванием или установкой компонентов.\n\n"
"Это действие может потребоваться, если у вас возникают проблемы "
"со скачиванием или установкой компонентов.\n\n"
"Продолжить?",
QMessageBox.Question,
{"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}
)
if reply != "Да":
):
return
self.tabs.setEnabled(False)
@@ -518,10 +540,10 @@ class WinetricksManagerDialog(QDialog):
self.log_output.clear()
self.cache_clear_process = QProcess(self)
self.cache_clear_process.setProcessChannelMode(QProcess.MergedChannels)
ProcessUtils.prepare_process(self.cache_clear_process)
def handle_output():
output = self.cache_clear_process.readAll().data().decode('utf-8', 'ignore').strip()
output = self.cache_clear_process.readAll().data().decode("utf-8", "ignore").strip()
if output:
self._log(output)
@@ -529,7 +551,7 @@ class WinetricksManagerDialog(QDialog):
if exit_code == 0:
self.is_reloading_after_cache_clear = True
self.category_statuses.clear()
os.makedirs(self.cache_dir, exist_ok=True)
self.cache_service.ensure_cache_dir()
self._log("Обновление списков...")
self.initial_states.clear()
self.load_all_categories()
@@ -540,11 +562,15 @@ class WinetricksManagerDialog(QDialog):
self.clear_cache_button.setEnabled(True)
self.close_button.setEnabled(True)
self._update_ui_state()
self.cache_clear_process.deleteLater()
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
if self.parent() and hasattr(self.parent(), "winehelper_path"):
winehelper_path = self.parent().winehelper_path
else:
winehelper_path = EnvironmentConfig.RUN_SCRIPT
args = ["clear-winetricks-cache", "--force"]
self._log(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(args)}\n")
@@ -552,48 +578,51 @@ class WinetricksManagerDialog(QDialog):
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 == "Да":
if self.apply_process and self.apply_process.state() == QProcess.ProcessState.Running:
if DialogUtils.confirm(
self, "Подтверждение", "Процесс установки еще не завершен. Вы уверены, что хотите прервать его?"
):
self.user_cancelled = True
self.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===")
self._log("=== Прерывание установки... Ожидание завершения процесса... ===")
self.apply_process.terminate()
event.ignore()
else:
event.ignore()
event.ignore()
else:
# Корректно завершаем фоновые процессы (загрузка категорий, очистка кэша)
for process in list(self.processes.values()):
if process.state() == QProcess.ProcessState.Running:
process.terminate()
# cache_clear_process после deleteLater может стать удалённым
# C++-объектом, поэтому защищаемся проверкой живости
cache_clear_process = self.cache_clear_process
if (
cache_clear_process is not None
and not DialogUtils.is_widget_deleted(cache_clear_process)
and cache_clear_process.state() == QProcess.ProcessState.Running
):
cache_clear_process.terminate()
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):
"""Добавляет сообщение в лог с возможностью указания цвета."""
"""
Добавляет сообщение в лог с возможностью указания цвета.
Args:
message: Текст сообщения
color: Цвет текста (например, "red")
"""
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)
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
if message.strip():
# Зеркалируем в консоль через логгер процесса: консоль — единое
# зеркало содержимого диалога. Ведущие/хвостовые переносы
# (отступ в виджете) не логируем, иначе префикс строки уйдёт
# на отдельную строку
log_text = message.strip("\n")
if color == "red":
process_logger.error("%s", log_text)
else:
process_logger.info("%s", log_text)
+211
View File
@@ -0,0 +1,211 @@
"""
Единый логгер приложения.
По умолчанию пишет в stdout — виден при запуске GUI из терминала
(``./winehelper gui``). При запуске из меню терминала нет, вывод
просто уходит в никуда, ничего не ломая.
Уровень задаётся переменной окружения ``WH_LOG_LEVEL``
(debug/info/warning/error, по умолчанию info).
Использование в модулях::
from winehelperqt.logger import get_logger
logger = get_logger(__name__)
"""
import logging
import os
import re
import shutil
import sys
import time
DEFAULT_LEVEL = "INFO"
# ANSI-коды цветов для уровней логирования
_LEVEL_COLORS = {
"DEBUG": "\033[36m", # голубой
"INFO": "\033[32m", # зелёный
"WARNING": "\033[33m", # жёлтый
"ERROR": "\033[31m", # красный
"CRITICAL": "\033[1;31m", # жирный красный
}
_COLOR_RESET = "\033[0m"
class ColoredFormatter(logging.Formatter):
"""Форматтер с цветной подсветкой уровня логирования."""
def format(self, record: logging.LogRecord) -> str:
color = _LEVEL_COLORS.get(record.levelname, "")
if color:
record.levelname = f"{color}{record.levelname}{_COLOR_RESET}"
return super().format(record)
# Строка прогресса в интерактивном терминале выводится как обычная
# строка лога (с префиксом [время] [уровень] [process]) с анимацией:
# обновления перекрывают текущую строку (\r). Чтобы строка с префиксом
# гарантированно помещалась в ширину терминала, бар укорачивается.
# _bar_pending — в stdout ещё не записан перевод строки после строки прогресса;
# _bar_prefix — префикс текущей строки прогресса (для перекрытия всей строки)
_bar_pending = False
_bar_prefix = ""
# ANSI-последовательности (не занимают видимых колонок)
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
def _progress_prefix() -> str:
"""Префикс строки прогресса — идентичен префиксу обычной строки лога."""
ts = time.strftime("%H:%M:%S")
if sys.stdout.isatty():
level = f"{_LEVEL_COLORS['INFO']}INFO{_COLOR_RESET}"
else:
level = "INFO"
return f"[{ts}] [{level}] [process] "
def _terminal_columns() -> int:
"""Ширина терминала в колонках (с запасом при недоступности)."""
try:
return shutil.get_terminal_size(fallback=(80, 24)).columns
except (OSError, ValueError):
return 80
def _fit_to_terminal(text: str, prefix: str) -> str:
"""
Укорачивает текст так, чтобы префикс + текст помещались в ширину
терминала (обрезка слева: проценты находятся в конце и всегда видны).
"""
visual_prefix = len(_ANSI_RE.sub("", prefix))
available = _terminal_columns() - visual_prefix - 1
if available < 4:
available = 4
if len(text) > available:
return text[-available:]
return text
def console_progress_write(text: str) -> None:
"""
Пишет строку прогресса в консоль.
В интерактивный терминал (tty) — как строка лога с префиксом,
перекрытием текущей строки (анимация); бар укорачивается, чтобы
строка не переносилась. В перенаправленный вывод (не tty) —
строкой лога, чтобы в файле была полная история.
"""
global _bar_pending, _bar_prefix
if sys.stdout.isatty():
if not _bar_pending:
_bar_prefix = _progress_prefix()
sys.stdout.write(_bar_prefix + _fit_to_terminal(text, _bar_prefix) + "\x1b[K")
else:
sys.stdout.write("\r" + _bar_prefix + _fit_to_terminal(text, _bar_prefix) + "\x1b[K")
sys.stdout.flush()
_bar_pending = True
else:
get_logger("process").info("%s", text)
def console_progress_line(text: str) -> None:
"""
Пишет завершённую строку прогресса (например, итоговое состояние бара).
В tty — как строка лога с префиксом: если строка прогресса открыта,
перекрытием той же строки (весь прогресс остаётся на одной строке).
В перенаправленный вывод — строкой лога.
"""
global _bar_pending, _bar_prefix
if sys.stdout.isatty():
if _bar_pending:
sys.stdout.write("\r" + _bar_prefix + _fit_to_terminal(text, _bar_prefix) + "\n")
else:
prefix = _progress_prefix()
sys.stdout.write(prefix + _fit_to_terminal(text, prefix) + "\n")
sys.stdout.flush()
_bar_pending = False
_bar_prefix = ""
else:
get_logger("process").info("%s", text)
def console_progress_end() -> None:
"""Завершает строку прогресса переводом строки в tty."""
global _bar_pending, _bar_prefix
if _bar_pending:
sys.stdout.write("\n")
sys.stdout.flush()
_bar_pending = False
_bar_prefix = ""
class _BarAwareStreamHandler(logging.StreamHandler):
"""StreamHandler, завершающий незавершённую строку прогресса перед записью."""
def emit(self, record: logging.LogRecord) -> None:
global _bar_pending, _bar_prefix
if _bar_pending:
try:
self.stream.write("\n")
self.stream.flush()
except (ValueError, OSError):
pass
_bar_pending = False
_bar_prefix = ""
super().emit(record)
def setup_logger(level: str | None = None) -> None:
"""
Настраивает корневой логгер приложения.
Args:
level: Уровень логирования (debug/info/warning/error).
Если не задан, берётся из WH_LOG_LEVEL, иначе info.
"""
if level is None:
level = os.environ.get("WH_LOG_LEVEL", DEFAULT_LEVEL)
log_level = getattr(logging, level.upper(), logging.INFO)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler = _BarAwareStreamHandler(sys.stdout)
log_format = "[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s"
if sys.stdout.isatty():
formatter: logging.Formatter = ColoredFormatter(log_format, datefmt="%H:%M:%S")
else:
formatter = logging.Formatter(log_format, datefmt="%H:%M:%S")
handler.setFormatter(formatter)
root_logger.setLevel(log_level)
root_logger.addHandler(handler)
def get_logger(name: str) -> logging.Logger:
"""Возвращает логгер для модуля."""
return logging.getLogger(name)
def log_info_always(logger: logging.Logger, msg: str, *args) -> None:
"""
Пишет INFO-сообщение независимо от настроенного уровня логирования.
Для пользовательски важных строк (стартовая строка с binding'ом,
подтверждение смены уровня логирования): они не должны ни теряться
на уровнях warning/error, ни маскироваться под DEBUG на уровне debug.
Уровень корневого логгера временно опускается до INFO и сразу
возвращается обратно.
"""
root_logger = logging.getLogger()
configured_level = root_logger.level
root_logger.setLevel(logging.INFO)
logger.info(msg, *args)
root_logger.setLevel(configured_level)
-10
View File
@@ -1,13 +1,3 @@
"""
Парсеры для 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',
]
+100 -78
View File
@@ -1,99 +1,121 @@
"""
Парсер для извлечения информации из скриптов установки.
"""
import shlex
from winehelperqt.logger import get_logger
from winehelperqt.utils.export_utils import ExportUtils
logger = get_logger(__name__)
class ScriptParser:
"""Утилитарный класс для парсинга информации из скриптов установки."""
@staticmethod
def extract_icons_from_script(script_path):
def parse_script(script_path):
"""
Извлекает иконку для скрипта.
Извлекает всю информацию из скрипта установки за один проход по файлу.
Сначала ищет переменную 'export ICONS_BUTTON_GUI=', если не находит,
то ищет 'export PROG_ICON=', затем ищет вызовы 'create_desktop'.
Извлекает: PROG_NAME, PROG_URL, описание (# info_ru:) и иконки
(приоритет: ICONS_BUTTON_GUI > PROG_ICON > create_desktop).
Args:
script_path: Путь к скрипту установки
Returns:
Список имен иконок
Словарь с ключами prog_name, url, description, icons.
При ошибке чтения — значения по умолчанию (как у старых
отдельных методов: None / "Ошибка чтения файла: ..." / []).
"""
logger.debug("Парсим скрипт установки: %s", script_path)
try:
with open(script_path, 'r', encoding='utf-8') as f:
with open(script_path, "r", encoding="utf-8") as f:
lines = f.readlines()
except Exception as e:
logger.warning("Не удалось прочитать скрипт %s: %s", script_path, e)
return {
"prog_name": None,
"url": None,
"description": f"Ошибка чтения файла: {str(e)}",
"icons": [],
}
# Приоритет у 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()
# Переменные: берём первое вхождение каждой (в т.ч. пустое — как раньше
# в _extract_variable), описание — первую строку "# info_ru:"
prog_name = None
prog_url = None
description = None
prog_name_done = False
prog_url_done = False
description_done = False
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()
for line in lines:
if prog_name_done and prog_url_done and description_done:
break
parsed_name, parsed_value = ExportUtils.parse_line(line)
if not prog_name_done and parsed_name == "PROG_NAME":
prog_name = parsed_value if parsed_value else None
prog_name_done = True
if not prog_url_done and parsed_name == "PROG_URL":
prog_url = parsed_value if parsed_value else None
prog_url_done = True
if not description_done and line.startswith("# info_ru:"):
description = line.replace("# info_ru:", "").strip()
description_done = True
# Поиск в create_desktop
icon_names = []
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
if description is None:
description = "Описание отсутствует"
return {
"prog_name": prog_name,
"url": prog_url,
"description": description,
"icons": ScriptParser._extract_icons_from_lines(lines, script_path),
}
@staticmethod
def _extract_icons_from_lines(lines, script_path):
"""
Ищет иконки в уже прочитанных строках скрипта.
Приоритет: ICONS_BUTTON_GUI > PROG_ICON > имена иконок из вызовов create_desktop.
"""
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:
icons = icon_names_str.split()
logger.debug("Найдены иконки %s в %s", icons, script_path)
return icons
for line in lines:
if line.strip().startswith("export PROG_ICON="):
icon_names_str = line.split("=", 1)[1].strip().strip("\"'")
if icon_names_str:
icons = icon_names_str.split()
logger.debug("Найдены иконки %s в %s", icons, script_path)
return icons
# Поиск в 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) as e:
logger.debug("Пропущена строка create_desktop в %s: %s", script_path, e)
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)}"
logger.debug("Иконки из create_desktop в %s: %s", script_path, icon_names)
return icon_names
+46 -45
View File
@@ -2,17 +2,25 @@
Парсер для файла sha256sum.list.
Извлекает версии Wine и компонентов из файла хешей.
"""
import os
import re
from winehelperqt.logger import get_logger
from winehelperqt.utils.version_utils import sort_versions
logger = get_logger(__name__)
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'
GROUP_HEADER_PATTERN = re.compile(r"^#+\s*(.*?)\s*#*$")
HASH_PATTERN = re.compile(r"^[a-f0-9]{64}")
PREFIX_SECTION_START = re.compile(r"^#####\s*PREFIX\s*#####$")
SECTION_END_PATTERN = re.compile(r"^#####")
ARCHIVE_EXTENSION = ".tar.xz"
@classmethod
def parse_wine_versions(cls, sha256_path):
@@ -34,7 +42,7 @@ class Sha256Parser:
current_group = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
with open(sha256_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
@@ -56,12 +64,14 @@ class Sha256Parser:
if len(parts) == 2:
filename = parts[1]
if filename.endswith(cls.ARCHIVE_EXTENSION):
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
version_name = filename[: -len(cls.ARCHIVE_EXTENSION)]
versions_data[current_group].append(version_name)
except IOError:
except IOError as e:
logger.warning("Не удалось прочитать файл версий %s: %s", sha256_path, e)
return {}
logger.debug("Версии Wine из %s: %s", sha256_path, versions_data)
return versions_data
@classmethod
@@ -84,7 +94,7 @@ class Sha256Parser:
current_group = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
with open(sha256_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
@@ -100,12 +110,14 @@ class Sha256Parser:
if len(parts) == 2:
filename = parts[1]
if filename.endswith(cls.ARCHIVE_EXTENSION):
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
version_name = filename[: -len(cls.ARCHIVE_EXTENSION)]
versions_data.append(version_name)
except IOError:
except IOError as e:
logger.warning("Не удалось прочитать файл версий %s: %s", sha256_path, e)
return []
logger.debug("Версии компонента %s из %s: %s", component_group, sha256_path, versions_data)
return versions_data
@classmethod
@@ -120,7 +132,7 @@ class Sha256Parser:
Словарь {архитектура: [(имя_префикса, описание)]}, например:
{'win32': [('prefix_x86_v1', 'Описание')], 'win64': [...]}
"""
prepared_prefixes = {'win32': [], 'win64': []}
prepared_prefixes = {"win32": [], "win64": []}
if not os.path.exists(sha256_path):
return prepared_prefixes
@@ -130,20 +142,16 @@ class Sha256Parser:
current_prefix_name = None
try:
with open(sha256_path, 'r', encoding='utf-8') as f:
with open(sha256_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if "##### PREFIX #####" in line:
if cls.PREFIX_SECTION_START.match(line):
in_prefix_section = True
continue
elif line.startswith("#####"):
if cls.SECTION_END_PATTERN.match(line):
if current_prefix_name and current_description:
cls._add_prefix_to_list(
prepared_prefixes,
current_prefix_name,
current_description
)
cls._add_prefix_to_list(prepared_prefixes, current_prefix_name, current_description)
in_prefix_section = False
current_description = ""
current_prefix_name = None
@@ -154,21 +162,17 @@ class Sha256Parser:
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
)
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', '')
current_prefix_name = parts[1].replace(".tar.xz", "")
else:
current_prefix_name = None
elif line.startswith('# '):
elif line.startswith("# "):
comment_line = line[2:].strip()
if current_description:
current_description += "\n" + comment_line
@@ -176,17 +180,15 @@ class Sha256Parser:
current_description = comment_line
if current_prefix_name and current_description:
cls._add_prefix_to_list(
prepared_prefixes,
current_prefix_name,
current_description
)
cls._add_prefix_to_list(prepared_prefixes, current_prefix_name, current_description)
except IOError:
return {'win32': [], 'win64': []}
except IOError as e:
logger.warning("Не удалось прочитать префиксы из %s: %s", sha256_path, e)
return {"win32": [], "win64": []}
cls._add_clean_prefix_option(prepared_prefixes)
logger.debug("Подготовленные префиксы из %s: %s", sha256_path, prepared_prefixes)
return prepared_prefixes
@classmethod
@@ -194,21 +196,19 @@ class Sha256Parser:
"""Добавляет префикс в список с определением архитектуры."""
arch = None
if "_x86_" in prefix_name or "_i586_" in prefix_name:
arch = 'win32'
arch = "win32"
elif "_x64_" in prefix_name or "_amd64_" in prefix_name:
arch = 'win64'
arch = "win64"
if arch:
prepared_prefixes[arch].append(
(prefix_name, description.strip().replace('\\n', '\n'))
)
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))
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):
@@ -225,9 +225,10 @@ class Sha256Parser:
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:
versions = [name for name in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, name))]
sorted_versions = sort_versions(versions, reverse=True)
logger.debug("Установленные версии в %s: %s", dist_path, sorted_versions)
return sorted_versions
except OSError as e:
logger.warning("Не удалось прочитать директорию dist %s: %s", dist_path, e)
return []
+27 -49
View File
@@ -2,9 +2,14 @@
Парсер для вывода утилиты Winetricks.
Извлекает списки компонентов и информацию об установленных пакетах.
"""
import os
import re
from winehelperqt.logger import get_logger
logger = get_logger(__name__)
class WinetricksItem:
"""Элемент списка компонентов Winetricks."""
@@ -21,23 +26,16 @@ class WinetricksOutputParser:
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
)
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
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*\]')
DOWNLOAD_AVAILABLE_PATTERN = re.compile(r"\[\s*доступно для скачивания[^]]*\]")
IN_CACHE_PATTERN = re.compile(r"\[\s*в кэше\s*\]")
SERVICE_WORDS = {'executing', 'using', 'warning:', '#', 'export'}
SERVICE_WORDS = {"executing", "using", "warning:", "#", "export"}
@classmethod
def parse_installed_verbs(cls, log_path):
@@ -56,13 +54,15 @@ class WinetricksOutputParser:
return installed_verbs
try:
with open(log_path, 'r', encoding='utf-8') as f:
with open(log_path, "r", encoding="utf-8") as f:
for line in f:
verb = line.split('#', 1)[0].strip()
# Учитываем экранированный #: разделяем только по неэкранированному #
parts = re.split(r"(?<!\\)#", line)
verb = parts[0].replace("\\#", "#").strip()
if verb:
installed_verbs.add(verb)
except Exception as e:
print(f"Предупреждение: не удалось прочитать {log_path}: {e}")
logger.warning("Не удалось прочитать %s: %s", log_path, e)
return installed_verbs
@@ -96,14 +96,11 @@ class WinetricksOutputParser:
description = cls._clean_description(description)
is_checked = name in installed_verbs
item_text = f"{name.ljust(27)}{description.strip()}"
item_text = f"{name.ljust(24)}{description.strip()}"
items.append(WinetricksItem(
name=name,
description=description.strip(),
is_installed=is_checked,
item_text=item_text
))
items.append(
WinetricksItem(name=name, description=description.strip(), is_installed=is_checked, item_text=item_text)
)
return items, found_items
@@ -111,9 +108,9 @@ class WinetricksOutputParser:
def _get_blacklist_pattern(cls, category):
"""Возвращает шаблон blacklist для указанной категории."""
patterns = {
'dlls': cls.DLLS_BLACKLIST_PATTERN,
'fonts': cls.FONTS_BLACKLIST_PATTERN,
'settings': cls.SETTINGS_BLACKLIST_PATTERN
"dlls": cls.DLLS_BLACKLIST_PATTERN,
"fonts": cls.FONTS_BLACKLIST_PATTERN,
"settings": cls.SETTINGS_BLACKLIST_PATTERN,
}
return patterns.get(category)
@@ -129,10 +126,10 @@ class WinetricksOutputParser:
Returns:
True если компонент допустим, False иначе
"""
if '/' in name or '\\' in name:
if "/" in name or "\\" in name:
return False
if name.lower() in cls.SERVICE_WORDS or name.endswith(':'):
if name.lower() in cls.SERVICE_WORDS or name.endswith(":"):
return False
blacklist_pattern = cls._get_blacklist_pattern(category)
@@ -152,25 +149,6 @@ class WinetricksOutputParser:
Returns:
Очищенное описание
"""
description = cls.DOWNLOAD_AVAILABLE_PATTERN.sub('', description)
description = cls.IN_CACHE_PATTERN.sub('', description)
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)
+284
View File
@@ -0,0 +1,284 @@
"""Слой совместимости PyQt6/PyQt5.
Весь GUI-код проекта импортирует Qt только отсюда:
from winehelperqt.qt_compat import Qt, QWidget, QT6
Выбор binding'а (приоритет сверху вниз):
1. Переменная окружения WH_QT_BINDING=pyqt5|pyqt6 — для тестирования.
2. Настройка «Qt binding» во вкладке «Настройки» GUI
(ключ qt_binding в ~/.config/winehelper/logging.conf): auto|pyqt6|pyqt5.
3. auto — PyQt6, если он установлен и рабочий (p11, Sisyphus),
иначе PyQt5 (p10, где PyQt6 отсутствует в репозитории).
В strict-режимах (pyqt5/pyqt6) fallback отключён: если выбранный binding
недоступен, при импорте этого модуля поднимается ImportError с понятным
сообщением (его перехватывает winehelper_gui.py и завершается с ошибкой).
Внешние прослойки (QtPy) не используются: в p10 лежит слишком старая версия.
Код проекта пишется в общем для обоих binding'ов стиле: enum'ы только
в scoped-форме (Qt.AlignmentFlag.AlignCenter), exec() вместо exec_(),
сравнения enum с int — через Qt.CheckState(state) == ...
"""
import configparser
import importlib.util
import os
from pathlib import Path
VALID_BINDINGS = ("auto", "pyqt6", "pyqt5")
# Тот же файл, что использует GlobalSettingsService; читаем напрямую,
# т.к. binding выбирается при импорте, до инициализации сервисов GUI.
_CONFIG_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "winehelper" / "logging.conf"
def _read_binding_choice() -> str:
"""Читает выбранный binding: WH_QT_BINDING > конфиг GUI > auto."""
forced = os.environ.get("WH_QT_BINDING", "").lower().strip()
if forced in ("pyqt5", "pyqt6"):
return forced
try:
config = configparser.ConfigParser()
config.read(_CONFIG_PATH, encoding="utf-8")
saved = config["gui"].get("qt_binding", "auto").lower().strip()
if saved in VALID_BINDINGS:
return saved
except (configparser.Error, OSError, KeyError):
pass
return "auto"
BINDING = _read_binding_choice()
QT6 = False
_loaded = False
_import_errors = []
if BINDING in ("auto", "pyqt6"):
try:
from PyQt6 import sip
from PyQt6.QtCore import (
QEasingCurve,
QObject,
QProcess,
QProcessEnvironment,
QPropertyAnimation,
QSettings,
QSize,
Qt,
QTimer,
pyqtSignal,
)
from PyQt6.QtGui import (
QColor,
QCursor,
QFont,
QFontDatabase,
QIcon,
QPainter,
QPalette,
QPixmap,
QTextCharFormat,
QTextCursor,
)
from PyQt6.QtNetwork import QLocalServer, QLocalSocket
from PyQt6.QtWidgets import (
QAbstractButton,
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMainWindow,
QMenu,
QMessageBox,
QProgressBar,
QPushButton,
QRadioButton,
QScrollArea,
QSizePolicy,
QStackedWidget,
QSystemTrayIcon,
QTabBar,
QTabWidget,
QTextBrowser,
QTextEdit,
QToolButton,
QVBoxLayout,
QWidget,
)
QT6 = True
_loaded = True
except ImportError as e:
_import_errors.append(f"PyQt6: {e}")
if not _loaded and BINDING in ("auto", "pyqt5"):
try:
from PyQt5 import sip
from PyQt5.QtCore import (
QEasingCurve,
QObject,
QProcess,
QProcessEnvironment,
QPropertyAnimation,
QSettings,
QSize,
Qt,
QTimer,
pyqtSignal,
)
from PyQt5.QtGui import (
QColor,
QCursor,
QFont,
QFontDatabase,
QIcon,
QPainter,
QPalette,
QPixmap,
QTextCharFormat,
QTextCursor,
)
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from PyQt5.QtWidgets import (
QAbstractButton,
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMainWindow,
QMenu,
QMessageBox,
QProgressBar,
QPushButton,
QRadioButton,
QScrollArea,
QSizePolicy,
QStackedWidget,
QSystemTrayIcon,
QTabBar,
QTabWidget,
QTextBrowser,
QTextEdit,
QToolButton,
QVBoxLayout,
QWidget,
)
_loaded = True
except ImportError as e:
_import_errors.append(f"PyQt5: {e}")
if not _loaded:
details = "; ".join(str(e) for e in _import_errors)
if BINDING == "auto":
raise ImportError(
"Не найден ни PyQt6, ни PyQt5. Установите python3-module-PyQt6 "
f"(p11, Sisyphus) или python3-module-PyQt5 (p10). Детали: {details}"
)
raise ImportError(f"В настройках WineHelper выбран {BINDING}, но он недоступен. Детали: {details}")
CURRENT_BINDING = "pyqt6" if QT6 else "pyqt5"
def available_bindings() -> list:
"""Возвращает список установленных binding'ов ('pyqt5', 'pyqt6') без их загрузки."""
result = []
if importlib.util.find_spec("PyQt5") is not None:
result.append("pyqt5")
if importlib.util.find_spec("PyQt6") is not None and importlib.util.find_spec("PyQt6.sip") is not None:
result.append("pyqt6")
return result
__all__ = [
"BINDING",
"CURRENT_BINDING",
"QAbstractButton",
"QApplication",
"QCheckBox",
"QColor",
"QComboBox",
"QCursor",
"QDialog",
"QDialogButtonBox",
"QEasingCurve",
"QFileDialog",
"QFont",
"QFontDatabase",
"QFormLayout",
"QFrame",
"QGridLayout",
"QGroupBox",
"QHBoxLayout",
"QIcon",
"QInputDialog",
"QLabel",
"QLineEdit",
"QListWidget",
"QListWidgetItem",
"QLocalServer",
"QLocalSocket",
"QMainWindow",
"QMenu",
"QMessageBox",
"QObject",
"QPainter",
"QPalette",
"QPixmap",
"QProcess",
"QProcessEnvironment",
"QProgressBar",
"QPropertyAnimation",
"QPushButton",
"QRadioButton",
"QScrollArea",
"QSettings",
"QSize",
"QSizePolicy",
"QStackedWidget",
"QSystemTrayIcon",
"QT6",
"QTabBar",
"QTabWidget",
"QTextBrowser",
"QTextCharFormat",
"QTextCursor",
"QTextEdit",
"QTimer",
"QToolButton",
"QVBoxLayout",
"QWidget",
"Qt",
"VALID_BINDINGS",
"available_bindings",
"pyqtSignal",
"sip",
]
-24
View File
@@ -1,27 +1,3 @@
"""
Сервисы для 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',
]
+113 -195
View File
@@ -5,17 +5,20 @@
- Запуск приложений из .desktop файлов
- Остановка приложений
- Мониторинг запущенных приложений
- Создание логов запуска (в обычном и debug режиме)
- Запуск в debug-режиме (WINEDEBUG)
"""
import os
import shlex
from functools import partial
from typing import Dict, Optional
from datetime import datetime
from PyQt5.QtCore import QProcess, QProcessEnvironment
from winehelperqt.dialogs.winedebug_base import DEFAULT_WINEDEBUG
from winehelperqt.logger import get_logger
from winehelperqt.qt_compat import QProcess, QWidget
from winehelperqt.utils.process_utils import ProcessUtils
logger = get_logger(__name__)
class AppLauncher:
"""Сервис для запуска и управления приложениями Wine."""
@@ -32,117 +35,127 @@ class AppLauncher:
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:
def launch_app_by_command(
self,
desktop_path: str,
command: list,
env_vars: Optional[Dict[str, str]] = None,
debug: bool = False,
parent: Optional[QWidget] = None,
) -> QProcess:
"""
Запускает приложение по команде (для сложных случаев с env).
Args:
desktop_path: Путь к .desktop файлу (для отслеживания)
command: Команда запуска
command: Список аргументов команды запуска
env_vars: Дополнительные переменные окружения
debug: Режим отладки
parent: Родительский виджет для QProcess, чтобы deleteLater
гарантированно удалял C++-объект
Returns:
QProcess запущенного приложения
QProcess запущенного приложения. Ошибка старта процесса ОС
(не найден исполняемый файл и т.п.) исключение не поднимает:
она приходит асинхронно через сигналы errorOccurred/finished
процесса (см. _on_start_error).
"""
if desktop_path in self._active_processes:
raise RuntimeError(f"Приложение уже запущено: {desktop_path}")
process = QProcess()
process.setProcessChannelMode(QProcess.MergedChannels)
if not command:
raise ValueError("Команда запуска не может быть пустой")
# Настраиваем окружение
env = QProcessEnvironment.systemEnvironment()
if env_vars:
for key, value in env_vars.items():
env.insert(key, value)
return self._start_process(
desktop_path,
command,
env_vars=env_vars,
debug=debug,
connect_finished=False,
parent=parent,
)
def _start_process(
self,
desktop_path: str,
command: list,
env_vars: Optional[Dict[str, str]] = None,
debug: bool = False,
connect_finished: bool = True,
parent: Optional[QWidget] = None,
) -> QProcess:
"""
Запускает QProcess для приложения и регистрирует его.
Args:
desktop_path: Путь к .desktop файлу (для отслеживания)
command: Список аргументов команды запуска
env_vars: Дополнительные переменные окружения
debug: Режим отладки
connect_finished: Подключать ли обработчик завершения
parent: Родительский виджет для QProcess, чтобы deleteLater
гарантированно удалял C++-объект
Returns:
QProcess запущенного приложения
"""
process = QProcess(parent)
env_vars = dict(env_vars or {})
if debug:
env.insert("WINEDEBUG", "+loaddll,+seh")
process.setProcessEnvironment(env)
env_vars["WINEDEBUG"] = DEFAULT_WINEDEBUG
ProcessUtils.prepare_process(process, env_vars)
# Парсим команду
args = shlex.split(command)
executable = args[0]
process.start(executable, args[1:])
if not process.waitForStarted():
raise RuntimeError(f"Не удалось запустить приложение: {desktop_path}")
executable = command[0]
logger.debug(
"Запуск процесса: %s %s (env=%s)",
shlex.quote(executable),
" ".join(shlex.quote(a) for a in command[1:]),
env_vars,
)
process.start(executable, command[1:])
# Регистрируем процесс сразу: вызывающий код получит его через
# get_process_for_app после возврата, а неудачный старт отработает
# _on_start_error — асинхронно, без блокировки GUI
self._active_processes[desktop_path] = process
# НЕ подключаем обработчик — пусть вызывает сам
# Ошибку старта ждём через errorOccurred, не блокируя GUI waitForStarted:
# Qt гарантированно шлёт finished(-1, CrashExit) сразу после
# errorOccurred(FailedToStart), поэтому UI вернётся в исходное
# состояние обработчиками finished, подключёнными вызывающим кодом
process.errorOccurred.connect(partial(self._on_start_error, desktop_path, process))
if connect_finished:
process.finished.connect(
lambda exit_code, exit_status: self._on_app_finished(desktop_path, process, exit_code)
)
return process
def _on_start_error(self, desktop_path: str, process: QProcess, error) -> None:
"""
Обрабатывает неудачный старт процесса приложения.
Процесс не должен остаться в реестре запущенных: иначе кнопка
«Остановить» и завершение сессии не смогут его завершить (зомби).
"""
if error != QProcess.ProcessError.FailedToStart:
return
logger.error("Не удалось запустить приложение %s", desktop_path)
if self._active_processes.get(desktop_path) is process:
self._active_processes.pop(desktop_path, None)
try:
# Старт мог формально провалиться после форка — добиваем процесс
if process.state() != QProcess.ProcessState.NotRunning:
process.kill()
except RuntimeError:
pass # объект QProcess уже удалён
try:
process.deleteLater()
except RuntimeError:
pass
def mark_app_finished(self, desktop_path: str) -> Optional[QProcess]:
"""
Отмечает приложение как завершённое и возвращает процесс.
@@ -155,17 +168,6 @@ class AppLauncher:
"""
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:
"""
Проверяет, запущено ли приложение.
@@ -177,7 +179,7 @@ class AppLauncher:
True если приложение запущено
"""
process = self._active_processes.get(desktop_path)
return process and process.state() == QProcess.Running
return process is not None and process.state() == QProcess.ProcessState.Running
def get_running_apps(self) -> Dict[str, QProcess]:
"""
@@ -187,8 +189,7 @@ class AppLauncher:
Словарь {desktop_path: QProcess}
"""
return {
path: proc for path, proc in self._active_processes.items()
if proc.state() == QProcess.Running
path: proc for path, proc in self._active_processes.items() if proc.state() == QProcess.ProcessState.Running
}
def get_process_for_app(self, desktop_path: str) -> Optional[QProcess]:
@@ -203,91 +204,8 @@ class AppLauncher:
"""
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:
def _on_app_finished(self, desktop_path: str, process: QProcess, exit_code: int = 0) -> 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
if exit_code != 0:
logger.warning("Приложение %s завершилось с кодом %d", desktop_path, exit_code)
@@ -1,132 +0,0 @@
"""
Сервис для установки приложений 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()
]
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more