diff --git a/src/toolManager/gui_fixed.py b/src/toolManager/gui_fixed.py index 1e96f0010..5d9979ae0 100644 --- a/src/toolManager/gui_fixed.py +++ b/src/toolManager/gui_fixed.py @@ -3,7 +3,6 @@ import sys import ctypes import subprocess -import platform from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView, QProgressBar, @@ -15,12 +14,12 @@ from PyQt6.QtGui import QFont import logging -PYTHON = sys.executable -SYSTEM = platform.system() -IS_WINDOWS = SYSTEM == "Windows" -IS_LINUX = SYSTEM == "Linux" +try: + from toolManager.constants import IS_WINDOWS, IS_LINUX +except (ImportError, ValueError): + from constants import IS_WINDOWS, IS_LINUX -import os +PYTHON = sys.executable BASE_DIR = os.path.dirname(os.path.abspath(__file__)) if IS_WINDOWS: diff --git a/src/toolManager/main.py b/src/toolManager/main.py index e005821de..445c00549 100644 --- a/src/toolManager/main.py +++ b/src/toolManager/main.py @@ -14,18 +14,26 @@ from PyQt6.QtGui import QFont try: - from gui_fixed import ToolManagerGUI + from toolManager.gui_fixed import ToolManagerGUI except ImportError: - pass # Will handle gracefully later if missing + from gui_fixed import ToolManagerGUI + +try: + from toolManager.constants import IS_WINDOWS +except (ImportError, ValueError): + from constants import IS_WINDOWS + +try: + from toolManager.registry import CATEGORIES +except (ImportError, ValueError): + from registry import CATEGORIES -# ==================== CONFIG ==================== BASE_DIR = Path(__file__).resolve().parent INFO_JSON = BASE_DIR / "information.json" -FULL_GUI = BASE_DIR / "gui_fixed.py" PYTHON = sys.executable -ANALOG_TOOLS = ["esim", "kicad", "ngspice"] -DIGITAL_TOOLS = ["esim", "kicad", "ngspice", "ghdl", "verilator", "llvm"] +ANALOG_TOOLS = CATEGORIES["analog"] +DIGITAL_TOOLS = CATEGORIES["digital"] TOOL_LABELS = { "esim": "eSim", @@ -54,7 +62,6 @@ def is_admin(): def relaunch_as_admin(): script = str(Path(__file__).resolve()) - # Swap to pythonw.exe to suppress the black console window python_exe = PYTHON if python_exe.lower().endswith("python.exe"): pythonw_exe = python_exe[:-10] + "pythonw.exe" @@ -126,7 +133,6 @@ def run(self): self.finished.emit(success) -# ==================== MAIN WINDOW ==================== class ToolManagerWindow(QMainWindow): def __init__(self): super().__init__() @@ -396,7 +402,6 @@ def _create_uninstall_tab(self): layout.setContentsMargins(30, 30, 30, 30) layout.setSpacing(16) - # Warning banner warn = QFrame() warn.setStyleSheet(""" QFrame { @@ -656,7 +661,7 @@ def _run_uninstall(self, tools, label): subprocess.Popen( [PYTHON, backend, "uninstall", tool, "none"], creationflags=(subprocess.CREATE_NO_WINDOW - if sys.platform == "win32" else 0) + if IS_WINDOWS else 0) ) QMessageBox.information( self, "Uninstall Started", @@ -669,9 +674,7 @@ def _run_uninstall(self, tools, label): def main(): - if sys.platform == "win32" and not is_admin(): - # Note: We deliberately skip a manual PyQt consent dialog here because - # Windows will natively prompt the user with a UAC shield anyway. + if IS_WINDOWS and not is_admin(): relaunch_as_admin() sys.exit(0) diff --git a/src/toolManager/qt/__init__.py b/src/toolManager/qt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/toolManager/qt/gui_fixed.py b/src/toolManager/qt/gui_fixed.py new file mode 100644 index 000000000..06e61a99b --- /dev/null +++ b/src/toolManager/qt/gui_fixed.py @@ -0,0 +1,9 @@ +from toolManager.gui_fixed import ToolManagerGUI, UninstallWindow, CommandWorker +from toolManager.registry import TOOLS +from toolManager.constants import IS_WINDOWS, IS_LINUX +from toolManager.paths import get_toolmanager_root + +__all__ = [ + "ToolManagerGUI", "UninstallWindow", "CommandWorker", + "TOOLS", "IS_WINDOWS", "IS_LINUX", "get_toolmanager_root", +] diff --git a/src/toolManager/qt/main.py b/src/toolManager/qt/main.py new file mode 100644 index 000000000..98dfbc2bf --- /dev/null +++ b/src/toolManager/qt/main.py @@ -0,0 +1,49 @@ +from toolManager.main import ToolManagerWindow, main +from toolManager.registry import TOOLS, CATEGORIES +from toolManager.constants import IS_WINDOWS +from toolManager.qt.gui_fixed import ToolManagerGUI + +ANALOG_TOOLS = CATEGORIES["analog"] +DIGITAL_TOOLS = CATEGORIES["digital"] +TOOL_LABELS = {tid: spec.label for tid, spec in TOOLS.items() + if tid in CATEGORIES["analog"] or tid in CATEGORIES["digital"]} +TOOL_VERSIONS = { + "esim": "2.4", + "kicad": "latest", + "ngspice": "latest", + "ghdl": "latest", + "verilator": "latest", + "llvm": "latest", +} + +try: + from toolManager.platform_utils import is_admin, relaunch_as_admin +except ImportError: + import ctypes + import os + import sys + + def is_admin(): + if IS_WINDOWS: + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except Exception: + return False + return True + + def relaunch_as_admin(): + script = sys.argv[0] + python_exe = sys.executable + if python_exe.lower().endswith("python.exe"): + pythonw_exe = python_exe[:-10] + "pythonw.exe" + if os.path.exists(pythonw_exe): + python_exe = pythonw_exe + ctypes.windll.shell32.ShellExecuteW( + None, "runas", python_exe, f'"{script}"', None, 1 + ) + +__all__ = [ + "ToolManagerWindow", "main", "ToolManagerGUI", + "TOOL_LABELS", "TOOL_VERSIONS", "ANALOG_TOOLS", "DIGITAL_TOOLS", + "IS_WINDOWS", "is_admin", "relaunch_as_admin", +] diff --git a/src/toolManager/qt/updater_gui.py b/src/toolManager/qt/updater_gui.py new file mode 100644 index 000000000..9075386bc --- /dev/null +++ b/src/toolManager/qt/updater_gui.py @@ -0,0 +1,4 @@ +from toolManager.updater_gui import PackageUpdaterWindow, main +from toolManager.registry import TOOLS + +__all__ = ["PackageUpdaterWindow", "main", "TOOLS"] diff --git a/src/toolManager/qt/worker.py b/src/toolManager/qt/worker.py new file mode 100644 index 000000000..45b74f490 --- /dev/null +++ b/src/toolManager/qt/worker.py @@ -0,0 +1,61 @@ +from PyQt6.QtCore import QThread, pyqtSignal +import subprocess +import sys + +try: + from toolManager.worker import BaseWorker, CommandWorker, InstallWorker, InstallerThread +except ImportError: + from toolManager.gui_fixed import CommandWorker + from toolManager.updater_gui import InstallerThread + + class BaseWorker(QThread): + def cancel(self): + pass + + try: + from toolManager.worker import InstallWorker + except ImportError: + from pathlib import Path + from toolManager.registry import TOOLS + + BASE_DIR = Path(__file__).resolve().parent.parent + PYTHON = sys.executable + + class InstallWorker(QThread): + progress = pyqtSignal(str) + finished = pyqtSignal(bool) + + def __init__(self, tools): + super().__init__() + self.tools = tools + + def run(self): + success = True + backend = str(BASE_DIR / "tool_manager_windows.py") + for tool, version in self.tools: + label = TOOLS[tool].label if tool in TOOLS else tool + self.progress.emit(f"Installing {label} {version}...") + try: + proc = subprocess.Popen( + [PYTHON, backend, "install", tool, version], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="ignore", + ) + for line in proc.stdout: + line = line.rstrip() + if line: + self.progress.emit(f" {line}") + if "[ERROR]" in line or "install_failed|" in line: + success = False + proc.wait() + if proc.returncode != 0: + success = False + except Exception as e: + self.progress.emit(f" [ERROR] {tool}: {e}") + success = False + self.finished.emit(success) + +__all__ = ["BaseWorker", "CommandWorker", "InstallWorker", "InstallerThread"]