Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions src/toolManager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
from typing import Dict, Optional, Any
from datetime import datetime

# Import local paths helper with robust fallback
try:
from .paths import get_install_state_path
except (ImportError, ValueError):
try:
from paths import get_install_state_path
except ImportError:
def get_install_state_path():
return Path(__file__).resolve().parent / "information.json"

def get_install_state_path():
return Path(__file__).resolve().parent / "information.json"

logger = logging.getLogger(__name__)

Expand Down
32 changes: 0 additions & 32 deletions src/toolManager/constants.py

This file was deleted.

27 changes: 15 additions & 12 deletions src/toolManager/gui_fixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import sys
import ctypes
import subprocess
import platform
from pathlib import Path
from platform_utils import IS_WINDOWS, IS_LINUX, IS_MAC
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QHeaderView, QProgressBar,
Expand All @@ -15,20 +16,20 @@
from PyQt6.QtGui import QFont
import logging

PYTHON = sys.executable
SYSTEM = platform.system()
IS_WINDOWS = SYSTEM == "Windows"
IS_LINUX = SYSTEM == "Linux"

import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PYTHON = sys.executable

BASE_DIR = Path(__file__).resolve().parent

if IS_WINDOWS:
BACKEND = os.path.join(BASE_DIR, "tool_manager_windows.py")
BACKEND = BASE_DIR / "tool_manager_windows.py"
elif IS_LINUX:
BACKEND = os.path.join(BASE_DIR, "tool_manager_linux.py")
BACKEND = BASE_DIR / "tool_manager_linux.py"
elif IS_MAC:
BACKEND = BASE_DIR / "tool_manager_macos.py"
else:
BACKEND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tool_manager_windows.py")
BACKEND = BASE_DIR / "tool_manager_linux.py"


TOOLS = {
"esim": {
Expand Down Expand Up @@ -101,7 +102,9 @@ def run(self):
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
text=True,
encoding="utf-8",
errors="ignore"
)
process.stdin.write(self.password + "\n")
process.stdin.flush()
Expand Down Expand Up @@ -402,7 +405,7 @@ def get_sudo_password(self):
if IS_LINUX and self.sudo_password is None:
password, ok = QInputDialog.getText(
self, "Authentication Required",
"Enter your sudo password:", QLineEdit.Password
"Enter your sudo password:", QLineEdit.EchoMode.Password
)
if ok and password:
self.sudo_password = password
Expand Down
170 changes: 131 additions & 39 deletions src/toolManager/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import os
import sys
import json
import ctypes
import subprocess
from pathlib import Path
Expand All @@ -12,6 +11,7 @@
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont
from platform_utils import IS_WINDOWS, IS_LINUX, IS_MAC, distro_label

try:
from gui_fixed import ToolManagerGUI
Expand All @@ -21,7 +21,24 @@
# ==================== CONFIG ====================
BASE_DIR = Path(__file__).resolve().parent
INFO_JSON = BASE_DIR / "information.json"
FULL_GUI = BASE_DIR / "gui_fixed.py"

if IS_WINDOWS:
BACKEND = BASE_DIR / "tool_manager_windows.py"
FULL_GUI = BASE_DIR / "gui_fixed.py"
OS_LABEL = "Windows Edition"
elif IS_LINUX:
BACKEND = BASE_DIR / "tool_manager_linux.py"
FULL_GUI = BASE_DIR / "updater_gui.py"
OS_LABEL = f"{distro_label()} Edition"
elif IS_MAC:
BACKEND = BASE_DIR / "tool_manager_macos.py"
FULL_GUI = BASE_DIR / "updater_gui.py"
OS_LABEL = f"{distro_label()} Edition"
else:
BACKEND = BASE_DIR / "tool_manager_linux.py"
FULL_GUI = BASE_DIR / "updater_gui.py"
OS_LABEL = "Linux Edition"

PYTHON = sys.executable

ANALOG_TOOLS = ["esim", "kicad", "ngspice"]
Expand All @@ -46,12 +63,16 @@
}

def is_admin():
if not IS_WINDOWS:
return True
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except Exception:
return False

def relaunch_as_admin():
if not IS_WINDOWS:
return True
script = str(Path(__file__).resolve())

# Swap to pythonw.exe to suppress the black console window
Expand All @@ -67,17 +88,30 @@ def relaunch_as_admin():

def load_installed_versions():
versions = {k: "Not installed" for k in TOOL_LABELS}
try:
if INFO_JSON.exists():
with open(INFO_JSON) as f:
data = json.load(f)
for pkg in data.get("important_packages", []):
name = pkg.get("package_name", "")
ver = pkg.get("version", "Not installed")
if name in versions and ver not in ("", "Not installed"):
versions[name] = ver
except Exception as e:
print(f"Could not load version info: {e}")
for tool in TOOL_LABELS:
try:
result = subprocess.run(
[PYTHON, str(BACKEND), "check", tool, "none"],
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore",
timeout=15,
)
output = (result.stdout or "") + (result.stderr or "")
for line in output.splitlines():
line = line.strip()
if line.startswith("installed|"):
parts = line.split("|", 2)
path = parts[2] if len(parts) >= 3 else "installed"
versions[tool] = Path(path).name if path != "none" else "installed"
break
elif line.startswith("not_installed|"):
versions[tool] = "Not installed"
break
except Exception as e:
print(f"Check failed for {tool}: {e}")

