mirror of
https://github.com/relativemodder/aegnux.git
synced 2025-12-10 05:29:38 +05:00
first version
This commit is contained in:
BIN
assets/msxml3.zip
Normal file
BIN
assets/msxml3.zip
Normal file
Binary file not shown.
BIN
bin/cabextract
Executable file
BIN
bin/cabextract
Executable file
Binary file not shown.
19627
bin/winetricks
Executable file
19627
bin/winetricks
Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,25 @@
|
||||
import os
|
||||
|
||||
LOG_THROTTLE_SECONDS=0.1
|
||||
DESKTOP_FILE_NAME='com.relative.Aegnux'
|
||||
|
||||
BASE_DIR = os.getcwd()
|
||||
|
||||
AE_ICON_PATH = BASE_DIR + '/icons/afterfx.png'
|
||||
STYLES_PATH = BASE_DIR + '/styles'
|
||||
|
||||
WINE_RUNNER_TAR = BASE_DIR + '/assets/wine-10.17-amd64-wow64.tar.gz'
|
||||
WINETRICKS_BIN = BASE_DIR + '/bin/winetricks'
|
||||
CABEXTRACT_BIN = BASE_DIR + '/bin/cabextract'
|
||||
|
||||
VCR_ZIP = BASE_DIR + '/assets/vcr.zip'
|
||||
MSXML_ZIP = BASE_DIR + '/assets/msxml3.zip'
|
||||
|
||||
WINE_STYLE_REG = STYLES_PATH + '/wine_dark_theme.reg'
|
||||
|
||||
AE_DOWNLOAD_URL = 'https://huggingface.co/cutefishae/AeNux-model/resolve/main/2024.zip'
|
||||
AE_PLUGINS_URL = 'https://huggingface.co/cutefishae/AeNux-model/resolve/main/aenux-require-plugin.zip'
|
||||
|
||||
AE_FILENAME = '/tmp/ae2024.zip'
|
||||
|
||||
DOWNLOAD_CHUNK_SIZE=1024
|
||||
162
src/installationthread.py
Normal file
162
src/installationthread.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
import shutil
|
||||
from src.config import (
|
||||
AE_DOWNLOAD_URL, AE_FILENAME,
|
||||
WINE_RUNNER_TAR, WINETRICKS_BIN,
|
||||
CABEXTRACT_BIN, WINE_STYLE_REG,
|
||||
VCR_ZIP, MSXML_ZIP
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
class InstallationThread(ProcessThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def set_download_method(self, method: DownloadMethod):
|
||||
self.download_method = method
|
||||
|
||||
def set_offline_filename(self, filename: str):
|
||||
self.ae_filename = filename
|
||||
|
||||
def cleanup(self):
|
||||
self.log_signal.emit(f'[CLEANUP] Removing temporary AE .zip file')
|
||||
if self.download_method == DownloadMethod.ONLINE:
|
||||
os.remove(AE_FILENAME)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.progress_signal.emit(10)
|
||||
|
||||
if self.download_method == DownloadMethod.ONLINE:
|
||||
self.download_file_to(AE_DOWNLOAD_URL, AE_FILENAME)
|
||||
self.ae_filename = AE_FILENAME
|
||||
|
||||
self.progress_signal.emit(15)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Unpacking AE from {self.ae_filename}...')
|
||||
self.unpack_zip(self.ae_filename, get_aegnux_installation_dir().as_posix())
|
||||
os.rename(get_aegnux_installation_dir().joinpath('Support Files'), get_ae_install_dir())
|
||||
|
||||
self.progress_signal.emit(20)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Unpacking Wine Runner from {WINE_RUNNER_TAR}...')
|
||||
self.unpack_tar(WINE_RUNNER_TAR, get_wine_runner_dir().as_posix())
|
||||
|
||||
self.progress_signal.emit(30)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Copying winetricks to {get_winetricks_bin()}...')
|
||||
shutil.copy(WINETRICKS_BIN, get_winetricks_bin())
|
||||
|
||||
self.progress_signal.emit(35)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Copying cabextract to {get_cabextract_bin()}...')
|
||||
shutil.copy(CABEXTRACT_BIN, get_cabextract_bin())
|
||||
|
||||
self.progress_signal.emit(40)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Initializing wineprefix in {get_wineprefix_dir()}...')
|
||||
self.run_command(['wineboot'], in_prefix=True)
|
||||
|
||||
self.progress_signal.emit(50)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] Tweaking visual settings in prefix')
|
||||
self.run_command(['wine', 'regedit', WINE_STYLE_REG], in_prefix=True)
|
||||
|
||||
self.progress_signal.emit(55)
|
||||
|
||||
self.log_signal.emit(f'[DEBUG] [WORKAROUND] Killing wineserver')
|
||||
self.run_command(['wineserver', '-k'], in_prefix=True)
|
||||
|
||||
self.progress_signal.emit(60)
|
||||
|
||||
tweaks = ['dxvk', 'corefonts', 'gdiplus', 'fontsmooth=rgb']
|
||||
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.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.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.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
|
||||
)
|
||||
|
||||
if is_nvidia_present():
|
||||
self.log_signal.emit("[INFO] Starting NVIDIA libs installation...")
|
||||
self.install_nvidia_libs()
|
||||
|
||||
self.progress_signal.emit(99)
|
||||
|
||||
self.cleanup()
|
||||
|
||||
mark_aegnux_as_installed()
|
||||
|
||||
self.progress_signal.emit(100)
|
||||
|
||||
self.finished_signal.emit(True)
|
||||
except Exception as e:
|
||||
self.log_signal.emit(f'[ERROR] {e}')
|
||||
self.finished_signal.emit(False)
|
||||
|
||||
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"
|
||||
nvidia_libs_dir = get_wineprefix_dir().joinpath("nvidia-libs")
|
||||
os.makedirs(nvidia_libs_dir, exist_ok=True)
|
||||
tar_file = nvidia_libs_dir.joinpath("nvidia-libs-v0.8.5.tar.xz")
|
||||
|
||||
self.download_file_to(download_url, tar_file)
|
||||
self.log_signal.emit("[DEBUG] Download completed.")
|
||||
self.log_signal.emit("[DEBUG] Extracting NVIDIA libs...")
|
||||
|
||||
extract_dir = nvidia_libs_dir.joinpath("nvidia-libs-v0.8.5")
|
||||
|
||||
self.unpack_tar(tar_file, nvidia_libs_dir)
|
||||
|
||||
self.log_signal.emit("[DEBUG] Extraction completed.")
|
||||
|
||||
os.remove(tar_file)
|
||||
|
||||
self.log_signal.emit("[DEBUG] Running setup script...")
|
||||
|
||||
setup_script = extract_dir.joinpath("setup_nvlibs.sh")
|
||||
|
||||
self.log_signal.emit(f"[DEBUG] Running: {setup_script} install")
|
||||
|
||||
returncode = self.run_command([setup_script.as_posix(), 'install'], in_prefix=True)
|
||||
|
||||
if returncode != 0:
|
||||
self.log_signal.emit(f"[ERROR] Setup script failed with return code {returncode}.")
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
self.log_signal.emit("[INFO] NVIDIA libs installation completed!")
|
||||
|
||||
13
src/killaethread.py
Normal file
13
src/killaethread.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from src.processthread import ProcessThread
|
||||
|
||||
class KillAEThread(ProcessThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
self.run_command(
|
||||
['wineserver', '-k'],
|
||||
in_prefix=True
|
||||
)
|
||||
|
||||
self.finished_signal.emit(True)
|
||||
@@ -1,6 +1,14 @@
|
||||
import os
|
||||
from ui.mainwindow import MainWindowUI
|
||||
from translations import gls
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
from src.installationthread import InstallationThread
|
||||
from src.runaethread import RunAEThread
|
||||
from src.killaethread import KillAEThread
|
||||
from src.removeaethread import RemoveAEThread
|
||||
from src.utils import show_download_method_dialog, get_ae_plugins_dir, get_wineprefix_dir
|
||||
from src.types import DownloadMethod
|
||||
|
||||
|
||||
class MainWindow(MainWindowUI):
|
||||
@@ -9,7 +17,93 @@ class MainWindow(MainWindowUI):
|
||||
|
||||
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)
|
||||
self.kill_button.clicked.connect(self.kill_ae_button_clicked)
|
||||
self.remove_aegnux_button.clicked.connect(self.remove_aegnux_button_clicked)
|
||||
self.toggle_logs_button.clicked.connect(self.toggle_logs)
|
||||
self.plugins_button.clicked.connect(self.plugins_folder_clicked)
|
||||
self.wineprefix_button.clicked.connect(self.wineprefix_folder_clicked)
|
||||
|
||||
self.install_thread = InstallationThread()
|
||||
self.install_thread.log_signal.connect(self._log)
|
||||
self.install_thread.progress_signal.connect(self.progress_bar.setValue)
|
||||
self.install_thread.finished_signal.connect(self._finished)
|
||||
|
||||
self.run_ae_thread = RunAEThread()
|
||||
self.run_ae_thread.log_signal.connect(self._log)
|
||||
self.run_ae_thread.finished_signal.connect(self._finished)
|
||||
|
||||
self.kill_ae_thread = KillAEThread()
|
||||
self.remove_ae_thread = RemoveAEThread()
|
||||
self.remove_ae_thread.finished_signal.connect(self._finished)
|
||||
|
||||
self.init_installation()
|
||||
|
||||
def lock_ui(self, lock: bool = True):
|
||||
self.install_button.setEnabled(not lock)
|
||||
self.run_button.setEnabled(not lock)
|
||||
self.remove_aegnux_button.setEnabled(not lock)
|
||||
|
||||
@Slot()
|
||||
def toggle_logs(self):
|
||||
if self.logs_edit.isHidden():
|
||||
self.logs_edit.show()
|
||||
return
|
||||
self.logs_edit.hide()
|
||||
|
||||
@Slot(bool)
|
||||
def _finished(self, success: bool):
|
||||
self.lock_ui(False)
|
||||
self.progress_bar.hide()
|
||||
self.init_installation()
|
||||
|
||||
@Slot(str)
|
||||
def _log(self, message: str):
|
||||
self.logs_edit.append(message + '\n')
|
||||
|
||||
@Slot()
|
||||
def install_button_clicked(self):
|
||||
pass
|
||||
method = show_download_method_dialog(gls('installation_method_title'), gls('installation_method_text'))
|
||||
|
||||
if method == DownloadMethod.CANCEL:
|
||||
return
|
||||
|
||||
self.install_thread.set_download_method(method)
|
||||
|
||||
if method == DownloadMethod.OFFLINE:
|
||||
filename, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
gls('offline_ae_zip_title'),
|
||||
"",
|
||||
"Zip Files (*.zip);;All Files (*)"
|
||||
)
|
||||
if filename == '':
|
||||
return
|
||||
|
||||
self.install_thread.set_offline_filename(filename)
|
||||
|
||||
self.lock_ui()
|
||||
self.progress_bar.show()
|
||||
self.install_thread.start()
|
||||
|
||||
@Slot()
|
||||
def run_ae_button_clicked(self):
|
||||
self.lock_ui()
|
||||
self.run_ae_thread.start()
|
||||
|
||||
@Slot()
|
||||
def kill_ae_button_clicked(self):
|
||||
self.kill_ae_thread.start()
|
||||
|
||||
@Slot()
|
||||
def remove_aegnux_button_clicked(self):
|
||||
self.lock_ui()
|
||||
self.remove_ae_thread.start()
|
||||
|
||||
@Slot()
|
||||
def plugins_folder_clicked(self):
|
||||
os.system(f'xdg-open "{get_ae_plugins_dir()}"')
|
||||
|
||||
@Slot()
|
||||
def wineprefix_folder_clicked(self):
|
||||
os.system(f'xdg-open "{get_wineprefix_dir()}"')
|
||||
263
src/processthread.py
Normal file
263
src/processthread.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import zipfile
|
||||
import tarfile
|
||||
import subprocess
|
||||
import select
|
||||
import fcntl
|
||||
from src.utils import format_size, get_wineprefix_dir, get_wine_bin_path_env
|
||||
from src.config import DOWNLOAD_CHUNK_SIZE, LOG_THROTTLE_SECONDS
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
|
||||
class ProcessThread(QThread):
|
||||
log_signal = Signal(str)
|
||||
progress_signal = Signal(int)
|
||||
finished_signal = Signal(bool)
|
||||
cancelled = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._is_cancelled = False
|
||||
|
||||
def cancel(self):
|
||||
self._is_cancelled = True
|
||||
|
||||
def download_file_to(self, url: str, filename: str):
|
||||
r = requests.get(url, stream=True)
|
||||
total = int(r.headers.get('content-length', 0))
|
||||
|
||||
downloaded = 0
|
||||
start_time = time.time()
|
||||
last_update_time = time.time() # throttling timer
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
for data in r.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
|
||||
if self._is_cancelled:
|
||||
self.log_signal.emit(f'[DOWNLOAD] Cancelled by user. Deleting partial file: {filename}')
|
||||
r.close()
|
||||
os.remove(filename)
|
||||
self.cancelled.emit()
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
f.write(data)
|
||||
downloaded += len(data)
|
||||
|
||||
current_time = time.time()
|
||||
if current_time - last_update_time >= LOG_THROTTLE_SECONDS:
|
||||
if total > 0:
|
||||
percent = int((downloaded / total) * 100)
|
||||
else:
|
||||
percent = 0
|
||||
|
||||
elapsed_time = current_time - start_time
|
||||
speed = (downloaded / elapsed_time) if elapsed_time > 0 else 0
|
||||
|
||||
self.log_signal.emit(f'[DOWNLOADING] {filename} ({percent}%/{format_size(total)}), {format_size(speed)}/s')
|
||||
last_update_time = current_time
|
||||
|
||||
if total > 0:
|
||||
final_percent = 100
|
||||
else:
|
||||
final_percent = 0
|
||||
|
||||
self.log_signal.emit(f'[DOWNLOADED] {filename} (100%/{format_size(total)})')
|
||||
|
||||
|
||||
def unpack_zip(self, zip_file_path: str, extract_to_path: str):
|
||||
self.log_signal.emit(f'[EXTRACTING] Starting ZIP extraction: {zip_file_path}')
|
||||
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
||||
members = [m for m in zip_ref.infolist() if not m.is_dir()]
|
||||
total_files = len(members)
|
||||
extracted_files = 0
|
||||
last_update_time = time.time()
|
||||
|
||||
os.makedirs(extract_to_path, exist_ok=True)
|
||||
|
||||
for file_info in members:
|
||||
if self._is_cancelled:
|
||||
self.log_signal.emit('[EXTRACTING] ZIP extraction cancelled by user.')
|
||||
self.cancelled.emit()
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
zip_ref.extract(file_info, extract_to_path)
|
||||
extracted_files += 1
|
||||
|
||||
current_time = time.time()
|
||||
# Throttling logic
|
||||
if current_time - last_update_time >= LOG_THROTTLE_SECONDS or extracted_files == total_files:
|
||||
if total_files > 0:
|
||||
percent = int((extracted_files / total_files) * 100)
|
||||
else:
|
||||
percent = 0
|
||||
|
||||
self.log_signal.emit(f'[EXTRACTING] {zip_file_path}: {extracted_files}/{total_files} files ({percent}%)')
|
||||
last_update_time = current_time
|
||||
|
||||
self.log_signal.emit(f'[EXTRACTED] ZIP finished extracting to {extract_to_path}')
|
||||
|
||||
|
||||
def unpack_tar(self, tar_file_path: str, extract_to_path: str):
|
||||
self.log_signal.emit(f'[EXTRACTING] Starting TAR extraction: {tar_file_path}')
|
||||
with tarfile.open(tar_file_path, 'r') as tar_ref:
|
||||
members = [m for m in tar_ref.getmembers() if m.isfile()]
|
||||
total_files = len(members)
|
||||
extracted_files = 0
|
||||
last_update_time = time.time()
|
||||
|
||||
os.makedirs(extract_to_path, exist_ok=True)
|
||||
|
||||
for member in members:
|
||||
if self._is_cancelled:
|
||||
self.log_signal.emit('[EXTRACTING] TAR extraction cancelled by user.')
|
||||
self.cancelled.emit()
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
tar_ref.extract(member, extract_to_path)
|
||||
extracted_files += 1
|
||||
|
||||
current_time = time.time()
|
||||
if total_files > 0 and (current_time - last_update_time >= LOG_THROTTLE_SECONDS or extracted_files == total_files):
|
||||
percent = int((extracted_files / total_files) * 100)
|
||||
self.log_signal.emit(f'[EXTRACTING] {tar_file_path}: {extracted_files}/{total_files} files ({percent}%)')
|
||||
last_update_time = current_time
|
||||
|
||||
self.log_signal.emit(f'[EXTRACTED] TAR finished extracting to {extract_to_path}')
|
||||
|
||||
|
||||
def _set_non_blocking(self, file):
|
||||
if os.name == 'posix':
|
||||
fd = file.fileno()
|
||||
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
|
||||
def run_command(self, command: list, cwd: str = None, in_prefix: bool = False):
|
||||
self.log_signal.emit(f'[COMMAND] Running command: {" ".join(command)}')
|
||||
self._is_cancelled = False
|
||||
|
||||
env = os.environ.copy()
|
||||
if in_prefix:
|
||||
env['WINEPREFIX'] = get_wineprefix_dir()
|
||||
env['PATH'] = get_wine_bin_path_env(env.get('PATH', os.defpath))
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=cwd,
|
||||
env=env
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self.log_signal.emit(f'[ERROR] Command not found: {command[0]}')
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
self._set_non_blocking(process.stdout)
|
||||
self._set_non_blocking(process.stderr)
|
||||
|
||||
stdout_buffer = b''
|
||||
stderr_buffer = b''
|
||||
|
||||
pipes = {
|
||||
process.stdout.fileno(): ('STDOUT', stdout_buffer),
|
||||
process.stderr.fileno(): ('STDERR', stderr_buffer)
|
||||
}
|
||||
|
||||
last_log_time = time.time()
|
||||
|
||||
while process.poll() is None or pipes:
|
||||
if self._is_cancelled:
|
||||
self.log_signal.emit('[COMMAND] Process cancelled by user. Terminating...')
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log_signal.emit('[COMMAND] Process did not terminate, killing...')
|
||||
process.kill()
|
||||
self.cancelled.emit()
|
||||
self.finished_signal.emit(False)
|
||||
return
|
||||
|
||||
if pipes:
|
||||
rlist, _, _ = select.select(pipes.keys(), [], [], 0.1)
|
||||
else:
|
||||
rlist = []
|
||||
|
||||
current_time = time.time()
|
||||
if rlist or current_time - last_log_time >= LOG_THROTTLE_SECONDS:
|
||||
for fd in rlist:
|
||||
stream_name, current_buffer_ref = pipes[fd]
|
||||
pipe = process.stdout if stream_name == 'STDOUT' else process.stderr
|
||||
|
||||
try:
|
||||
chunk = pipe.read(1024)
|
||||
except BlockingIOError:
|
||||
chunk = b''
|
||||
|
||||
if chunk:
|
||||
current_buffer = current_buffer_ref + chunk
|
||||
|
||||
if stream_name == 'STDOUT':
|
||||
stdout_buffer = current_buffer
|
||||
pipes[fd] = (stream_name, stdout_buffer)
|
||||
else:
|
||||
stderr_buffer = current_buffer
|
||||
pipes[fd] = (stream_name, stderr_buffer)
|
||||
|
||||
if not chunk and process.poll() is not None:
|
||||
del pipes[fd]
|
||||
break
|
||||
|
||||
if current_time - last_log_time >= LOG_THROTTLE_SECONDS:
|
||||
if process.stdout.fileno() in pipes:
|
||||
stream_name, current_buffer = pipes[process.stdout.fileno()]
|
||||
lines = current_buffer.split(b'\n')
|
||||
stdout_buffer = lines.pop()
|
||||
pipes[process.stdout.fileno()] = (stream_name, stdout_buffer)
|
||||
|
||||
for line_bytes in lines:
|
||||
line = line_bytes.decode('utf-8', errors='replace').strip()
|
||||
if line:
|
||||
self.log_signal.emit(f'[STDOUT] {line}')
|
||||
|
||||
if process.stderr.fileno() in pipes:
|
||||
stream_name, current_buffer = pipes[process.stderr.fileno()]
|
||||
lines = current_buffer.split(b'\n')
|
||||
stderr_buffer = lines.pop()
|
||||
pipes[process.stderr.fileno()] = (stream_name, stderr_buffer)
|
||||
|
||||
for line_bytes in lines:
|
||||
line = line_bytes.decode('utf-8', errors='replace').strip()
|
||||
if line:
|
||||
self.log_signal.emit(f'[STDERR] {line}')
|
||||
|
||||
last_log_time = current_time
|
||||
|
||||
if process.poll() is not None and not pipes:
|
||||
break
|
||||
|
||||
def flush_buffer(buffer, stream_name):
|
||||
if buffer.strip():
|
||||
try:
|
||||
line = buffer.decode('utf-8', errors='replace').strip()
|
||||
if line:
|
||||
self.log_signal.emit(f'[{stream_name}] {line}')
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
flush_buffer(stdout_buffer, 'STDOUT')
|
||||
flush_buffer(stderr_buffer, 'STDERR')
|
||||
|
||||
return_code = process.wait()
|
||||
|
||||
if return_code == 0:
|
||||
self.log_signal.emit(f'[COMMAND] Command finished successfully. Return code: {return_code}')
|
||||
else:
|
||||
self.log_signal.emit(f'[COMMAND] Command failed. Return code: {return_code}')
|
||||
|
||||
return return_code
|
||||
11
src/removeaethread.py
Normal file
11
src/removeaethread.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from src.processthread import ProcessThread
|
||||
from src.utils import get_aegnux_installation_dir
|
||||
import shutil
|
||||
|
||||
class RemoveAEThread(ProcessThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
shutil.rmtree(get_aegnux_installation_dir(), True)
|
||||
self.finished_signal.emit(True)
|
||||
15
src/runaethread.py
Normal file
15
src/runaethread.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from src.processthread import ProcessThread
|
||||
from src.utils import get_ae_install_dir
|
||||
|
||||
class RunAEThread(ProcessThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
self.run_command(
|
||||
['wine', 'AfterFX.exe'],
|
||||
cwd=get_ae_install_dir(),
|
||||
in_prefix=True
|
||||
)
|
||||
|
||||
self.finished_signal.emit(True)
|
||||
6
src/types.py
Normal file
6
src/types.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
class DownloadMethod(Enum):
|
||||
ONLINE = 1
|
||||
OFFLINE = 2
|
||||
CANCEL = 3
|
||||
140
src/utils.py
Normal file
140
src/utils.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from src.types import DownloadMethod
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from pathlib import Path
|
||||
|
||||
def format_size(size_bytes):
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 2)
|
||||
|
||||
return f"{s} {size_name[i]}"
|
||||
|
||||
def is_nvidia_present():
|
||||
if shutil.which('nvidia-smi'):
|
||||
return True
|
||||
|
||||
if os.path.exists('/proc/driver/nvidia'):
|
||||
return True
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
['lspci'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=False
|
||||
)
|
||||
stdout, _ = process.communicate()
|
||||
if 'NVIDIA' in stdout.decode('utf-8', errors='ignore').upper():
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def show_download_method_dialog(title: str, message: str) -> DownloadMethod:
|
||||
dialog = QMessageBox()
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText(message)
|
||||
|
||||
download_btn = dialog.addButton("Download", QMessageBox.ButtonRole.AcceptRole)
|
||||
choose_file_btn = dialog.addButton("Choose Local File", QMessageBox.ButtonRole.ActionRole)
|
||||
cancel_btn = dialog.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
dialog.exec()
|
||||
|
||||
clicked_button = dialog.clickedButton()
|
||||
|
||||
if clicked_button == download_btn:
|
||||
return DownloadMethod.ONLINE
|
||||
|
||||
if clicked_button == choose_file_btn:
|
||||
return DownloadMethod.OFFLINE
|
||||
|
||||
return DownloadMethod.CANCEL
|
||||
|
||||
def get_aegnux_installation_dir():
|
||||
data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
||||
aegnux_dir = Path(data_home).joinpath('aegnux')
|
||||
|
||||
if not os.path.exists(aegnux_dir):
|
||||
os.makedirs(aegnux_dir)
|
||||
|
||||
return aegnux_dir
|
||||
|
||||
def get_ae_install_dir():
|
||||
aegnux_dir = get_aegnux_installation_dir()
|
||||
ae_dir = aegnux_dir.joinpath('AE')
|
||||
|
||||
if not os.path.exists(ae_dir):
|
||||
os.makedirs(ae_dir)
|
||||
|
||||
return ae_dir
|
||||
|
||||
def get_ae_plugins_dir():
|
||||
ae_dir = get_ae_install_dir()
|
||||
return ae_dir.joinpath('Plug-ins')
|
||||
|
||||
def get_wineprefix_dir():
|
||||
aegnux_dir = get_aegnux_installation_dir()
|
||||
wineprefix_dir = aegnux_dir.joinpath('wineprefix')
|
||||
|
||||
if not os.path.exists(wineprefix_dir):
|
||||
os.makedirs(wineprefix_dir)
|
||||
|
||||
return wineprefix_dir
|
||||
|
||||
def get_wine_runner_dir():
|
||||
aegnux_dir = get_aegnux_installation_dir()
|
||||
runner_dir = aegnux_dir.joinpath('runner')
|
||||
|
||||
if not os.path.exists(runner_dir):
|
||||
os.makedirs(runner_dir)
|
||||
|
||||
return runner_dir
|
||||
|
||||
def get_wine_bin():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('bin/wine')
|
||||
|
||||
def get_wineserver_bin():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('bin/wineserver')
|
||||
|
||||
def get_winetricks_bin():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('bin/winetricks')
|
||||
|
||||
def get_cabextract_bin():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('bin/cabextract')
|
||||
|
||||
def get_vcr_dir_path():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('vcr')
|
||||
|
||||
def get_msxml_dir_path():
|
||||
runner_dir = get_wine_runner_dir()
|
||||
return runner_dir.joinpath('msxml')
|
||||
|
||||
def get_aegnux_installed_flag_path():
|
||||
hades = get_aegnux_installation_dir()
|
||||
return hades.joinpath('installed')
|
||||
|
||||
def check_aegnux_installed():
|
||||
return os.path.exists(get_aegnux_installed_flag_path())
|
||||
|
||||
def mark_aegnux_as_installed():
|
||||
with open(get_aegnux_installed_flag_path(), 'w') as f:
|
||||
f.write('have fun :)')
|
||||
|
||||
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}'
|
||||
@@ -9,13 +9,16 @@
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
#install_button {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
margin-left: 3em;
|
||||
margin-right: 3em;
|
||||
#install_button,
|
||||
#run_ae,
|
||||
#toggle_logs_button,
|
||||
#remove_aegnux_button,
|
||||
#kill_ae,
|
||||
#plugins_button,
|
||||
#wineprefix_button {
|
||||
width: 100%;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
#footer_label {
|
||||
|
||||
36
styles/wine_dark_theme.reg
Normal file
36
styles/wine_dark_theme.reg
Normal file
@@ -0,0 +1,36 @@
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_CURRENT_USER\Control Panel\Colors]
|
||||
"ActiveBorder"="49 54 58"
|
||||
"ActiveTitle"="49 54 58"
|
||||
"AppWorkSpace"="60 64 72"
|
||||
"Background"="49 54 58"
|
||||
"ButtonAlternativeFace"="200 0 0"
|
||||
"ButtonDkShadow"="154 154 154"
|
||||
"ButtonFace"="49 54 58"
|
||||
"ButtonHilight"="119 126 140"
|
||||
"ButtonLight"="60 64 72"
|
||||
"ButtonShadow"="60 64 72"
|
||||
"ButtonText"="219 220 222"
|
||||
"GradientActiveTitle"="49 54 58"
|
||||
"GradientInactiveTitle"="49 54 58"
|
||||
"GrayText"="155 155 155"
|
||||
"Hilight"="119 126 140"
|
||||
"HilightText"="255 255 255"
|
||||
"InactiveBorder"="49 54 58"
|
||||
"InactiveTitle"="49 54 58"
|
||||
"InactiveTitleText"="219 220 222"
|
||||
"InfoText"="159 167 180"
|
||||
"InfoWindow"="49 54 58"
|
||||
"Menu"="49 54 58"
|
||||
"MenuBar"="49 54 58"
|
||||
"MenuHilight"="119 126 140"
|
||||
"MenuText"="219 220 222"
|
||||
"Scrollbar"="73 78 88"
|
||||
"TitleText"="219 220 222"
|
||||
"Window"="35 38 41"
|
||||
"WindowFrame"="49 54 58"
|
||||
"WindowText"="219 220 222"
|
||||
|
||||
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ThemeManager]
|
||||
"ThemeActive"=0
|
||||
@@ -3,5 +3,14 @@ STRINGS = {
|
||||
'welcome_to_aegnux': 'Welcome to Aegnux!',
|
||||
'subtitle_text': 'A simpler way to get After Effects running on GNU/Linux',
|
||||
'install': 'Install',
|
||||
'footer_text': 'Made with 💙 by Relative'
|
||||
'footer_text': 'Made with 💙 by Relative',
|
||||
'installation_method_title': 'Installation Method',
|
||||
'installation_method_text': 'How would you like to install Aegnux?',
|
||||
'offline_ae_zip_title': 'Select AE Zip File',
|
||||
'run_ae': 'Run AE',
|
||||
'kill_ae': 'Kill AE',
|
||||
'toggle_logs': 'Toggle logs',
|
||||
'remove_aegnux': 'Remove Aegnux',
|
||||
'plugins': 'Plugins',
|
||||
'wineprefix': 'Wine prefix'
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QVBoxLayout, QWidget,
|
||||
QVBoxLayout, QWidget, QHBoxLayout,
|
||||
QLabel, QMainWindow, QPushButton,
|
||||
QSpacerItem, QSizePolicy
|
||||
QSpacerItem, QSizePolicy, QTextEdit, QProgressBar
|
||||
)
|
||||
from PySide6.QtCore import Qt, QSize
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from translations import gls
|
||||
from src.config import AE_ICON_PATH, STYLES_PATH
|
||||
from src.utils import check_aegnux_installed
|
||||
|
||||
class MainWindowUI(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -24,6 +25,29 @@ class MainWindowUI(QMainWindow):
|
||||
QSpacerItem(1, 2, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding)
|
||||
)
|
||||
|
||||
def add_fixed_vertical_sizer(self, height: int):
|
||||
self.root_layout.addItem(
|
||||
QSpacerItem(1, height, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
)
|
||||
|
||||
def init_installation(self):
|
||||
if check_aegnux_installed():
|
||||
self.install_button.hide()
|
||||
self.run_button.show()
|
||||
self.kill_button.show()
|
||||
self.remove_aegnux_button.show()
|
||||
|
||||
self.plugins_button.show()
|
||||
self.wineprefix_button.show()
|
||||
else:
|
||||
self.install_button.show()
|
||||
self.run_button.hide()
|
||||
self.kill_button.hide()
|
||||
self.remove_aegnux_button.hide()
|
||||
|
||||
self.plugins_button.hide()
|
||||
self.wineprefix_button.hide()
|
||||
|
||||
def _construct_ui(self):
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
@@ -53,14 +77,81 @@ class MainWindowUI(QMainWindow):
|
||||
|
||||
self.root_layout.addWidget(subtitle_label)
|
||||
|
||||
self.add_fixed_vertical_sizer(30)
|
||||
|
||||
action_row = QHBoxLayout()
|
||||
action_col = QVBoxLayout()
|
||||
|
||||
self.install_button = QPushButton(gls('install'))
|
||||
self.install_button.setIcon(QIcon.fromTheme('install-symbolic'))
|
||||
self.install_button.setIconSize(QSize(35, 20))
|
||||
self.install_button.setIconSize(QSize(25, 15))
|
||||
self.install_button.setObjectName('install_button')
|
||||
self.root_layout.addWidget(self.install_button)
|
||||
self.install_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
action_col.addWidget(self.install_button)
|
||||
|
||||
|
||||
self.run_button = QPushButton(gls('run_ae'))
|
||||
self.run_button.setIcon(QIcon.fromTheme('media-playback-start'))
|
||||
self.run_button.setIconSize(QSize(25, 15))
|
||||
self.run_button.setObjectName('run_ae')
|
||||
action_col.addWidget(self.run_button)
|
||||
self.run_button.hide()
|
||||
|
||||
folders_row = QHBoxLayout()
|
||||
self.plugins_button = QPushButton(gls('plugins'))
|
||||
self.plugins_button.setIcon(QIcon.fromTheme('document-open-folder'))
|
||||
self.plugins_button.setIconSize(QSize(25, 15))
|
||||
self.plugins_button.setObjectName('plugins_button')
|
||||
|
||||
self.wineprefix_button = QPushButton(gls('wineprefix'))
|
||||
self.wineprefix_button.setIcon(QIcon.fromTheme('document-open-folder'))
|
||||
self.wineprefix_button.setIconSize(QSize(25, 15))
|
||||
self.wineprefix_button.setObjectName('wineprefix_button')
|
||||
|
||||
self.toggle_logs_button = QPushButton(gls('toggle_logs'))
|
||||
self.toggle_logs_button.setIcon(QIcon.fromTheme('view-list-text'))
|
||||
self.toggle_logs_button.setIconSize(QSize(25, 15))
|
||||
self.toggle_logs_button.setObjectName('toggle_logs_button')
|
||||
action_col.addWidget(self.toggle_logs_button)
|
||||
|
||||
folders_row.addWidget(self.plugins_button)
|
||||
folders_row.addWidget(self.wineprefix_button)
|
||||
|
||||
action_col.addLayout(folders_row)
|
||||
|
||||
destruction_row = QHBoxLayout()
|
||||
|
||||
self.kill_button = QPushButton(gls('kill_ae'))
|
||||
self.kill_button.setObjectName('kill_ae')
|
||||
destruction_row.addWidget(self.kill_button)
|
||||
self.kill_button.hide()
|
||||
|
||||
|
||||
self.remove_aegnux_button = QPushButton(gls('remove_aegnux'))
|
||||
self.remove_aegnux_button.setObjectName('remove_aegnux_button')
|
||||
destruction_row.addWidget(self.remove_aegnux_button)
|
||||
self.remove_aegnux_button.hide()
|
||||
|
||||
action_col.addLayout(destruction_row)
|
||||
|
||||
|
||||
self.logs_edit = QTextEdit()
|
||||
self.logs_edit.setObjectName('logs_edit')
|
||||
self.logs_edit.setFixedHeight(140)
|
||||
self.logs_edit.setReadOnly(True)
|
||||
self.logs_edit.hide()
|
||||
action_col.addWidget(self.logs_edit)
|
||||
|
||||
self.progress_bar = QProgressBar(minimum=0, maximum=100, value=0)
|
||||
self.progress_bar.hide()
|
||||
action_col.addWidget(self.progress_bar)
|
||||
|
||||
action_row.addItem(QSpacerItem(50, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
action_row.addLayout(action_col)
|
||||
action_row.addItem(QSpacerItem(50, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
|
||||
self.root_layout.addLayout(action_row)
|
||||
|
||||
self.add_expanding_vertical_sizer()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user