#!/usr/bin/python3 import tkinter as tk from tkinter import messagebox, ttk import os import subprocess import urllib.request import json import tempfile import zipfile import shutil import sys import socket # Add current directory to path for imports current_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, current_dir) # Try to import common_tools, but provide fallback try: from common_tools import LxTools, center_window_cross_platform USE_LXTOOLS = True print("Using local common_tools from LxTools project") except ImportError: try: from shared_libs.common_tools import LxTools, center_window_cross_platform USE_LXTOOLS = True print("Using shared_libs.common_tools") except ImportError: print("Warning: common_tools not found, using integrated methods") USE_LXTOOLS = False # ---------------------------- # App Configuration Class (korrigiert) # ---------------------------- class LXToolsAppConfig: VERSION = "1.0.8" APP_NAME = "LX Tools Installer" WINDOW_WIDTH = 650 WINDOW_HEIGHT = 600 DEBUG_WINDOW_HEIGHT = 700 # LxTools Installer eigene Ressourcen WORK_DIR = os.path.dirname(os.path.abspath(__file__)) ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") # Download URLs für alle installierbaren Projekte PROJECTS = { "wirepy": { "name": "Wire-Py", "description": "🔐 WireGuard VPN Manager", "download_url": "https://git.ilunix.de/punix/Wire-Py/archive/main.zip", "api_url": "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases", "archive_folder": "Wire-Py", "icon_key": "wirepy_icon", }, "logviewer": { "name": "LogViewer", "description": "📋 System Log Viewer", "download_url": "https://git.ilunix.de/punix/shared_libs/archive/main.zip", "api_url": "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases", "archive_folder": "shared_libs", "icon_key": "logviewer_icon", }, } # Shared Libraries (für alle Projekte benötigt) SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" SHARED_LIBS_API_URL = ( "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" ) @staticmethod def get_icon_path(icon_key): """Get icon path with fallbacks - KORRIGIERT""" base_paths = [ LXToolsAppConfig.ICONS_DIR, os.path.join(LXToolsAppConfig.WORK_DIR, "lx-icons"), "./lx-icons", "/usr/share/icons/lx-icons", ] icon_mapping = { "app_icon": ["64/download.png", "48/download.png", "32/download.png"], "wirepy_icon": ["32/wg_vpn.png", "48/wg_vpn.png"], "logviewer_icon": ["32/log.png", "48/log.png"], "download_icon": ["32/download.png", "48/download.png"], "download_error_icon": ["32/download_error.png", "48/error.png"], "success_icon": ["32/download.png", "48/download.png"], } if icon_key not in icon_mapping: return None for base_path in base_paths: if not os.path.exists(base_path): continue for icon_file in icon_mapping[icon_key]: full_path = os.path.join(base_path, icon_file) if os.path.exists(full_path): print(f"Found icon: {full_path}") return full_path print(f"Icon not found: {icon_key}") return None # ---------------------------- # Integrierte LxTools Methoden (korrigiert) # ---------------------------- class IntegratedLxTools: @staticmethod def center_window_cross_platform(window): """Center window on screen - works with multiple monitors""" window.update_idletasks() # Get window dimensions window_width = window.winfo_reqwidth() window_height = window.winfo_reqheight() # Get screen dimensions screen_width = window.winfo_screenwidth() screen_height = window.winfo_screenheight() # Calculate position pos_x = (screen_width // 2) - (window_width // 2) pos_y = (screen_height // 2) - (window_height // 2) # Ensure window is not positioned off-screen pos_x = max(0, pos_x) pos_y = max(0, pos_y) window.geometry(f"{window_width}x{window_height}+{pos_x}+{pos_y}") @staticmethod def msg_window(parent, title, message, msg_type="info", width=400, height=200): """Custom message window with proper centering""" msg_win = tk.Toplevel(parent) msg_win.title(title) msg_win.geometry(f"{width}x{height}") msg_win.transient(parent) msg_win.grab_set() # Configure grid msg_win.grid_columnconfigure(0, weight=1) msg_win.grid_rowconfigure(0, weight=1) msg_win.grid_rowconfigure(1, weight=0) # Message frame msg_frame = ttk.Frame(msg_win, padding=20) msg_frame.grid(row=0, column=0, sticky="nsew") msg_frame.grid_columnconfigure(0, weight=1) msg_frame.grid_rowconfigure(0, weight=1) # Message text msg_label = tk.Label( msg_frame, text=message, wraplength=width - 40, justify="left", font=("Helvetica", 10), ) msg_label.grid(row=0, column=0, sticky="nsew") # Button frame btn_frame = ttk.Frame(msg_win) btn_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=(0, 20)) btn_frame.grid_columnconfigure(0, weight=1) # OK Button ok_btn = ttk.Button(btn_frame, text="OK", command=msg_win.destroy) ok_btn.grid(row=0, column=0) # Center the window IntegratedLxTools.center_window_cross_platform(msg_win) # Focus msg_win.focus_set() ok_btn.focus_set() return msg_win # ---------------------------- # Theme Manager Class (korrigiert) # ---------------------------- class ThemeManager: @staticmethod def apply_light_theme(root): """Apply light theme using your working method""" try: # Verwende TK-Themes aus dem aktuellen LxTools Projekt-Ordner theme_dir = LXToolsAppConfig.THEMES_DIR water_theme_path = os.path.join(theme_dir, "water.tcl") print(f"Looking for theme at: {water_theme_path}") if os.path.exists(water_theme_path): try: # DEINE funktionierende Methode: root.tk.call("source", water_theme_path) root.tk.call("set_theme", "light") print("Successfully applied water theme with set_theme light") return True except tk.TclError as e: print(f"Theme loading failed: {e}") # Fallback: Versuche ohne set_theme try: root.tk.call("source", water_theme_path) style = ttk.Style() available_themes = style.theme_names() print(f"Available themes: {available_themes}") # Versuche verschiedene Theme-Namen for theme_name in ["water", "Water", "light", "awlight"]: if theme_name in available_themes: style.theme_use(theme_name) print(f"Applied theme: {theme_name}") return True except Exception as e2: print(f"Fallback theme loading failed: {e2}") else: print(f"Theme file not found: {water_theme_path}") print(f"Current working directory: {os.getcwd()}") print(f"Theme directory exists: {os.path.exists(theme_dir)}") if os.path.exists(theme_dir): print(f"Files in theme directory: {os.listdir(theme_dir)}") # System theme fallback try: style = ttk.Style() if "clam" in style.theme_names(): style.theme_use("clam") print("Using fallback theme: clam") return True except: pass except Exception as e: print(f"Theme loading completely failed: {e}") return False # ---------------------------- # Image Manager Class (korrigiert) # ---------------------------- class ImageManager: def __init__(self): self.images = {} def load_image(self, icon_key, fallback_paths=None): """Load PNG image using tk.PhotoImage with fallback options""" if icon_key in self.images: return self.images[icon_key] # Get primary path from config primary_path = LXToolsAppConfig.get_icon_path(icon_key) paths_to_try = [] if primary_path: paths_to_try.append(primary_path) # Add fallback paths if fallback_paths: paths_to_try.extend(fallback_paths) # Try to load image from paths for path in paths_to_try: try: if os.path.exists(path): photo = tk.PhotoImage(file=path) self.images[icon_key] = photo print(f"Successfully loaded image: {path}") return photo except tk.TclError as e: print(f"Failed to load image from {path}: {e}") continue # Return None if no image found print(f"No image found for key: {icon_key}") return None # ---------------------------- # OS Detection Class (korrigiert) # ---------------------------- class OSDetector: OS_DETECTION = [ ("mint", "Linux Mint"), ("pop", "Pop!_OS"), ("manjaro", "Manjaro"), ("garuda", "Garuda Linux"), ("endeavouros", "EndeavourOS"), ("fedora", "Fedora"), ("tumbleweed", "SUSE Tumbleweed"), ("leap", "SUSE Leap"), ("suse", "openSUSE"), ("arch", "Arch Linux"), ("ubuntu", "Ubuntu"), ("debian", "Debian"), ] @staticmethod def detect_os(): """Detect operating system using ordered list""" try: with open("/etc/os-release", "r") as f: content = f.read().lower() # Check each OS in order (specific first) for keyword, os_name in OSDetector.OS_DETECTION: if keyword in content: return os_name return "Unknown System" except FileNotFoundError: return "File not found" # ---------------------------- # Network Checker Class (korrigiert) # ---------------------------- class NetworkChecker: @staticmethod def check_internet(host="8.8.8.8", port=53, timeout=3): # ← Korrigierter Name """Check if internet connection is available""" try: socket.setdefaulttimeout(timeout) socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) return True except socket.error: return False @staticmethod def check_repository(url="https://git.ilunix.de"): # ← Korrigierter Name """Check if repository is accessible""" try: urllib.request.urlopen(url, timeout=5) return True except: return False # ---------------------------- # App Manager Class (korrigiert) # ---------------------------- class AppManager: def __init__(self): self.projects = LXToolsAppConfig.PROJECTS def get_all_projects(self): """Get all project configurations""" return self.projects def get_project_info(self, project_key): """Get project information by key""" return self.projects.get(project_key) def is_installed(self, project_key): """Check if project is installed""" if project_key == "wirepy": return os.path.exists("/usr/local/bin/wirepy") elif project_key == "logviewer": return os.path.exists("/usr/local/bin/logviewer") else: return os.path.exists(f"/usr/local/bin/{project_key}") def get_installed_version(self, project_key): """Get installed version from config file""" try: if project_key == "wirepy": config_file = ( "/usr/lib/python3/dist-packages/shared_libs/wp_app_config.py" ) elif project_key == "logviewer": config_file = ( "/usr/lib/python3/dist-packages/shared_libs/logview_app_config.py" ) else: config_file = f"/usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py" if os.path.exists(config_file): with open(config_file, "r") as f: content = f.read() for line in content.split("\n"): if "VERSION" in line and "=" in line: version = line.split("=")[1].strip().strip("\"'") return version return "Unknown" except Exception as e: print(f"Error getting version for {project_key}: {e}") return "Unknown" def get_latest_version(self, project_key): """Get latest version from API - KORRIGIERT""" try: project_info = self.get_project_info(project_key) if not project_info: return "Unknown" with urllib.request.urlopen( project_info["api_url"], timeout=10 ) as response: data = json.loads(response.read().decode()) if data and len(data) > 0: latest_version = data[0].get("tag_name", "Unknown") return latest_version.lstrip("v") return "Unknown" # ← FIX: Korrigierte Syntax except Exception as e: print(f"API Error for {project_key}: {e}") return "Unknown" # ← FIX: Korrigierte Syntax # ---------------------------- # Installation Manager Class (korrigiert) # ---------------------------- class InstallationManager: def __init__( self, app_manager, progress_callback=None, icon_callback=None, debug_callback=None, ): self.app_manager = app_manager self.progress_callback = progress_callback self.icon_callback = icon_callback self.debug_callback = debug_callback def install_project(self, project_key): """Install any project generically""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"Unknown project: {project_key}") self.update_progress(f"Starting {project_info['name']} installation...") self.update_icon("downloading") try: with tempfile.TemporaryDirectory() as temp_dir: # Download project self.update_progress(f"Downloading {project_info['name']}...") if not self._download_and_extract( project_info["download_url"], temp_dir ): raise Exception(f"Failed to download {project_info['name']}") # Download shared libs self.update_progress("Downloading shared libraries...") shared_temp = os.path.join(temp_dir, "shared") if not self._download_and_extract( LXToolsAppConfig.SHARED_LIBS_URL, shared_temp ): raise Exception("Failed to download shared libraries") # Create installation script self.update_progress("Preparing installation...") script_path = self._create_install_script( project_key, project_info, temp_dir ) # Execute installation self.update_progress("Installing...") self._execute_install_script(script_path) self.update_progress(f"{project_info['name']} installation completed!") self.update_icon("success") return True except Exception as e: self.update_icon("error") raise Exception(f"Installation failed: {e}") def _download_and_extract(self, url, extract_to): """Download and extract ZIP file""" try: with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: urllib.request.urlretrieve(url, tmp_file.name) with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: zip_ref.extractall(extract_to) os.unlink(tmp_file.name) return True except Exception as e: self.debug_log(f"Download failed: {e}") return False def _create_install_script(self, project_key, project_info, temp_dir): """Create installation script for any project""" with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: project_source = os.path.join(temp_dir, project_info["archive_folder"]) shared_source = os.path.join(temp_dir, "shared", "shared_libs") if project_key == "wirepy": script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Installation Script ===" # Create directories mkdir -p /usr/lib/python3/dist-packages/shared_libs mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications mkdir -p /usr/local/etc/ssl mkdir -p /usr/share/polkit-1/actions # Install shared libraries echo "Installing shared libraries..." if [ -d "{shared_source}" ]; then cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true fi # Install Wire-Py files echo "Installing Wire-Py files..." cp -f "{project_source}/wirepy.py" /usr/local/bin/ cp -f "{project_source}/start_wg.py" /usr/local/bin/ cp -f "{project_source}/ssl_encrypt.py" /usr/local/bin/ cp -f "{project_source}/ssl_decrypt.py" /usr/local/bin/ cp -f "{project_source}/match_found.py" /usr/local/bin/ cp -f "{project_source}/tunnel.py" /usr/local/bin/ 2>/dev/null || true # Make executable chmod 755 /usr/local/bin/wirepy.py chmod 755 /usr/local/bin/start_wg.py chmod 755 /usr/local/bin/ssl_encrypt.py chmod 755 /usr/local/bin/ssl_decrypt.py chmod 755 /usr/local/bin/match_found.py chmod 755 /usr/local/bin/tunnel.py 2>/dev/null || true # Install config cp -f "{project_source}/wp_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ # Install icons echo "Installing icons..." if [ -d "{project_source}/lx-icons" ]; then cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ fi # Install desktop file if [ -f "{project_source}/Wire-Py.desktop" ]; then cp -f "{project_source}/Wire-Py.desktop" /usr/share/applications/ fi # Install policy file if [ -f "{project_source}/org.sslcrypt.policy" ]; then cp -f "{project_source}/org.sslcrypt.policy" /usr/share/polkit-1/actions/ fi # Install language files echo "Installing language files..." if [ -d "{project_source}/languages/de" ]; then cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Create symlink rm -f /usr/local/bin/wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy # Create SSL key if not exists if [ ! -f "/usr/local/etc/ssl/pwgk.pem" ]; then echo "Creating SSL key..." openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 2>/dev/null || echo "Warning: SSL key creation failed" chmod 600 /usr/local/etc/ssl/pwgk.pem 2>/dev/null || true fi echo "=== {project_info['name']} installation completed ===" """ elif project_key == "logviewer": script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Installation Script ===" # Create directories mkdir -p /usr/lib/python3/dist-packages/shared_libs mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications # Install shared libraries echo "Installing shared libraries..." if [ -d "{shared_source}" ]; then cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true fi # Install LogViewer echo "Installing LogViewer..." cp -f "{shared_source}/logviewer.py" /usr/local/bin/ chmod 755 /usr/local/bin/logviewer.py # Install config cp -f "{shared_source}/logview_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ # Install icons (if available) if [ -d "{shared_source}/lx-icons" ]; then cp -rf "{shared_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true fi # Install desktop file (if available) if [ -f "{shared_source}/LogViewer.desktop" ]; then cp -f "{shared_source}/LogViewer.desktop" /usr/share/applications/ fi # Install language files (if available) if [ -d "{shared_source}/languages/de" ]; then cp -f "{shared_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Create symlink rm -f /usr/local/bin/logviewer ln -sf /usr/local/bin/logviewer.py /usr/local/bin/logviewer echo "=== {project_info['name']} installation completed ===" """ else: # Generisches Script für zukünftige Projekte script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Installation Script ===" # Create directories mkdir -p /usr/lib/python3/dist-packages/shared_libs mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications # Install shared libraries echo "Installing shared libraries..." if [ -d "{shared_source}" ]; then cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true fi # Install project files (generic approach) echo "Installing {project_info['name']} files..." if [ -f "{project_source}/{project_key}.py" ]; then cp -f "{project_source}/{project_key}.py" /usr/local/bin/ chmod 755 /usr/local/bin/{project_key}.py # Create symlink rm -f /usr/local/bin/{project_key} ln -sf /usr/local/bin/{project_key}.py /usr/local/bin/{project_key} fi # Install config (if exists) if [ -f "{project_source}/{project_key}_app_config.py" ]; then cp -f "{project_source}/{project_key}_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ fi # Install icons (if available) if [ -d "{project_source}/lx-icons" ]; then cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true fi # Install desktop file (if available) if [ -f "{project_source}/{project_info['name']}.desktop" ]; then cp -f "{project_source}/{project_info['name']}.desktop" /usr/share/applications/ fi # Install language files (if available) if [ -d "{project_source}/languages/de" ]; then cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi echo "=== {project_info['name']} installation completed ===" """ f.write(script_content) script_path = f.name # Make script executable os.chmod(script_path, 0o755) self.debug_log(f"Created install script: {script_path}") return script_path def _execute_install_script(self, script_path): """Execute installation script with pkexec""" try: result = subprocess.run( ["pkexec", "bash", script_path], capture_output=True, text=True, timeout=120, ) if result.returncode != 0: error_msg = f"Installation script failed: {result.stderr}" self.debug_log(f"ERROR: {error_msg}") self.debug_log(f"STDOUT: {result.stdout}") raise Exception(error_msg) self.debug_log("Installation script output:") self.debug_log(result.stdout) except subprocess.TimeoutExpired: raise Exception("Installation timed out") except subprocess.CalledProcessError as e: raise Exception(f"Installation script failed: {e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) self.debug_log(f"Progress: {message}") def update_icon(self, status): if self.icon_callback: self.icon_callback(status) def debug_log(self, message): if self.debug_callback: self.debug_callback(message) print(message) # ---------------------------- # Uninstallation Manager Class (korrigiert) # ---------------------------- class UninstallationManager: def __init__(self, app_manager, progress_callback=None, debug_callback=None): self.app_manager = app_manager self.progress_callback = progress_callback self.debug_callback = debug_callback def uninstall_project(self, project_key): """Uninstall any project generically""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"Unknown project: {project_key}") if not self.app_manager.is_installed(project_key): raise Exception(f"{project_info['name']} is not installed.") self.update_progress(f"Starting {project_info['name']} uninstallation...") try: # Create uninstallation script script_path = self._create_uninstall_script(project_key, project_info) # Execute uninstallation self.update_progress("Executing uninstallation...") self._execute_uninstall_script(script_path) # Remove user config directories self._cleanup_user_files(project_key) self.update_progress(f"{project_info['name']} uninstalled successfully!") return True except Exception as e: self.update_progress(f"Error during uninstallation: {str(e)}") raise def _create_uninstall_script(self, project_key, project_info): """Create uninstallation script""" with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: if project_key == "wirepy": script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Uninstallation Script ===" # Remove Wire-Py files rm -f /usr/local/bin/wirepy.py rm -f /usr/local/bin/start_wg.py rm -f /usr/local/bin/ssl_encrypt.py rm -f /usr/local/bin/ssl_decrypt.py rm -f /usr/local/bin/match_found.py rm -f /usr/local/bin/tunnel.py rm -f /usr/local/bin/wirepy # Remove config rm -f /usr/lib/python3/dist-packages/shared_libs/wp_app_config.py # Remove desktop file rm -f /usr/share/applications/Wire-Py.desktop # Remove policy file rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo # Check if other projects are still installed OTHER_PROJECTS_INSTALLED=false if [ -f "/usr/local/bin/logviewer" ]; then OTHER_PROJECTS_INSTALLED=true fi # Remove shared resources only if no other projects if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then echo "No other LX projects found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/local/etc/ssl rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true fi echo "=== {project_info['name']} uninstallation completed ===" """ elif project_key == "logviewer": script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Uninstallation Script ===" # Remove LogViewer files rm -f /usr/local/bin/logviewer.py rm -f /usr/local/bin/logviewer # Remove config rm -f /usr/lib/python3/dist-packages/shared_libs/logview_app_config.py # Remove desktop file rm -f /usr/share/applications/LogViewer.desktop # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo # Check if other projects are still installed OTHER_PROJECTS_INSTALLED=false if [ -f "/usr/local/bin/wirepy" ]; then OTHER_PROJECTS_INSTALLED=true fi # Remove shared resources only if no other projects if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then echo "No other LX projects found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true fi echo "=== {project_info['name']} uninstallation completed ===" """ else: # Generisches Uninstall-Script script_content = f"""#!/bin/bash set -e echo "=== {project_info['name']} Uninstallation Script ===" # Remove project files rm -f /usr/local/bin/{project_key}.py rm -f /usr/local/bin/{project_key} # Remove config rm -f /usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py # Remove desktop file rm -f /usr/share/applications/{project_info['name']}.desktop echo "=== {project_info['name']} uninstallation completed ===" """ f.write(script_content) script_path = f.name # Make script executable os.chmod(script_path, 0o755) self.debug_log(f"Created uninstall script: {script_path}") return script_path def _execute_uninstall_script(self, script_path): """Execute uninstallation script with pkexec""" try: result = subprocess.run( ["pkexec", "bash", script_path], capture_output=True, text=True, timeout=60, ) if result.returncode != 0: error_msg = f"Uninstallation script failed: {result.stderr}" self.debug_log(f"ERROR: {error_msg}") self.debug_log(f"STDOUT: {result.stdout}") raise Exception(error_msg) self.debug_log("Uninstallation script output:") self.debug_log(result.stdout) except subprocess.TimeoutExpired: raise Exception("Uninstallation timed out") except subprocess.CalledProcessError as e: raise Exception(f"Uninstallation script failed: {e}") def _cleanup_user_files(self, project_key): """Clean up user configuration files""" try: if project_key == "wirepy": config_dir = os.path.expanduser("~/.config/wire_py") log_file = os.path.expanduser("~/.local/share/lxlogs/wirepy.log") elif project_key == "logviewer": config_dir = os.path.expanduser("~/.config/logviewer") log_file = os.path.expanduser("~/.local/share/lxlogs/logviewer.log") else: config_dir = os.path.expanduser(f"~/.config/{project_key}") log_file = os.path.expanduser( f"~/.local/share/lxlogs/{project_key}.log" ) # Remove user config directory if os.path.exists(config_dir): shutil.rmtree(config_dir) self.debug_log(f"Removed user config: {config_dir}") # Remove log file if os.path.exists(log_file): os.remove(log_file) self.debug_log(f"Removed log file: {log_file}") except Exception as e: self.debug_log(f"Warning: Could not clean up user files: {e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) self.debug_log(f"Progress: {message}") def debug_log(self, message): if self.debug_callback: self.debug_callback(message) print(message) # ---------------------------- # Main GUI Application Class (korrigiert) # ---------------------------- class LXToolsGUI: def __init__(self): self.root = None self.progress_label = None self.download_icon_label = None self.project_var = None self.status_labels = {} self.version_labels = {} self.debug_text = None self.show_debug = False # Initialize managers self.app_manager = AppManager() self.installation_manager = InstallationManager( self.app_manager, self.update_progress, self.update_download_icon, self.debug_log, ) self.uninstallation_manager = UninstallationManager( self.app_manager, self.update_progress, self.debug_log ) self.image_manager = ImageManager() # Detect OS self.detected_os = OSDetector.detect_os() def create_gui(self): """Create the main GUI""" self.root = tk.Tk() self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") # Set window size window_height = ( LXToolsAppConfig.DEBUG_WINDOW_HEIGHT if self.show_debug else LXToolsAppConfig.WINDOW_HEIGHT ) self.root.geometry(f"{LXToolsAppConfig.WINDOW_WIDTH}x{window_height}") # Apply theme ThemeManager.apply_light_theme(self.root) # Configure main grid self.root.grid_columnconfigure(0, weight=1) self.root.grid_rowconfigure(0, weight=0) # Header self.root.grid_rowconfigure(1, weight=0) # OS Info self.root.grid_rowconfigure(2, weight=1) # Projects self.root.grid_rowconfigure(3, weight=0) # Progress self.root.grid_rowconfigure(4, weight=0) # Buttons if self.show_debug: self.root.grid_rowconfigure(5, weight=1) # Debug # Create GUI sections self._create_header_section() self._create_os_info_section() self._create_projects_section() self._create_progress_section() self._create_buttons_section() if self.show_debug: self._create_debug_section() # Load app icon self._load_app_icon() # Center window if USE_LXTOOLS: center_window_cross_platform(self.root) else: IntegratedLxTools.center_window_cross_platform(self.root) # Initial status refresh self.root.after(100, self.refresh_status) return self.root def _create_header_section(self): """Create header section with title and version""" header_frame = ttk.Frame(self.root, padding=15) header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) header_frame.grid_columnconfigure(0, weight=1) # Title title_label = tk.Label( header_frame, text=f"🚀 {LXToolsAppConfig.APP_NAME}", font=("Helvetica", 16, "bold"), fg="#2E86AB", ) title_label.grid(row=0, column=0, sticky="w") # Version version_label = tk.Label( header_frame, text=f"Version {LXToolsAppConfig.VERSION}", font=("Helvetica", 10), fg="gray", ) version_label.grid(row=1, column=0, sticky="w") def _create_os_info_section(self): """Create OS information section""" os_frame = ttk.LabelFrame(self.root, text="System Information", padding=10) os_frame.grid(row=1, column=0, sticky="ew", padx=15, pady=10) os_frame.grid_columnconfigure(1, weight=1) # OS Detection tk.Label(os_frame, text="Detected OS:", font=("Helvetica", 10, "bold")).grid( row=0, column=0, sticky="w", padx=(0, 10) ) tk.Label(os_frame, text=self.detected_os, font=("Helvetica", 10)).grid( row=0, column=1, sticky="w" ) # Working Directory tk.Label(os_frame, text="Working Dir:", font=("Helvetica", 10, "bold")).grid( row=1, column=0, sticky="w", padx=(0, 10) ) tk.Label(os_frame, text=LXToolsAppConfig.WORK_DIR, font=("Helvetica", 9)).grid( row=1, column=1, sticky="w" ) def _create_projects_section(self): """Create projects selection and status section""" projects_frame = ttk.LabelFrame( self.root, text="Available Projects", padding=10 ) projects_frame.grid(row=2, column=0, sticky="nsew", padx=15, pady=(0, 10)) projects_frame.grid_columnconfigure(0, weight=1) projects_frame.grid_rowconfigure(1, weight=1) # Project selection selection_frame = ttk.Frame(projects_frame) selection_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) selection_frame.grid_columnconfigure(1, weight=1) tk.Label( selection_frame, text="Select Project:", font=("Helvetica", 10, "bold") ).grid(row=0, column=0, sticky="w", padx=(0, 10)) self.project_var = tk.StringVar() project_combo = ttk.Combobox( selection_frame, textvariable=self.project_var, values=list(self.app_manager.get_all_projects().keys()), state="readonly", width=20, ) project_combo.grid(row=0, column=1, sticky="w") project_combo.bind("<>", self._on_project_selected) # Projects status frame with scrollbar status_container = ttk.Frame(projects_frame) status_container.grid(row=1, column=0, sticky="nsew") status_container.grid_columnconfigure(0, weight=1) status_container.grid_rowconfigure(0, weight=1) # Canvas for scrolling canvas = tk.Canvas(status_container, height=200) scrollbar = ttk.Scrollbar( status_container, orient="vertical", command=canvas.yview ) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") # Project status entries scrollable_frame.grid_columnconfigure(1, weight=1) row = 0 for project_key, project_info in self.app_manager.get_all_projects().items(): # Project icon and name project_frame = ttk.Frame(scrollable_frame, padding=5) project_frame.grid(row=row, column=0, columnspan=3, sticky="ew", pady=2) project_frame.grid_columnconfigure(1, weight=1) # Try to load project icon icon = self.image_manager.load_image(project_info["icon_key"]) if icon: icon_label = tk.Label(project_frame, image=icon) icon_label.image = icon # Keep reference icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10)) else: # Fallback emoji emoji_map = {"wirepy": "🔐", "logviewer": "📋"} emoji = emoji_map.get(project_key, "📦") tk.Label(project_frame, text=emoji, font=("Helvetica", 16)).grid( row=0, column=0, rowspan=2, padx=(0, 10) ) # Project name and description tk.Label( project_frame, text=f"{project_info['name']} - {project_info['description']}", font=("Helvetica", 11, "bold"), ).grid(row=0, column=1, sticky="w") # Status label status_label = tk.Label( project_frame, text="Checking...", font=("Helvetica", 9) ) status_label.grid(row=1, column=1, sticky="w") self.status_labels[project_key] = status_label # Version label version_label = tk.Label(project_frame, text="", font=("Helvetica", 9)) version_label.grid(row=2, column=1, sticky="w") self.version_labels[project_key] = version_label row += 1 def _create_progress_section(self): """Create progress section with download icon""" progress_frame = ttk.LabelFrame(self.root, text="Progress", padding=10) progress_frame.grid(row=3, column=0, sticky="ew", padx=15, pady=(0, 10)) progress_frame.grid_columnconfigure(1, weight=1) # Download icon self.download_icon_label = tk.Label(progress_frame, text="", width=4) self.download_icon_label.grid(row=0, column=0, padx=(0, 10)) # Progress text self.progress_label = tk.Label( progress_frame, text="Ready for installation...", font=("Helvetica", 10), fg="blue", anchor="w", ) self.progress_label.grid(row=0, column=1, sticky="ew") # Reset download icon self._reset_download_icon() def _create_buttons_section(self): """Create buttons section""" buttons_frame = ttk.Frame(self.root, padding=10) buttons_frame.grid(row=4, column=0, sticky="ew", padx=15, pady=(0, 10)) buttons_frame.grid_columnconfigure(0, weight=1) # Button container btn_container = ttk.Frame(buttons_frame) btn_container.grid(row=0, column=0) # Install/Update button install_btn = ttk.Button( btn_container, text="📥 Install/Update", command=self.install_project, width=15, ) install_btn.grid(row=0, column=0, padx=(0, 10)) # Uninstall button uninstall_btn = ttk.Button( btn_container, text="🗑️ Uninstall", command=self.uninstall_project, width=15 ) uninstall_btn.grid(row=0, column=1, padx=(0, 10)) # Refresh button refresh_btn = ttk.Button( btn_container, text="Refresh", command=self.refresh_status, width=15 ) refresh_btn.grid(row=0, column=2, padx=(0, 10)) # Debug toggle button debug_btn = ttk.Button( btn_container, text="Debug", command=self.toggle_debug, width=15 ) debug_btn.grid(row=0, column=3) def _create_debug_section(self): """Create debug section""" debug_frame = ttk.LabelFrame(self.root, text="Debug Output", padding=10) debug_frame.grid(row=5, column=0, sticky="nsew", padx=15, pady=(0, 10)) debug_frame.grid_columnconfigure(0, weight=1) debug_frame.grid_rowconfigure(0, weight=1) # Debug text with scrollbar debug_container = ttk.Frame(debug_frame) debug_container.grid(row=0, column=0, sticky="nsew") debug_container.grid_columnconfigure(0, weight=1) debug_container.grid_rowconfigure(0, weight=1) self.debug_text = tk.Text( debug_container, height=8, font=("Courier", 9), bg="#f8f8f8", fg="#333333" ) self.debug_text.grid(row=0, column=0, sticky="nsew") # Scrollbar for debug text debug_scrollbar = ttk.Scrollbar( debug_container, orient="vertical", command=self.debug_text.yview ) debug_scrollbar.grid(row=0, column=1, sticky="ns") self.debug_text.configure(yscrollcommand=debug_scrollbar.set) # Clear debug button clear_debug_btn = ttk.Button( debug_frame, text="Clear Debug", command=self.clear_debug ) clear_debug_btn.grid(row=1, column=0, sticky="e", pady=(5, 0)) def _load_app_icon(self): """Load application icon""" try: icon_path = LXToolsAppConfig.get_icon_path("app_icon") if icon_path and os.path.exists(icon_path): icon = tk.PhotoImage(file=icon_path) self.root.iconphoto(False, icon) print(f"App icon loaded: {icon_path}") else: print("App icon not found, using default") except Exception as e: print(f"Failed to load app icon: {e}") def _reset_download_icon(self): """Reset download icon to neutral state""" icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: # Fallback to emoji self.download_icon_label.config(text="📥", font=("Helvetica", 16)) def _on_project_selected(self, event=None): """Handle project selection change""" selected = self.project_var.get() if selected: self.update_progress(f"Selected: {selected}") def update_download_icon(self, status): """Update download icon based on status""" if not self.download_icon_label: return if status == "downloading": icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="⬇️", font=("Helvetica", 16)) elif status == "error": icon = self.image_manager.load_image("download_error_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="❌", font=("Helvetica", 16)) elif status == "success": icon = self.image_manager.load_image("success_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="✅", font=("Helvetica", 16)) self.download_icon_label.update() def update_progress(self, message): """Update progress message""" if self.progress_label: self.progress_label.config(text=message) self.progress_label.update() print(f"Progress: {message}") def debug_log(self, message): """Add message to debug log""" if self.debug_text: self.debug_text.insert(tk.END, f"{message}\n") self.debug_text.see(tk.END) self.debug_text.update() print(f"Debug: {message}") def clear_debug(self): """Clear debug text""" if self.debug_text: self.debug_text.delete(1.0, tk.END) def toggle_debug(self): """Toggle debug window visibility""" self.show_debug = not self.show_debug # Recreate window with new size if self.root: self.root.destroy() # Create new window self.create_gui() self.root.mainloop() def refresh_status(self): """Refresh application status and version information""" self.update_progress("Refreshing status and checking versions...") self._reset_download_icon() for project_key, project_info in self.app_manager.get_all_projects().items(): status_label = self.status_labels[project_key] version_label = self.version_labels[project_key] if self.app_manager.is_installed(project_key): installed_version = self.app_manager.get_installed_version(project_key) status_label.config( text=f"✅ Installed (v{installed_version})", fg="green" ) # Get latest version from API try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": if installed_version != latest_version: version_label.config( text=f"Latest: v{latest_version} (Update available)", fg="orange", ) else: version_label.config( text=f"Latest: v{latest_version} (Up to date)", fg="green", ) else: version_label.config(text=f"Latest: Unknown", fg="gray") except Exception as e: version_label.config(text=f"Latest: Check failed", fg="gray") self.debug_log(f"Version check failed for {project_key}: {e}") else: status_label.config(text=f"❌ Not installed", fg="red") # Still show latest available version try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": version_label.config( text=f"Available: v{latest_version}", fg="blue" ) else: version_label.config(text=f"Available: Unknown", fg="gray") except Exception as e: version_label.config(text=f"Available: Check failed", fg="gray") self.debug_log(f"Version check failed for {project_key}: {e}") self.update_progress("Status refresh completed.") def install_project(self): """Handle install button click""" selected_project = self.project_var.get() if not selected_project: messagebox.showwarning("Warning", "Please select a project to install.") return # Check internet connection if not NetworkChecker.check_internet(): self.update_download_icon("error") messagebox.showerror( "Network Error", "No internet connection available.\nPlease check your network connection.", ) return if not NetworkChecker.check_repository(): self.update_download_icon("error") messagebox.showerror( "Repository Error", "Cannot access repository.\nPlease try again later." ) return # Reset download icon self._reset_download_icon() project_info = self.app_manager.get_project_info(selected_project) # Check if already installed if self.app_manager.is_installed(selected_project): installed_version = self.app_manager.get_installed_version(selected_project) latest_version = self.app_manager.get_latest_version(selected_project) dialog_text = ( f"{project_info['name']} is already installed.\n\n" f"Installed version: v{installed_version}\n" f"Latest version: v{latest_version}\n\n" f"YES = Update (reinstall all files)\n" f"NO = Uninstall\n" f"Cancel = Do nothing" ) result = messagebox.askyesnocancel( f"{project_info['name']} already installed", dialog_text ) if result is None: # Cancel self.update_progress("Installation cancelled.") return elif not result: # Uninstall self.uninstall_project(selected_project) return else: # Update self.update_progress("Updating application...") try: self.installation_manager.install_project(selected_project) # Success message if USE_LXTOOLS: LxTools.msg_window( self.root, "Success", f"{project_info['name']} has been successfully installed/updated.", "info", ) else: messagebox.showinfo( "Success", f"{project_info['name']} has been successfully installed/updated.", ) self.refresh_status() except Exception as e: # Show error icon self.update_download_icon("error") error_msg = f"Installation failed: {e}" self.debug_log(f"ERROR: {error_msg}") if USE_LXTOOLS: LxTools.msg_window( self.root, "Error", error_msg, "error", width=500, height=300 ) else: messagebox.showerror("Error", error_msg) def uninstall_project(self, project_key=None): """Handle uninstall button click""" if project_key is None: project_key = self.project_var.get() if not project_key: messagebox.showwarning("Warning", "Please select a project to uninstall.") return project_info = self.app_manager.get_project_info(project_key) if not self.app_manager.is_installed(project_key): messagebox.showinfo("Info", f"{project_info['name']} is not installed.") return result = messagebox.askyesno( "Confirm Uninstall", f"Are you sure you want to uninstall {project_info['name']}?\n\n" f"This will remove all application files and user configurations.", ) if not result: return try: self.uninstallation_manager.uninstall_project(project_key) # Success message if USE_LXTOOLS: LxTools.msg_window( self.root, "Success", f"{project_info['name']} has been successfully uninstalled.", "info", ) else: messagebox.showinfo( "Success", f"{project_info['name']} has been successfully uninstalled.", ) self.refresh_status() except Exception as e: error_msg = f"Uninstallation failed: {e}" self.debug_log(f"ERROR: {error_msg}") if USE_LXTOOLS: LxTools.msg_window( self.root, "Error", error_msg, "error", width=500, height=300 ) else: messagebox.showerror("Error", error_msg) def run(self): """Start the GUI application""" try: print(f"Starting {LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") print(f"Working directory: {LXToolsAppConfig.WORK_DIR}") print(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}") print(f"Using LxTools: {USE_LXTOOLS}") root = self.create_gui() root.mainloop() except KeyboardInterrupt: print("\nApplication interrupted by user.") except Exception as e: print(f"Fatal error: {e}") if self.root: messagebox.showerror("Fatal Error", f"Application failed to start: {e}") # ---------------------------- # Main Application Entry Point # ---------------------------- def main(): """Main function to start the application""" try: # Check if running as root (not recommended) if os.geteuid() == 0: print("Warning: Running as root is not recommended!") print("The installer will use pkexec for privilege escalation when needed.") # Create and run the GUI app = LXToolsGUI() app.run() except KeyboardInterrupt: print("\nApplication interrupted by user.") except Exception as e: print(f"Fatal error: {e}") try: import tkinter.messagebox as mb mb.showerror("Fatal Error", f"Application failed to start: {e}") except: pass # If even tkinter fails, just print if __name__ == "__main__": main()