return versions

def darken_color(hex_color, amount=0.15):
Expand All @@ -87,35 +121,37 @@ def darken_color(hex_color, amount=0.15):
b = max(0, int(int(h[4:6], 16) * (1 - amount)))
return f"#{r:02x}{g:02x}{b:02x}"

class InstallWorker(QThread):
class _PackageWorker(QThread):
"""Base worker that runs backend sub-commands sequentially."""
progress = pyqtSignal(str)
finished = pyqtSignal(bool)

def __init__(self, tools):
def __init__(self, tools: list, cmd: str):
super().__init__()
self.tools = tools
self.tools = tools # list of (tool, version)
self.cmd = cmd # "install", "update", or "uninstall"

def run(self):
success = True
backend = str(BASE_DIR / "tool_manager_windows.py")
backend = str(BACKEND)
for tool, version in self.tools:
self.progress.emit(
f"Installing {TOOL_LABELS.get(tool, tool)} {version}..."
)
label = TOOL_LABELS.get(tool, tool)
action = self.cmd.capitalize()
self.progress.emit(f"{action}ing {label} {version}...".replace("Uninstalling", "Uninstalling"))
try:
proc = subprocess.Popen(
[PYTHON, backend, "install", tool, version],
[PYTHON, backend, self.cmd, tool, version],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="ignore"
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:
if "[ERROR]" in line or "_failed|" in line:
success = False
proc.wait()
if proc.returncode != 0:
Expand All @@ -126,6 +162,16 @@ def run(self):
self.finished.emit(success)


class InstallWorker(_PackageWorker):
def __init__(self, tools):
super().__init__(tools, "install")


class UninstallWorker(_PackageWorker):
def __init__(self, tools):
super().__init__(tools, "uninstall")


# ==================== MAIN WINDOW ====================
class ToolManagerWindow(QMainWindow):
def __init__(self):
Expand Down Expand Up @@ -193,7 +239,7 @@ def _create_header(self):
t1 = QLabel("eSim Tool Manager")
t1.setFont(QFont("Arial", 24, QFont.Weight.Bold))
t1.setStyleSheet("color: white; background: transparent;")
t2 = QLabel("Package Management System • Windows Edition")
t2 = QLabel(OS_LABEL)
t2.setFont(QFont("Arial", 11))
t2.setStyleSheet("color: rgba(255,255,255,0.9); background: transparent;")
vbox.addWidget(t1)
Expand Down Expand Up @@ -650,31 +696,77 @@ def _uninstall_all(self):
)

def _run_uninstall(self, tools, label):
try:
backend = str(BASE_DIR / "tool_manager_windows.py")
for tool, _ in tools:
subprocess.Popen(
[PYTHON, backend, "uninstall", tool, "none"],
creationflags=(subprocess.CREATE_NO_WINDOW
if sys.platform == "win32" else 0)
)
"""Run uninstall sequentially in a worker thread."""
self.progress_frame.setVisible(True)
self.progress_log.setText(f"Starting uninstall of {label}...")
self.progress_bar.setRange(0, 0)
self._install_worker = UninstallWorker(tools)
self._install_worker.progress.connect(self._on_progress)
self._install_worker.finished.connect(
lambda ok: self._on_uninstall_done(ok, label)
)
self._install_worker.start()

def _on_uninstall_done(self, success, label):
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
self._refresh_status()
if success:
self.progress_log.setText(f"{label} uninstalled.")
QMessageBox.information(
self, "Uninstall Started",
f"Uninstalling {label} in the background.\n\n"
"This may take a few minutes.\n"
"Click Refresh to update the status display when done."
self, "Uninstall Complete",
f"{label} removed successfully.\n\n"
"Click Refresh to update the status display."
)
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to uninstall:\n{e}")
else:
self.progress_log.setText(
f"{label} some packages may not have been removed.\n"
"Check the log above for details."
)
QMessageBox.warning(
self, "Uninstall Finished with Warnings",
f"{label} completed but some tools may not have\n"
"been removed correctly.\n\n"
"Check the progress log for details."
)

def _ensure_x11_platform():
if not IS_LINUX:
return
if os.environ.get("QT_QPA_PLATFORM"):
return
has_x11 = bool(os.environ.get("DISPLAY"))
has_wayland = bool(os.environ.get("WAYLAND_DISPLAY"))
if has_x11:
if has_wayland:
print(
"[INFO] Both DISPLAY and WAYLAND_DISPLAY are set "
"forcing QT_QPA_PLATFORM=xcb to avoid the Wayland "
"compositor crash",
flush=True,
)
else:
print(
"[INFO] DISPLAY is set forcing QT_QPA_PLATFORM=xcb.",
flush=True,
)
os.environ["QT_QPA_PLATFORM"] = "xcb"
elif has_wayland:
print(
"[WARN] Only WAYLAND_DISPLAY is set"
"letting Qt use its default Wayland platform plugin.",
flush=True,
)


def main():
if sys.platform == "win32" and not is_admin():
if IS_WINDOWS 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.
relaunch_as_admin()
sys.exit(0)

_ensure_x11_platform()
app = QApplication(sys.argv)
app.setFont(QFont("Arial", 10))
app.setStyle("Fusion")
Expand Down
Loading
Loading