22 Commits
v0.2 ... main

Author SHA1 Message Date
Andrew
4285914512 symlinking ae to program files because some programs break without it 2025-11-09 13:57:26 +03:00
Andrew
aed41431f2 bugfix 2025-11-09 13:38:03 +03:00
Andrew
e05e9e576b file and protocol handling made more safe 2025-11-09 13:11:19 +03:00
Andrew
d0fb24a741 added misterhorse URL handler 2025-11-09 12:50:48 +03:00
Andrew
700a669bc5 for-the-badge 2025-11-08 08:06:20 +03:00
Andrew
e6307377ef community 2025-11-08 08:05:33 +03:00
Andrew
1fd0252870 link fix 2025-11-08 08:04:38 +03:00
Andrew
6d6dd386f5 readme update 2025-11-08 08:02:36 +03:00
Andrew
49830680b0 roadmap update 2025-11-08 03:51:13 +03:00
Andrew
83c077a54d .aep file argument support added 2025-11-08 03:50:34 +03:00
Andrew
969ecd7884 I forgot to update the roadmap 2025-11-07 07:40:30 +03:00
Andrew
77a3bc3727 wine build updated 2025-11-05 06:04:10 +03:00
Andrew
f9aa1b3878 private plugin installation 2025-11-05 04:55:38 +03:00
Andrew
3a835bdb63 refactoring 2025-11-02 04:02:20 +03:00
Andrew
cf8d516b8a Yeah, bundling the entire terminal emulator for flatpak builds lol 2025-11-02 03:53:41 +03:00
Andrew
0430495564 uk-ua update 2025-11-02 03:44:35 +03:00
Andrew
3ad6138077 Merge pull request #5 from progzone122/main
localization: add ukrainian translation
2025-11-02 03:43:26 +03:00
Andrew
b93d02faf3 get default terminal (thanks diablosat) 2025-11-02 03:42:42 +03:00
Andrew
b30982f9e8 finally, dxvk fix 2025-11-02 03:34:49 +03:00
diablo
a4447fa369 add ukrainian translation 2025-11-02 01:20:32 +02:00
Andrew
8cdd09da51 fontsmooth fix 2025-11-02 00:14:32 +03:00
Andrew
36b095210d more polishing 2025-11-02 00:09:08 +03:00
18 changed files with 674 additions and 99 deletions

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ bin/*
assets/wine
assets/vcr.zip
assets/msxml3.zip
assets/gdiplus.dll
assets/gdiplus.dll
assets/dxvk.tar.gz

View File

@@ -6,6 +6,17 @@ A convenient way to install Adobe After Effects on Linux using Wine. Heavily ins
**⚠️ SOFTWARE IS NOT IN THE RELEASE STATE**
<div align="center">
### Aegnux community
[![Reddit Badge](https://img.shields.io/badge/reddit-orange?style=for-the-badge&logoColor=%23ff4500&label=Aegnux%20Sub&link=https%3A%2F%2Fwww.reddit.com%2Fr%2FAegnux)](https://www.reddit.com/r/Aegnux/)
[![AUR Badge](https://img.shields.io/badge/aur-package-blue?style=for-the-badge&label=AUR&link=https%3A%2F%2Faur.archlinux.org%2Fpackages%2Faegnux)](https://aur.archlinux.org/packages/aegnux)
[![Telegram Badge](https://img.shields.io/badge/telegram-chat-blue?style=for-the-badge&logoSize=%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20&label=Telegram&link=https%3A%2F%2Ft.me%2FAegnux)](https://t.me/Aegnux)
</div>
*If you're interested in the project's roadmap, check out [**ROADMAP.md**](https://github.com/relativemodder/aegnux/blob/main/ROADMAP.md)*.
[Download Flatpak package](https://github.com/relativemodder/com.relative.Aegnux/releases/latest) if you want to install it on any distro.

View File

@@ -2,13 +2,15 @@
- [ ] Get hardware acceleration working on AMD GPUs
- [ ] Plugins installation
- [x] Plugins installation
- [x] Ged rid of binary blobs
- [ ] Replace slow winetricks script
- [ ] Replace slow winetricks script (almost done)
- [ ] Desktop integration (.aep file association)
- [x] Desktop integration (.aep file association)
- [ ] Link opening support (e.g. Mister Horse or CC Login)
- [ ] Make .deb package

8
assets/dxvk.reg Normal file
View File

@@ -0,0 +1,8 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Wine\DllOverrides]
"d3d10core"="native,builtin"
"d3d11"="native,builtin"
"d3d8"="native,builtin"
"d3d9"="native,builtin"
"dxgi"="native,builtin"

7
assets/fontsmooth.reg Normal file
View File

@@ -0,0 +1,7 @@
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Control Panel\Desktop]
"FontSmoothing"="2"
"FontSmoothingGamma"=dword:00000578
"FontSmoothingOrientation"=dword:00000001
"FontSmoothingType"=dword:00000002

View File

@@ -1,12 +1,13 @@
#!/bin/sh
# Download Kitty Binary
echo Downloading Kitty Binary...
curl -LO https://github.com/kovidgoyal/kitty/releases/download/v0.43.1/kitty-0.43.1-x86_64.txz
mkdir -p ./bin/kitty && tar Jxf kitty-0.43.1-x86_64.txz --strip-components=0 -C ./bin/kitty
rm kitty-0.43.1-x86_64.txz
# Yeah, bundling the entire terminal emulator for flatpak builds lol
if [ -d /app ]; then
# Download Kitty Binary
echo Downloading Kitty Binary...
curl -LO https://github.com/kovidgoyal/kitty/releases/download/v0.43.1/kitty-0.43.1-x86_64.txz
mkdir -p ./bin/kitty && tar Jxf kitty-0.43.1-x86_64.txz --strip-components=0 -C ./bin/kitty
rm kitty-0.43.1-x86_64.txz
fi
# Download winetricks
echo Downloading Winetricks...
@@ -27,9 +28,9 @@ rm cabextract-1.11-1.x86_64.rpm
# Download Wine
echo Downloading Wine...
curl -LO https://github.com/Kron4ek/Wine-Builds/releases/download/10.17/wine-10.17-amd64-wow64.tar.xz
mkdir -p ./assets/wine && tar Jxf wine-10.17-amd64-wow64.tar.xz --strip-components=1 -C ./assets/wine
rm wine-10.17-amd64-wow64.tar.xz
curl -LO https://github.com/Kron4ek/Wine-Builds/releases/download/10.18/wine-10.18-staging-tkg-amd64-wow64.tar.xz
mkdir -p ./assets/wine && tar Jxf wine-10.18-staging-tkg-amd64-wow64.tar.xz --strip-components=1 -C ./assets/wine
rm wine-10.18-staging-tkg-amd64-wow64.tar.xz
# Download Visual C++ Redistributable Runtimes
@@ -52,5 +53,11 @@ echo Downloading gdiplus.dll...
curl -LO https://github.com/relativemodder/aegnux/releases/download/vcrbin/gdiplus.dll
mv gdiplus.dll ./assets/
# Download dxvk
echo Downloading dxvk...
curl -LO https://github.com/doitsujin/dxvk/releases/download/v2.7.1/dxvk-2.7.1.tar.gz
mv dxvk-2.7.1.tar.gz ./assets/dxvk.tar.gz
echo --------------------------------------------
echo Done!

7
run.sh
View File

@@ -1,8 +1,5 @@
#!/bin/sh
SCRIPT_PATH=$(readlink -f "$0")
SCRIPT_DIR=$(dirname "SCRIPT_PATH")
cd "$(dirname "$0")"
cd "$SCRIPT_DIR"
python main.py
python main.py "$@"

View File

@@ -11,7 +11,18 @@ def main():
app.setDesktopFileName(DESKTOP_FILE_NAME)
app.setWindowIcon(QIcon(AE_ICON_PATH))
mainWindow = MainWindow()
mainWindow.show()
show_window = True
quit_after_handling_args = False
for arg in sys.argv:
if 'misterhorsepm://' in arg or '.aep' in arg:
show_window = False
quit_after_handling_args = True
break
mainWindow = MainWindow(quit_after_handling_args)
if show_window:
mainWindow.show()
return app.exec()

View File

@@ -12,6 +12,9 @@ WINE_RUNNER_DIR = BASE_DIR + '/assets/wine'
WINETRICKS_BIN = BASE_DIR + '/bin/winetricks'
CABEXTRACT_BIN = BASE_DIR + '/bin/cabextract'
GDIPLUS_DLL = BASE_DIR + '/assets/gdiplus.dll'
FONTSMOOTH_REG = BASE_DIR + '/assets/fontsmooth.reg'
DXVK_TAR = BASE_DIR + '/assets/dxvk.tar.gz'
DXVK_REG = BASE_DIR + '/assets/dxvk.reg'
VCR_ZIP = BASE_DIR + '/assets/vcr.zip'
MSXML_ZIP = BASE_DIR + '/assets/msxml3.zip'

View File

@@ -1,18 +1,21 @@
import os
from pathlib import Path
import shutil
import tarfile
import traceback
from src.config import (
AE_DOWNLOAD_URL, AE_FILENAME,
AE_DOWNLOAD_URL, AE_FILENAME, DXVK_REG, FONTSMOOTH_REG,
WINE_RUNNER_DIR, WINETRICKS_BIN,
CABEXTRACT_BIN, WINE_STYLE_REG,
VCR_ZIP, MSXML_ZIP, GDIPLUS_DLL
VCR_ZIP, MSXML_ZIP, GDIPLUS_DLL, DXVK_TAR
)
from src.processthread import ProcessThread
from src.utils import (
DownloadMethod, get_aegnux_installation_dir,
get_ae_install_dir, get_wine_runner_dir, is_nvidia_present,
get_winetricks_bin, get_wineprefix_dir, get_cabextract_bin,
get_vcr_dir_path, get_msxml_dir_path, mark_aegnux_as_installed
get_vcr_dir_path, get_msxml_dir_path, mark_aegnux_as_installed,
get_cep_dir
)
class InstallationThread(ProcessThread):
@@ -32,11 +35,8 @@ class InstallationThread(ProcessThread):
def run(self):
try:
try:
shutil.rmtree(get_aegnux_installation_dir(), True)
except:
self.log_signal.emit(f'[WARNING] Can\'t remove existing installation.')
self.try_cleanup_installation()
self.progress_signal.emit(10)
if self.download_method == DownloadMethod.ONLINE:
@@ -45,35 +45,7 @@ class InstallationThread(ProcessThread):
self.progress_signal.emit(15)
self.log_signal.emit(f'[DEBUG] Unpacking AE from {self.ae_filename}...')
aegnux_install_dir = get_aegnux_installation_dir()
self.unpack_zip(self.ae_filename, aegnux_install_dir.as_posix())
root_path = Path(aegnux_install_dir)
target_dir = Path(get_ae_install_dir())
source_folder_to_delete = None
self.log_signal.emit('[DEBUG] Searching for AfterFX.exe...')
for exe_path in root_path.rglob('AfterFX.exe'):
source_dir_to_move = exe_path.parent
self.log_signal.emit(f'[DEBUG] Found installation folder: {source_dir_to_move}')
for item in source_dir_to_move.iterdir():
shutil.move(item.as_posix(), target_dir.joinpath(item.name).as_posix())
self.log_signal.emit(f'[DEBUG] All contents moved to {target_dir}')
source_folder_to_delete = source_dir_to_move.parent
break
if source_folder_to_delete and source_folder_to_delete != root_path:
self.log_signal.emit(f'[DEBUG] Removing temporary folder: {source_folder_to_delete}')
try:
shutil.rmtree(source_folder_to_delete.as_posix())
self.log_signal.emit('[DEBUG] Temporary folder removed successfully.')
except OSError as e:
self.log_signal.emit(f'[ERROR] Failed to remove temporary folder {source_folder_to_delete}: {e}')
else:
self.log_signal.emit('[WARNING] Installation folder not found or matched root path. No folder was deleted.')
self.unpack_ae()
self.progress_signal.emit(20)
@@ -107,48 +79,27 @@ class InstallationThread(ProcessThread):
self.progress_signal.emit(60)
tweaks = ['dxvk', 'corefonts', 'fontsmooth=rgb']
tweaks = ['corefonts']
for tweak in tweaks:
self.log_signal.emit(f'[DEBUG] Installing {tweak} with winetricks')
self.run_command(['winetricks', '-q', tweak], in_prefix=True)
self.progress_signal.emit(60 + tweaks.index(tweak) * 2)
self.install_dxvk()
self.progress_signal.emit(70)
self.log_signal.emit(f'[DEBUG] Unpacking VCR to {get_vcr_dir_path()}...')
self.unpack_zip(VCR_ZIP, get_vcr_dir_path().as_posix())
self.progress_signal.emit(80)
self.log_signal.emit(f'[DEBUG] Installing VCR')
self.run_command(['wine', get_vcr_dir_path().joinpath('install_all.bat').as_posix()], in_prefix=True)
self.install_vcr()
self.progress_signal.emit(85)
self.log_signal.emit(f'[DEBUG] Unpacking MSXML3 to {get_msxml_dir_path()}...')
self.unpack_zip(MSXML_ZIP, get_msxml_dir_path().as_posix())
self.progress_signal.emit(90)
self.install_msxml3()
self.log_signal.emit(f'[DEBUG] Overriding MSXML3 DLL...')
system32_dir = get_wineprefix_dir().joinpath('drive_c/windows/system32')
shutil.copy(get_msxml_dir_path().joinpath('msxml3.dll'), system32_dir.joinpath('msxml3.dll'))
shutil.copy(get_msxml_dir_path().joinpath('msxml3r.dll'), system32_dir.joinpath('msxml3r.dll'))
self.install_gdiplus()
self.log_signal.emit(f'[DEBUG] Applying fontsmooth settings')
self.run_command(
['wine', 'reg', 'add',
'HKCU\\Software\\Wine\\DllOverrides', '/v',
'msxml3', '/d', 'native,builtin', '/f'],
in_prefix=True
)
self.log_signal.emit(f'[DEBUG] Overriding gdiplus DLL...')
shutil.copy(GDIPLUS_DLL, system32_dir.joinpath('gdiplus.dll'))
self.run_command(
['wine', 'reg', 'add',
'HKCU\\Software\\Wine\\DllOverrides', '/v',
'gdiplus', '/d', 'native,builtin', '/f'],
['wine', 'regedit', FONTSMOOTH_REG],
in_prefix=True
)
@@ -156,6 +107,13 @@ class InstallationThread(ProcessThread):
self.log_signal.emit("[INFO] Starting NVIDIA libs installation...")
self.install_nvidia_libs()
try:
self.log_signal.emit(f"[INFO] Created CEP directory in {get_cep_dir()}")
except:
pass
self.symlink_support_files()
self.progress_signal.emit(99)
self.cleanup()
@@ -166,8 +124,139 @@ class InstallationThread(ProcessThread):
self.finished_signal.emit(True)
except Exception as e:
traceback.print_exc()
self.log_signal.emit(f'[ERROR] {e}')
self.finished_signal.emit(False)
def symlink_support_files(self):
ae_dir = get_ae_install_dir()
ae_pf_dir = get_wineprefix_dir().joinpath('drive_c/Program Files/Adobe/Adobe After Effects 2024')
support_files_dir = ae_pf_dir.joinpath('Support Files')
if not ae_pf_dir.exists():
os.makedirs(ae_pf_dir)
if not support_files_dir.exists():
os.symlink(ae_dir, support_files_dir)
self.log_signal.emit(f'[DEBUG] Created symlink from {ae_dir} to {support_files_dir}')
def try_cleanup_installation(self):
try:
shutil.rmtree(get_aegnux_installation_dir(), True)
except:
self.log_signal.emit(f'[WARNING] Can\'t remove existing installation.')
def install_vcr(self):
self.log_signal.emit(f'[DEBUG] Unpacking VCR to {get_vcr_dir_path()}...')
self.unpack_zip(VCR_ZIP, get_vcr_dir_path().as_posix())
self.progress_signal.emit(80)
self.log_signal.emit(f'[DEBUG] Installing VCR')
self.run_command(['wine', get_vcr_dir_path().joinpath('install_all.bat').as_posix()], in_prefix=True)
def install_msxml3(self):
self.log_signal.emit(f'[DEBUG] Unpacking MSXML3 to {get_msxml_dir_path()}...')
self.unpack_zip(MSXML_ZIP, get_msxml_dir_path().as_posix())
self.progress_signal.emit(90)
self.log_signal.emit(f'[DEBUG] Overriding MSXML3 DLL...')
system32_dir = get_wineprefix_dir().joinpath('drive_c/windows/system32')
shutil.copy(get_msxml_dir_path().joinpath('msxml3.dll'), system32_dir.joinpath('msxml3.dll'))
shutil.copy(get_msxml_dir_path().joinpath('msxml3r.dll'), system32_dir.joinpath('msxml3r.dll'))
self.run_command(
['wine', 'reg', 'add',
'HKCU\\Software\\Wine\\DllOverrides', '/v',
'msxml3', '/d', 'native,builtin', '/f'],
in_prefix=True
)
def install_gdiplus(self):
system32_dir = get_wineprefix_dir().joinpath('drive_c/windows/system32')
self.log_signal.emit(f'[DEBUG] Overriding gdiplus DLL...')
shutil.copy(GDIPLUS_DLL, system32_dir.joinpath('gdiplus.dll'))
self.run_command(
['wine', 'reg', 'add',
'HKCU\\Software\\Wine\\DllOverrides', '/v',
'gdiplus', '/d', 'native,builtin', '/f'],
in_prefix=True
)
def get_tar_root_dir_name(self, tar_path) -> str | None:
with tarfile.open(tar_path, 'r') as tar:
member_names = tar.getnames()
if not member_names:
return None
first_member = member_names[0]
root_name = first_member.split(os.path.sep)[0]
return root_name
def install_dxvk(self):
system32_dir = get_wineprefix_dir().joinpath('drive_c/windows/system32')
syswow64_dir = get_wineprefix_dir().joinpath('drive_c/windows/syswow64')
self.unpack_tar(DXVK_TAR, get_wineprefix_dir())
dxvk_name = self.get_tar_root_dir_name(DXVK_TAR)
dxvk_root_dir = get_wineprefix_dir().joinpath(dxvk_name)
self.log_signal.emit(f'[DEBUG] DXVK root dir is {dxvk_root_dir}')
source_x64 = dxvk_root_dir.joinpath('x64')
source_x32 = dxvk_root_dir.joinpath('x32')
for source in [source_x64, source_x32]:
for item in source.iterdir():
system_dir = system32_dir if 'x64' in source.as_posix() else syswow64_dir
if item.is_file():
shutil.copy2(item, system_dir.joinpath(item.name))
elif item.is_dir():
dest = system_dir.joinpath(item.name)
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(item, dest)
self.log_signal.emit(f'[DEBUG] Overriding DXVK dlls')
self.run_command(
['wine', 'regedit', DXVK_REG],
in_prefix=True
)
def unpack_ae(self):
self.log_signal.emit(f'[DEBUG] Unpacking AE from {self.ae_filename}...')
aegnux_install_dir = get_aegnux_installation_dir()
self.unpack_zip(self.ae_filename, aegnux_install_dir.as_posix())
root_path = Path(aegnux_install_dir)
target_dir = Path(get_ae_install_dir())
source_folder_to_delete = None
self.log_signal.emit('[DEBUG] Searching for AfterFX.exe...')
for exe_path in root_path.rglob('AfterFX.exe'):
source_dir_to_move = exe_path.parent
self.log_signal.emit(f'[DEBUG] Found installation folder: {source_dir_to_move}')
for item in source_dir_to_move.iterdir():
shutil.move(item.as_posix(), target_dir.joinpath(item.name).as_posix())
self.log_signal.emit(f'[DEBUG] All contents moved to {target_dir}')
source_folder_to_delete = source_dir_to_move.parent
break
if source_folder_to_delete and source_folder_to_delete != root_path:
self.log_signal.emit(f'[DEBUG] Removing temporary folder: {source_folder_to_delete}')
try:
shutil.rmtree(source_folder_to_delete.as_posix())
self.log_signal.emit('[DEBUG] Temporary folder removed successfully.')
except OSError as e:
self.log_signal.emit(f'[ERROR] Failed to remove temporary folder {source_folder_to_delete}: {e}')
else:
self.log_signal.emit('[WARNING] Installation folder not found or matched root path. No folder was deleted.')
def install_nvidia_libs(self):
download_url = "https://github.com/SveSop/nvidia-libs/releases/download/v0.8.5/nvidia-libs-v0.8.5.tar.xz"

View File

@@ -1,5 +1,6 @@
import os
import subprocess
import sys
from ui.mainwindow import MainWindowUI
from translations import gls
from PySide6.QtCore import Slot
@@ -9,19 +10,24 @@ from src.installationthread import InstallationThread
from src.runaethread import RunAEThread
from src.runexethread import RunExeThread
from src.killaethread import KillAEThread
from src.pluginthread import PluginThread
from src.removeaethread import RemoveAEThread
from src.utils import (
check_aegnux_tip_marked, get_wine_bin_path_env,
show_download_method_dialog, get_ae_plugins_dir, get_wineprefix_dir,
check_aegnux_tip_marked, get_default_terminal, get_mhtb_install_dir, get_wine_bin_path_env,
get_cep_dir, get_ae_plugins_dir, get_wineprefix_dir,
check_aegnux_installed, mark_aegnux_tip_as_shown, get_ae_install_dir, get_aegnux_installation_dir
)
from src.types import DownloadMethod
class MainWindow(MainWindowUI):
def __init__(self):
def __init__(self, quit_after_handling_args: bool = False):
super().__init__()
self.ran_from_aep_file = False
self.ran_from_mhtb_link = False
self.quit_after_handling_args = quit_after_handling_args
self.setWindowTitle(gls('welcome_win_title'))
self.install_button.clicked.connect(self.install_button_clicked)
self.run_button.clicked.connect(self.run_ae_button_clicked)
@@ -40,6 +46,11 @@ class MainWindow(MainWindowUI):
self.remove_ae_thread = RemoveAEThread()
self.remove_ae_thread.finished_signal.connect(self._finished)
self.plugin_thread = PluginThread()
self.plugin_thread.log_signal.connect(self._log)
self.plugin_thread.progress_signal.connect(self.progress_bar.setValue)
self.plugin_thread.finished_signal.connect(self._finished)
self.alt_t_action = QAction(self)
self.alt_t_action.setShortcut(QKeySequence("Alt+T"))
self.alt_t_action.triggered.connect(self.run_command_alt_t)
@@ -56,6 +67,7 @@ class MainWindow(MainWindowUI):
self.ae_action.triggered.connect(self.run_ae_button_clicked)
self.exe_action.triggered.connect(self.run_exe_button_clicked)
self.reg_action.triggered.connect(self.reg_button_clicked)
self.plugininst_action.triggered.connect(self.install_plugins_button_clicked)
self.kill_action.triggered.connect(self.kill_ae_button_clicked)
self.log_action.triggered.connect(self.toggle_logs)
self.term_action.triggered.connect(self.run_command_alt_t)
@@ -63,6 +75,58 @@ class MainWindow(MainWindowUI):
self.plugind_action.triggered.connect(self.plugins_folder_clicked)
self.aed_action.triggered.connect(self.ae_folder_clicked)
self.aeg_action.triggered.connect(self.aegnux_folder_clicked)
self.cep_action.triggered.connect(self.cep_folder_clicked)
def try_autoopen_mhtb(self):
if self.ran_from_mhtb_link:
return
self.ran_from_mhtb_link = True
mhtb_link = ''
for arg in sys.argv:
if 'misterhorsepm://' in arg:
mhtb_link = arg
break
if mhtb_link == '':
return
mhtb_dir = get_mhtb_install_dir()
if mhtb_dir is None:
QMessageBox.warning(
self,
gls('mhtb_not_found_title'),
gls('mhtb_not_found_text')
)
exit(0)
self.run_mhtb_thread = RunExeThread([f'{mhtb_dir.as_posix()}/ProductManager.exe', mhtb_link])
self.run_mhtb_thread.log_signal.connect(self._log)
self.run_mhtb_thread.finished_signal.connect(self._finished)
self.run_mhtb_thread.start()
def try_autoopen_aep(self):
self.run_ae_thread.clear_aep_file_arg()
if self.ran_from_aep_file:
return
self.ran_from_aep_file = True
aep_file = ''
for arg in sys.argv:
if '.aep' in arg:
aep_file = arg
break
if aep_file == '':
return
self.run_ae_thread.add_aep_file_arg(aep_file)
self.run_ae_button_clicked()
def init_installation(self):
if check_aegnux_installed():
@@ -73,7 +137,10 @@ class MainWindow(MainWindowUI):
self.runMenu.setEnabled(True)
self.browseMenu.setEnabled(True)
self.kill_action.setEnabled(True)
self.plugininst_action.setEnabled(True)
self.term_action.setEnabled(True)
self.try_autoopen_aep()
self.try_autoopen_mhtb()
else:
self.install_button.show()
@@ -84,11 +151,13 @@ class MainWindow(MainWindowUI):
self.browseMenu.setEnabled(False)
self.kill_action.setEnabled(False)
self.term_action.setEnabled(False)
self.plugininst_action.setEnabled(False)
def _construct_menubar(self):
self.runMenu = self.menuBar().addMenu(gls('run_menu'))
self.ae_action = self.runMenu.addAction(gls('ae_action'))
self.exe_action = self.runMenu.addAction(gls('exe_action'))
self.plugininst_action = self.runMenu.addAction(gls('plugininst_action'))
self.reg_action = self.runMenu.addAction(gls('reg_action'))
self.browseMenu = self.menuBar().addMenu(gls('browse_menu'))
@@ -96,6 +165,7 @@ class MainWindow(MainWindowUI):
self.plugind_action = self.browseMenu.addAction(gls('plugind_action'))
self.aed_action = self.browseMenu.addAction(gls('aed_action'))
self.aeg_action = self.browseMenu.addAction(gls('aeg_action'))
self.cep_action = self.browseMenu.addAction(gls('cep_action'))
self.debugMenu = self.menuBar().addMenu(gls('debug_menu'))
self.kill_action = self.debugMenu.addAction(gls('kill_action'))
@@ -106,6 +176,8 @@ class MainWindow(MainWindowUI):
self.install_button.setEnabled(not lock)
self.run_button.setEnabled(not lock)
self.remove_aegnux_button.setEnabled(not lock)
self.runMenu.setEnabled(not lock)
@Slot()
def toggle_logs(self):
@@ -116,10 +188,21 @@ class MainWindow(MainWindowUI):
@Slot(bool)
def _finished(self, success: bool):
if self.quit_after_handling_args:
exit(0)
self.lock_ui(False)
self.progress_bar.hide()
self.init_installation()
if not success:
QMessageBox.critical(
self,
gls('error'),
self.logs_edit.toPlainText().split('\n')[-2]
)
return
if check_aegnux_installed() and not check_aegnux_tip_marked():
QMessageBox.information(self, '', gls('tip_alt_t'))
mark_aegnux_tip_as_shown()
@@ -141,6 +224,12 @@ class MainWindow(MainWindowUI):
self.install_thread.set_download_method(method)
if method == DownloadMethod.OFFLINE:
QMessageBox.warning(
self,
gls('offline_note'),
gls('offline_note_text')
)
filename, _ = QFileDialog.getOpenFileName(
self,
gls('offline_ae_zip_title'),
@@ -156,6 +245,29 @@ class MainWindow(MainWindowUI):
self.progress_bar.show()
self.install_thread.start()
@Slot()
def install_plugins_button_clicked(self):
QMessageBox.information(
self,
gls('plugin_note'),
gls('plugin_note_text')
)
filename, _ = QFileDialog.getOpenFileName(
self,
gls('offline_ae_zip_title'),
"",
"Zip Files (*.zip);;All Files (*)"
)
if filename == '':
return
self.plugin_thread.set_plugin_zip_filename(filename)
self.lock_ui()
self.progress_bar.show()
self.plugin_thread.start()
@Slot()
def run_ae_button_clicked(self):
self.lock_ui()
@@ -222,16 +334,21 @@ class MainWindow(MainWindowUI):
def aegnux_folder_clicked(self):
os.system(f'xdg-open "{get_aegnux_installation_dir()}"')
@Slot()
def cep_folder_clicked(self):
os.system(f'xdg-open "{get_cep_dir()}"')
@Slot()
def run_command_alt_t(self):
env = os.environ.copy()
env['WINEPREFIX'] = get_wineprefix_dir()
env['PATH'] = get_wine_bin_path_env('/usr/bin')
process = subprocess.Popen(
['./bin/kitty/bin/kitty', 'bash'],
env=env
)
try:
terminal = get_default_terminal()
subprocess.Popen([terminal, "bash"], env=env)
except RuntimeError as e:
print("[CRITICAL ERROR]:", e)
@Slot()
def run_command_ctrl_q(self):

162
src/pluginthread.py Normal file
View File

@@ -0,0 +1,162 @@
import os
import shutil
from src.processthread import ProcessThread
from src.utils import get_private_plugins_unpack_path, get_ae_plugins_dir, get_wineprefix_dir
class PluginThread(ProcessThread):
def __init__(self):
super().__init__()
def set_plugin_zip_filename(self, filename: str):
self.plugin_zip_filename = filename
def run(self):
self.log_signal.emit('[DEBUG] Unpacking plugins from the archive...')
self.progress_signal.emit(5)
self.remove_ppu_dir()
ppu_dir = get_private_plugins_unpack_path()
self.unpack_zip(self.plugin_zip_filename, ppu_dir.as_posix())
self.progress_signal.emit(15)
self.install_aex_plugins()
self.install_cep_extensions()
self.install_presets()
self.run_installers()
self.remove_ppu_dir()
self.progress_signal.emit(95)
self.log_signal.emit('[INFO] The plugins have been installed')
self.finished_signal.emit(True)
self.progress_signal.emit(100)
def install_aex_plugins(self):
self.log_signal.emit('[DEBUG] Installing aex plugins...')
ppu_dir = get_private_plugins_unpack_path()
aex_src = ppu_dir.joinpath('aex')
for item in os.listdir(aex_src.as_posix()):
if self._is_cancelled:
self.cancelled.emit()
return
src_path = aex_src.joinpath(item)
dst_path = get_ae_plugins_dir().joinpath(item)
if os.path.isdir(src_path):
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
else:
shutil.copy2(src_path, dst_path)
self.log_signal.emit('[INFO] AEX plugins installed')
self.progress_signal.emit(30)
def install_presets(self):
self.log_signal.emit('[DEBUG] Installing presets...')
ppu_dir = get_private_plugins_unpack_path()
preset_src = ppu_dir.joinpath('preset-backup')
preset_dest = get_wineprefix_dir().joinpath('drive_c/users/relative/Documents/Adobe/After Effects 2024/User Presets')
os.makedirs(preset_dest, exist_ok=True)
for item in os.listdir(preset_src):
if self._is_cancelled:
self.cancelled.emit()
return
src_path = preset_src.joinpath(item)
dst_path = preset_dest.joinpath(item)
if os.path.isdir(src_path):
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
else:
shutil.copy2(src_path, dst_path)
self.log_signal.emit("[INFO] Presets installed")
self.progress_signal.emit(55)
def run_installers(self):
self.log_signal.emit('[DEBUG] Running installers...')
ppu_dir = get_private_plugins_unpack_path()
install_src = ppu_dir.joinpath('installer')
installers_counter = 0
for exe in os.listdir(install_src.as_posix()):
if exe.endswith('.exe') and exe not in ['E3D.exe', 'saber.exe']:
self.progress_signal.emit(55 + installers_counter * 3)
self.log_signal.emit(f"[INFO] Installing: {exe}")
self.run_command(
['wine', exe, '/verysilent', '/suppressmsgboxes'],
install_src.as_posix(), True
)
self.progress_signal.emit(70)
# Special handling for E3D and saber
for exe in ['E3D.exe', 'saber.exe']:
self.log_signal.emit(f"[INFO] Please manually install: {exe}")
self.run_command(
['wine', exe],
install_src.as_posix(), True
)
self.progress_signal.emit(85)
self.copy_element_files()
def copy_element_files(self):
self.log_signal.emit('[DEBUG] Copying Element files...')
ppu_dir = get_private_plugins_unpack_path()
install_src = ppu_dir.joinpath('installer')
video_copilot_dir = get_ae_plugins_dir().joinpath("VideoCopilot")
element_files = [
("Element.aex", "Element.aex"),
("Element.license", "Element.license")
]
for src_name, dst_name in element_files:
src_path = install_src.joinpath(src_name)
if os.path.exists(src_path):
shutil.copy2(src_path, os.path.join(video_copilot_dir, dst_name))
self.log_signal.emit(f"[INFO] {src_name} copied successfully")
self.log_signal.emit("[INFO] Element installed")
self.progress_signal.emit(90)
def install_cep_extensions(self):
self.log_signal.emit('[DEBUG] Installing CEP extensions...')
ppu_dir = get_private_plugins_unpack_path()
cep_dir = ppu_dir.joinpath('CEP')
cep_reg_file = cep_dir.joinpath("AddKeys.reg")
self.run_command(['wine', "regedit", cep_reg_file.as_posix()], in_prefix=True)
self.install_flow()
self.log_signal.emit("[INFO] CEP extensions installed")
self.progress_signal.emit(45)
def install_flow(self):
ppu_dir = get_private_plugins_unpack_path()
cep_dir = ppu_dir.joinpath('CEP')
self.log_signal.emit('[DEBUG] Installing Flow...')
flow_src = cep_dir.joinpath("flowv1.4.2")
cep_dst = get_wineprefix_dir().joinpath("drive_c/Program Files (x86)/Common Files/Adobe/CEP/extensions")
os.makedirs(cep_dst, exist_ok=True)
shutil.copytree(flow_src, os.path.join(cep_dst, "flowv1.4.2"), dirs_exist_ok=True)
self.log_signal.emit("[INFO] Flow installed")
def remove_ppu_dir(self):
ppu_dir = get_private_plugins_unpack_path()
try:
shutil.rmtree(ppu_dir.as_posix())
self.log_signal.emit('[DEBUG] Temporary folder removed successfully.')
except OSError as e:
self.log_signal.emit(f'[WARNING] Failed to remove temporary folder {ppu_dir}: {e}')

View File

@@ -6,5 +6,13 @@ class RunAEThread(RunExeThread):
def __init__(self):
super().__init__(['AfterFX.exe'])
def add_aep_file_arg(self, aep_file: str):
self.exe_args.append('Z:' + aep_file)
def clear_aep_file_arg(self):
for arg in self.exe_args:
if '.aep' in arg:
self.exe_args.remove(arg)
def run(self):
super().run()

View File

@@ -91,6 +91,15 @@ def get_wineprefix_dir():
return wineprefix_dir
def get_cep_dir():
wineprefix_dir = get_wineprefix_dir()
cep_dir = wineprefix_dir.joinpath('drive_c/Program Files (x86)/Common Files/Adobe/CEP')
if not os.path.exists(cep_dir):
os.makedirs(cep_dir)
return cep_dir
def get_wine_runner_dir():
aegnux_dir = get_aegnux_installation_dir()
runner_dir = aegnux_dir.joinpath('runner')
@@ -139,7 +148,61 @@ def get_wine_bin_path_env(old_path: str | None):
old_path = old_path if old_path is not None else os.getenv('PATH')
return f'{get_wine_runner_dir().as_posix()}/bin:{old_path}'
def get_mhtb_install_dir():
wineprefix_dir = get_wineprefix_dir()
mhtb_dir = wineprefix_dir.joinpath('drive_c/Program Files/Mister Horse Product Manager')
if not os.path.exists(mhtb_dir):
return None
return mhtb_dir
def get_default_terminal() -> str:
DEFAULT_ENVS = ["TERMINAL", "TERM_PROGRAM"]
TERMINALS = [
"konsole",
"kitty",
"alacritty",
"gnome-terminal",
"xfce4-terminal",
"terminator",
"lxterminal",
"tilix",
"st",
"mate-terminal",
"xterm",
"urxvt",
"deepin-terminal",
"sakura",
"tilda",
"guake",
"hyper",
"eterm",
"rxvt",
"lxterm",
"cosmic-terminal",
]
candidates = (
shutil.which(os.environ.get(var)) for var in DEFAULT_ENVS if os.environ.get(var)
)
# Debian/Ubuntu
alt = "/etc/alternatives/x-terminal-emulator"
if os.path.exists(alt):
candidates = (shutil.which(os.readlink(alt)), *candidates)
# Popular terminals
candidates = (*candidates, *(shutil.which(term) for term in TERMINALS))
terminal = next((t for t in candidates if t), None)
if terminal:
return terminal
raise RuntimeError(
"Your terminal was not found or is not supported.\n"
"Install one of the following: " + ", ".join(TERMINALS)
)
def get_aegnux_tip_marked_flag_path():
hades = get_aegnux_installation_dir()
@@ -150,4 +213,14 @@ def check_aegnux_tip_marked():
def mark_aegnux_tip_as_shown():
with open(get_aegnux_tip_marked_flag_path(), 'w') as f:
f.write('Press ALT+T to open up a terminal')
f.write('Press ALT+T to open up a terminal')
def get_private_plugins_unpack_path():
aegnux_dir = get_aegnux_installation_dir()
ppu_dir = aegnux_dir.joinpath('private-plugins')
if not os.path.exists(ppu_dir):
os.makedirs(ppu_dir)
return ppu_dir

View File

@@ -28,5 +28,17 @@ STRINGS = {
'debug_menu': 'Debug',
'kill_action': 'Kill AE',
'log_action': 'Toggle log',
'term_action': 'Open Terminal'
'term_action': 'Open Terminal',
'cep_action': 'CEP directory',
'offline_note': 'Offline note',
'offline_note_text': 'Note that the .zip archive should contain After Effects from Program Files (including AfterFX.exe).',
'error': 'Error',
'plugininst_action': 'Install private plugins from ZIP',
'plugin_note': 'Private plugins note',
'plugin_note_text': 'Those plugins are available at https://t.me/Aegnux',
'done_title': 'Done!',
'done_ae': 'AE has been installed.',
'done_plugins': 'The plugins have been installed.',
'mhtb_not_found_title': 'Mister Horse Product Manager Not Found',
'mhtb_not_found_text': 'Mister Horse Product Manager is not installed in the Wine prefix. Please install it first.'
}

View File

@@ -28,5 +28,16 @@ STRINGS = {
'debug_menu': 'Отладка',
'kill_action': 'Остановить AE',
'log_action': 'Вкл/выкл логи',
'term_action': 'Открыть терминал'
'term_action': 'Открыть терминал',
'cep_action': 'Папка с CEP',
'offline_note': 'Внимание, оффлайн',
'offline_note_text': 'Учтите, что .zip-архив должен содержать After Effects из Program Files (включая AfterFX.exe).',
'error': 'Ошибка',
'plugininst_action': 'Установить приватные плагины из ZIP',
'plugin_note': 'Замечание к приватным плагинам',
'plugin_note_text': 'Эти плагины доступны здесь: https://t.me/Aegnux',
'done_ae': 'AE был установлен.',
'done_plugins': 'Плагины были установлены.',
'mhtb_not_found_title': 'Mister Horse Product Manager не найден',
'mhtb_not_found_text': 'Mister Horse Product Manager не установлен в префиксе Wine. Пожалуйста, сначала установите его.'
}

43
translations/uk_UA.py Normal file
View File

@@ -0,0 +1,43 @@
STRINGS = {
"welcome_win_title": "Aegnux — Ласкаво просимо",
"welcome_to_aegnux": "Ласкаво просимо до Aegnux!",
"subtitle_text": "Простіший спосіб змусити працювати After Effects на GNU/Linux",
"install": "Встановити",
"footer_text": "Зроблено з любовʼю від Relative",
"installation_method_title": "Метод встановлення",
"installation_method_text": "Як ви хочете встановити Aegnux?",
"offline_ae_zip_title": "Виберіть ZIP-архів AE",
"run_ae": "Запустити AE",
"kill_ae": "Зупинити AE",
"toggle_logs": "Увімк/вимк логування",
"remove_aegnux": "Видалити Aegnux",
"plugins": "Плагіни",
"wineprefix": "Префікс Wine",
"tip_alt_t": "Порада: натисніть ALT+T, щоб відкрити термінал із середовищем Wine та вже заданим префіксом.",
"confirm_exit": "Підтвердження виходу",
"confirm_exit_text": "Ви справді хочете вийти з Aegnux?",
"run_menu": "Запустити",
"ae_action": "After Effects",
"exe_action": "Інший файл .EXE",
"reg_action": "Імпортувати файл реєстру",
"browse_menu": "Огляд",
"wpd_action": "Префікс Wine",
"plugind_action": "Папка з плагінами",
"aed_action": "Папка з AE",
"aeg_action": "Папка з інсталяцією Aegnux",
"debug_menu": "Відлагодження",
"kill_action": "Зупинити AE",
"log_action": "Увімк/вимк логування",
"term_action": "Відкрити термінал",
"cep_action": "Папка з CEP",
'offline_note': 'Увага, офлайн',
'offline_note_text': 'Зауважте, що .zip-архів повинен містити After Effects з Program Files (включаючи AfterFX.exe).',
'error': 'Помилка',
'plugininst_action': 'Встановити приватні плагіни з ZIP',
'plugin_note': 'Примітка до приватних плагінів',
'plugin_note_text': 'Ці плагіни доступні тут: https://t.me/Aegnux',
'done_ae': 'AE було встановлено.',
'done_plugins': 'Плагіни було встановлено.',
'mhtb_not_found_title': 'Mister Horse Product Manager не знайдено',
'mhtb_not_found_text': 'Mister Horse Product Manager не встановлено в префіксі Wine. Будь ласка, спочатку встановіть його.'
}

View File

@@ -14,7 +14,7 @@ class MainWindowUI(QMainWindow):
super().__init__()
self._construct_ui()
self._set_styles()
self.setMinimumSize(480, 600)
self.setMinimumSize(500, 600)
def _set_styles(self):
with open(f'{STYLES_PATH}/mainwindow.css') as fp:
@@ -33,8 +33,21 @@ class MainWindowUI(QMainWindow):
def _construct_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout_container = QWidget()
layout_container.setMinimumWidth(500)
self.root_layout = QVBoxLayout(central_widget)
self.root_layout = QVBoxLayout(layout_container)
main_layout = QHBoxLayout(central_widget)
main_layout.addItem(
QSpacerItem(1, 1, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
)
main_layout.addWidget(layout_container)
main_layout.addItem(
QSpacerItem(1, 1, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
)
self.add_expanding_vertical_sizer()