From ed269af1d27b534e2f7963be523f639e8b78052b Mon Sep 17 00:00:00 2001 From: punix Date: Mon, 23 Jun 2025 14:44:33 +0200 Subject: [PATCH] add requests for arch linux --- ensure_modules.py | 68 ----- lxtools_installer.py | 126 ++++---- manager.py | 665 +++++++++++++++++++++++-------------------- 3 files changed, 431 insertions(+), 428 deletions(-) delete mode 100644 ensure_modules.py diff --git a/ensure_modules.py b/ensure_modules.py deleted file mode 100644 index 53419b4..0000000 --- a/ensure_modules.py +++ /dev/null @@ -1,68 +0,0 @@ -import sys -import os - -# ✅ Path to be added in the .pth file -SHARED_LIBS_PATH = "/usr/local/share/shared_libs" -PTH_FILE_NAME = "shared_libs.pth" - - -def ensure_shared_libs_pth_exists(): - """ - Checks if all site-packages directories have a `.pth` file with the correct path. - Creates or updates it if missing or incorrect. - """ - # Search for all site-packages directories (e.g., /usr/lib/python3.x/site-packages/) - for root, dirs, files in os.walk("/usr"): - if "site-packages" in dirs: - site_packages_dir = os.path.join(root, "site-packages") - - pth_file_path = os.path.join(site_packages_dir, PTH_FILE_NAME) - - # Check if the file exists and is correct - if not os.path.exists(pth_file_path): - print(f"⚠️ .pth file not found: {pth_file_path}. Creating...") - with open(pth_file_path, "w") as f: - f.write(SHARED_LIBS_PATH + "\n") - - else: - # Check if the correct path is in the file - with open(pth_file_path, "r") as f: - content = f.read().strip() - - if not content == SHARED_LIBS_PATH: - print(f"⚠️ .pth file exists but has incorrect content. Fixing...") - with open(pth_file_path, "w") as f: - f.write(SHARED_LIBS_PATH + "\n") - - print("✅ All .pth files checked and corrected.") - - -def main(): - try: - # Try to import the module - from shared_libs.wp_app_config import AppConfig - - print("✅ 'shared_libs' is correctly loaded. Starting the application...") - - # Your main program logic here... - except ModuleNotFoundError as e: - # Only handle errors related to missing .pth file - if "No module named 'shared_libs'" in str(e): - print("⚠️ Error: 'shared_libs' module not found. Checking .pth file...") - ensure_shared_libs_pth_exists() - - # Try again after fixing the .pth file - try: - from shared_libs.wp_app_config import AppConfig - - print("✅ After correcting the .pth file: Module loaded.") - # Your main program logic here... - except Exception as e2: - print(f"❌ Error after correcting the .pth file: {e2}") - else: - # For other errors, re-raise them - raise - - -if __name__ == "__main__": - main() diff --git a/lxtools_installer.py b/lxtools_installer.py index 9343d24..1986ae8 100755 --- a/lxtools_installer.py +++ b/lxtools_installer.py @@ -3,12 +3,13 @@ import tkinter as tk from tkinter import ttk import os import subprocess +import getpass from datetime import datetime import tempfile import urllib.request import zipfile from manager import ( - OSDetector, + Detector, Theme, LocaleStrings, LXToolsAppConfig, @@ -75,14 +76,27 @@ class InstallationManager: raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}") def _create_wirepy_install_script(self): + detected_os = Detector.get_os() + if detected_os == "Arch Linux": + result_unzip = Detector.get_unzip() + result_wget = Detector.get_wget() """Create Wire-Py installation script""" script = f"""#!/bin/bash set -e echo "=== Wire-Py Installation ===" - +if [ "{detected_os}" = "Arch Linux" ]; then + if [ "{result_unzip}" = "False" ]; then + pacman -S --noconfirm unzip + fi + + if [ "{result_wget}" = "False" ]; then + pacman -S --noconfirm wget + fi +fi +{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" # Create necessary directories -mkdir -p /usr/local/share/shared_libs +mkdir -p {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications @@ -118,7 +132,7 @@ done # Install config if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then - cp -f "$WIREPY_DIR/wp_app_config.py" /usr/local/share/shared_libs/ + cp -f "$WIREPY_DIR/wp_app_config.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed wp_app_config.py" fi @@ -126,15 +140,15 @@ fi echo "Installing shared libraries..." for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then - cp -f "$SHARED_DIR/$file" /usr/local/share/shared_libs/ + cp -f "$SHARED_DIR/$file" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then - cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ - chmod 755 /usr/local/share/shared_libs/logviewer.py + cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ + chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py echo "Installed logviewer.py (executable)" fi @@ -171,7 +185,7 @@ fi # Create symlink for Wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy # Create symlink for LogViewer -ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer +ln -sf {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py /usr/local/bin/logviewer echo "Created Wirepy and LogViewer symlink" # Install language files if available @@ -197,14 +211,32 @@ echo "Wire-Py installation completed!" return script def _create_logviewer_install_script(self): + detected_os = Detector.get_os() + if detected_os == "Arch Linux": + result_unzip = Detector.get_unzip() + result_wget = Detector.get_wget() + result_requests = Detector.get_requests() + """Create LogViewer installation script""" script = f"""#!/bin/bash set -e - +{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" echo "=== LogViewer Installation ===" +if [ "{detected_os}" = "Arch Linux" ]; then + if [ "{result_unzip}" = "False" ]; then + pacman -S --noconfirm unzip + fi + if [ "{result_wget}" = "False" ]; then + pacman -S --noconfirm wget + fi + + if [ "{result_requests}" = "False" ]; then + pacman -S --noconfirm python-requests + fi +fi # Create necessary directories -mkdir -p /usr/local/share/shared_libs +mkdir -p {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications @@ -244,36 +276,21 @@ fi echo "Installing shared libraries..." for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then - cp -f "$SHARED_DIR/$file" /usr/local/share/shared_libs/ + cp -f "$SHARED_DIR/$file" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then - cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ - chmod 755 /usr/local/share/shared_libs/logviewer.py + cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ + chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py echo "Installed logviewer.py (executable)" fi -# Create LogViewer desktop file -cat > /usr/share/applications/LogViewer.desktop << 'EOF' -[Desktop Entry] -Version=1.0 -Type=Application -Name=LogViewer -Comment=System Log Viewer -Exec=/usr/local/bin/logviewer -Icon=/usr/share/icons/lx-icons/48/log.png -Terminal=false -Categories=System;Utility; -StartupNotify=true -EOF - -echo "Created LogViewer desktop file" # Create symlink for LogViewer -ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer +ln -sf {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py /usr/local/bin/logviewer echo "Created LogViewer symlink" # Install language files if available @@ -393,8 +410,9 @@ class UninstallationManager: raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") def _create_wirepy_uninstall_script(self): + detected_os = Detector.get_os() """Create Wire-Py uninstallation script""" - script = """#!/bin/bash + script = f"""#!/bin/bash set -e echo "=== Wire-Py Uninstallation ===" @@ -411,7 +429,7 @@ rm -f /usr/local/bin/wirepy echo "Removed wirepy symlink" # Remove config -rm -f /usr/local/share/shared_libs/wp_app_config.py +rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/wp_app_config.py echo "Removed wp_app_config.py" # Remove desktop file @@ -427,13 +445,16 @@ rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo echo "Removed language files" # Remove user config directory -if [ -d "$HOME/.config/wire_py" ]; then - rm -rf "$HOME/.config/wire_py" +if [ -d /home/{getpass.getuser()}/.config/wire_py ]; then + rm -rf /home/{getpass.getuser()}/.config/wire_py echo "Removed user config directory" fi +# Remove ssl private key +rm -rf /usr/local/etc/ssl +echo "Removed ssl private key" # Remove log file -rm -f "$HOME/.local/share/lxlogs/wirepy.log" +rm -f /home/{getpass.getuser()}/.local/share/lxlogs/wirepy.log echo "Removed log file" # Check if LogViewer is still installed before removing shared resources @@ -441,15 +462,14 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes - rm -rf /usr/local/etc/ssl # Remove shared libs for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do - rm -f /usr/local/share/shared_libs/$file + rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done # Try to remove shared_libs directory if empty - rmdir /usr/local/share/shared_libs 2>/dev/null || true + rmdir {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} 2>/dev/null || true else echo "LogViewer still installed, keeping shared resources" fi @@ -459,8 +479,9 @@ echo "Wire-Py uninstallation completed!" return script def _create_logviewer_uninstall_script(self): + detected_os = Detector.get_os() """Create LogViewer uninstallation script""" - script = """#!/bin/bash + script = f"""#!/bin/bash set -e echo "=== LogViewer Uninstallation ===" @@ -469,22 +490,18 @@ echo "=== LogViewer Uninstallation ===" rm -f /usr/local/bin/logviewer echo "Removed logviewer symlink" -# Remove desktop file -rm -f /usr/share/applications/LogViewer.desktop -echo "Removed desktop file" - # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo echo "Removed language files" # Remove user config directory -if [ -d "$HOME/.config/logviewer" ]; then - rm -rf "$HOME/.config/logviewer" +if [ -d /home/{getpass.getuser()}/.config/logviewer ]; then + rm -rf /home/{getpass.getuser()}/.config/logviewer echo "Removed user config directory" fi # Remove log file -rm -f "$HOME/.local/share/lxlogs/logviewer.log" +rm -f /home/{getpass.getuser()}/.local/share/lxlogs/logviewer.log echo "Removed log file" # Check if Wire-Py is still installed before removing shared resources @@ -495,19 +512,19 @@ if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then # Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer) for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do - rm -f /usr/local/share/shared_libs/$file + rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done # Remove logviewer.py last - rm -f /usr/local/share/shared_libs/logviewer.py + rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py # Try to remove shared_libs directory if empty - rmdir /usr/local/share/shared_libs 2>/dev/null || true + rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true else echo "Wire-Py still installed, keeping shared resources" # Only remove logviewer-specific files - rm -f /usr/local/share/shared_libs/logview_app_config.py - rm -f /usr/local/share/shared_libs/logviewer.py + rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py + rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py fi echo "LogViewer uninstallation completed!" @@ -614,7 +631,7 @@ class LXToolsGUI: self.image_manager = Image() # Detect OS - self.detected_os = OSDetector.detect_os() + self.detected_os = Detector.get_os() # Color scheme self.colors = { @@ -746,8 +763,9 @@ class LXToolsGUI: # Checks: internet_ok = NetworkChecker.check_internet_connection() repo_ok = NetworkChecker.check_repository_access() + result = Detector.get_host_python_version() - if internet_ok and repo_ok: + if internet_ok and repo_ok and result is not None: self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green elif not internet_ok: self.update_header_status( @@ -757,6 +775,10 @@ class LXToolsGUI: self.update_header_status( LocaleStrings.MSGO["repo_unavailable"], "#f39c12" ) # Orange + elif result is None: + self.update_header_status( + LocaleStrings.MSGO["python_check"], "#e74c3c" + ) # Red else: self.update_header_status( LocaleStrings.MSGO["system_check"], "#3498db" diff --git a/manager.py b/manager.py index a62071c..3e3d3da 100644 --- a/manager.py +++ b/manager.py @@ -2,6 +2,7 @@ import locale import gettext import tkinter as tk from tkinter import ttk +from pathlib import Path import os import sys import shutil @@ -10,261 +11,9 @@ import stat from network import GiteaUpdate -class LXToolsAppConfig: - VERSION = "1.1.5" - APP_NAME = "lxtoolsinstaller" - WINDOW_WIDTH = 450 - WINDOW_HEIGHT = 580 - - # Working directory - WORK_DIR = os.getcwd() - ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") - THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") - - # Locale settings - LOCALE_DIR = "./locale/" - - # Download URLs - WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" - SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" - - # API URLs for version checking - WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" - SHARED_LIBS_API_URL = ( - "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" - ) - - # Project configurations - PROJECTS = { - "wirepy": { - "name": "Wire-Py", - "description": "WireGuard VPN Manager with GUI", - "download_url": WIREPY_URL, - "api_url": WIREPY_API_URL, - "icon_key": "icon_vpn", - "main_executable": "wirepy.py", - "symlink_name": "wirepy", - "config_file": "wp_app_config.py", - "desktop_file": "Wire-Py.desktop", - "policy_file": "org.sslcrypt.policy", - "requires_ssl": True, - }, - "logviewer": { - "name": "LogViewer", - "description": "System Log Viewer with GUI", - "download_url": SHARED_LIBS_URL, - "api_url": SHARED_LIBS_API_URL, - "icon_key": "icon_log", - "main_executable": "logviewer.py", - "symlink_name": "logviewer", - "config_file": "logview_app_config.py", - "desktop_file": "LogViewer.desktop", - "policy_file": None, - "requires_ssl": False, - }, - } - - # OS Detection List (order matters - specific first, generic last) - OS_DETECTION = [ - ("mint", "Linux Mint"), - ("pop", "Pop!_OS"), - ("manjaro", "Manjaro"), - ("garuda", "Garuda Linux"), - ("endeavouros", "EndeavourOS"), - ("fedora", "Fedora"), - ("tumbleweed", "SUSE Tumbleweed"), - ("leap", "SUSE Leap"), - ("arch", "Arch Linux"), - ("ubuntu", "Ubuntu"), - ("debian", "Debian"), - ] - - # Package manager commands for TKinter installation - TKINTER_INSTALL_COMMANDS = { - "Ubuntu": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Fedora": ["dnf", "install", "-y", "tkinter"], - "Arch Linux": ["pacman", "-S", "--noconfirm", "tk"], - "Manjaro": ["pacman", "-S", "--noconfirm", "tk"], - "Garuda Linux": ["pacman", "-S", "--noconfirm", "tk"], - "EndeavourOS": ["pacman", "-S", "--noconfirm", "tk"], - "SUSE Tumbleweed": ["zypper", "install", "-y", "python314-tk"], - "SUSE Leap": ["zypper", "install", "-y", "python312-tk"], - } - +class Detector: @staticmethod - def extract_data_files(): - if getattr(sys, "_MEIPASS", None) is not None: - # Liste der Quellordner (entspricht dem "datas"-Eintrag in lxtools_installer.spec) - source_dirs = [ - os.path.join(sys._MEIPASS, "locale"), # für locale/... - os.path.join(sys._MEIPASS, "TK-Themes"), # für TK-Themes/... - os.path.join(sys._MEIPASS, "lx-icons"), # für lx-icons/... - ] - - target_dir = os.path.abspath( - os.getcwd() - ) # Zielverzeichnis: aktueller Ordner - - for source_dir in source_dirs: - group_name = os.path.basename( - source_dir - ) # Erhält den Gruppen-Name (z. B. 'locale', 'TK-Themes') - - for root, dirs, files in os.walk(source_dir): - for file in files: - src_path = os.path.join(root, file) - - # Relativer Pfad innerhalb des Quellordners - rel_path_from_source_root = os.path.relpath( - src_path, source_dir - ) - - # Ziel-Pfad unter dem Gruppen-Ordner im aktuellen Verzeichnis - dst_path = os.path.join( - target_dir, group_name, rel_path_from_source_root - ) - - os.makedirs(os.path.dirname(dst_path), exist_ok=True) - shutil.copy2(src_path, dst_path) - - @staticmethod - def setup_translations() -> gettext.gettext: - """Initialize translations and set the translation function""" - try: - locale.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.textdomain(LXToolsAppConfig.APP_NAME) - except: - pass - return gettext.gettext - - -LXToolsAppConfig.extract_data_files() -# Initialize translations -_ = LXToolsAppConfig.setup_translations() - - -class LocaleStrings: - - MSGI = { - "refresh_and_check": _("Refreshing status and checking versions..."), - "start_install": _("Starting installation of "), - "install": _("Installing "), - "install_success": _(" installation successfully!"), - "install_failed": _("Installation failed: "), - "install_create": _("Created install script: "), - "install_script_failed": _("Installation script failed: "), - "install_timeout": _("Installation timed out"), - "installed": _("Installed "), - } - - MSGU = { - "uninstall": _("Uninstalling "), - "uninstall_success": _(" uninstalled successfully!"), - "uninstall_failed": _("Uninstallation failed: "), - "uninstall_create": _("Created uninstall script: "), - "uninstall_script_failed": _("Uninstallation script failed: "), - "uninstall_timeout": _("Uninstallation timed out"), - } - # MSGO = Other messages - MSGO = { - "unknown_project": _("Unknown project: "), - "not_install": _(" is not installed."), - "download_from": _("Downloading from "), - "extract_files": _("Extracting files..."), - "download_failed": _("Download failed: "), - "head_string2": _("System: "), - "head_string3": _("Linux App Installer"), - "ready": _("Ready for installation"), - "no_internet": _("No internet connection"), - "repo_unavailable": _("Repository unavailable"), - "system_check": _("System checking..."), - "applications": _("Applications"), - "progress": _("Progress"), - "refresh2": _("Status refresh completed"), - } - - # MSGC = Strings on Cards - MSGC = { - "checking": _("Checking..."), - "version_check": _("Version: Checking..."), - "latest": _("Latest: "), - "update_available": _("Update available "), - "up_to_date": _("Up to date"), - "latest_unknown": _("Latest unknown"), - "could_not_check": _("Could not check latest version"), - "check_last_failed": _("Latest: Check failed"), - "version_check_failed": _("Version check failed"), - "not_installed": _("Not installed"), - "available": _("Available "), - "available_unknown": _("Available unknown"), - "available_ckeck_failed": _("Available: Check failed"), - } - - # MSGL = Strings on Logmessages - MSGL = { - "selected_app": _("Selected project: "), - "log_name": _("Installation Log"), - "work_dir": _("Working directory: "), - "icons_dir": _("Icons directory: "), - "detected_os": _("Detected OS: "), - "log_cleared": _("Log cleared"), - "working_dir": _("Working directory: "), - "user_interuppt": _("\nApplication interrupted by user."), - "fatal_error": _("Fatal error: "), - "fatal_app_error": _("Fatal Error Application failed to start: "), - } - - # MSGB = Strings on Buttons - MSGB = { - "clear_log": _("Clear Log"), - "install": _("Install/Update"), - "uninstall": _("Uninstall"), - "refresh": _("Refresh Status"), - } - - # MSGM = String on MessagDialogs - MSGM = { - "please_select": _("Please select a project to install."), - "network_error": _( - "No internet connection available.\nPlease check your network connection.", - ), - "repo_error": _( - "Cannot access repository.\nPlease try again later.", - ), - "has_success_update": _("has been successfully installed/updated."), - "please_select_uninstall": _("Please select a project to uninstall."), - } - - # MSGP = Others print strings - MSGP = { - "tk_install": _("Installing tkinter for )"), - "command_string": _("Command: "), - "tk_success": _("TKinter installation completed successfully!"), - "tk_failed": _("TKinter installation failed: "), - "tk_timeout": _("TKinter installation timed out"), - "tk_install_error": _("Error installing tkinter: "), - "tk_command_error": _("No tkinter installation command defined for "), - "fail_load_image": _("Failed to load image from "), - "logviewer_check": _("LogViewer installation check:"), - "symlink_exist": _(" Symlink exists: "), - "executable_exist": _(" Executable exists: "), - "is_executable": _(" Is executable: "), - "final_result": _(" Final result: "), - "get_version_error": _("Error getting version for "), - } - - -class OSDetector: - @staticmethod - def detect_os(): + def get_os() -> str: """Detect operating system using ordered list""" try: with open("/etc/os-release", "r") as f: @@ -280,54 +29,77 @@ class OSDetector: return "File not found" @staticmethod - def check_tkinter_available(): - """Check if tkinter is available""" + def get_host_python_version() -> str: try: - import tkinter + result = subprocess.run( + ["python3", "--version"], capture_output=True, text=True, check=True + ) + version_str = result.stdout.strip().replace("Python ", "") + return version_str[:4] # example "3.13" + except Exception: + print("Python not found") + return None + @staticmethod + def get_user_gt_1() -> bool: + """This method may be required for the future if the number of users""" + path = Path("/home") + + user_directories = [ + entry + for entry in path.iterdir() + if entry.is_dir() and entry.name != "root" and entry.name != "lost+found" + ] + + # Count the number of user directories + numbers = len(user_directories) + if not numbers > 1: return True - except ImportError: + else: return False @staticmethod - def install_tkinter(): - """Install tkinter based on detected OS""" - detected_os = OSDetector.detect_os() + def get_wget() -> bool: + """Check if wget is installed""" - if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS: - commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os] - - print(f"{LocaleStrings.MSGP["tk_install"]}{detected_os}...") - print(f"{LocaleStrings.MSGP["command_string"]}{' '.join(commands)}") - - try: - # Use pkexec for privilege escalation - full_command = ["pkexec", "bash", "-c", " ".join(commands)] - result = subprocess.run( - full_command, capture_output=True, text=True, timeout=300 - ) - - if result.returncode == 0: - print(f"{LocaleStrings.MSGP["tk_succcess"]}") - return True - else: - print(f"{LocaleStrings.MSGP["tk_failed"]}{result.stderr}") - return False - - except subprocess.TimeoutExpired: - print(LocaleStrings.MSGP["tk_timeout"]) - return False - except Exception as e: - print(f"{LocaleStrings.MSGP['tk_install_error']}{str(e)}") - return False + result = subprocess.run( + ["which", "wget"], capture_output=True, text=True, check=False + ) + if result.returncode == 0: + return True + else: + return False + + @staticmethod + def get_unzip() -> bool: + """Check if wget is installed""" + + result = subprocess.run( + ["which", "unzip"], capture_output=True, text=True, check=False + ) + if result.returncode == 0: + return True + else: + return False + + @staticmethod + def get_requests() -> bool: + """Check if requests is installed""" + result = subprocess.run( + ["pacman", "-Qs", "python-requests"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + return True else: - print(f"{LocaleStrings.MSGP["tk_command_error"]}{detected_os}") return False class Theme: @staticmethod - def apply_light_theme(root): + def apply_light_theme(root) -> bool: """Apply light theme""" try: theme_dir = LXToolsAppConfig.THEMES_DIR @@ -358,41 +130,41 @@ class Theme: class System: @staticmethod - def create_directories(directories): + def create_directories(directories) -> None: """Create system directories using pkexec""" for directory in directories: subprocess.run(["pkexec", "mkdir", "-p", directory], check=True) @staticmethod - def copy_file(src, dest, make_executable=False): + def copy_file(src, dest, make_executable=False) -> None: """Copy file using pkexec""" subprocess.run(["pkexec", "cp", src, dest], check=True) if make_executable: subprocess.run(["pkexec", "chmod", "755", dest], check=True) @staticmethod - def copy_directory(src, dest): + def copy_directory(src, dest) -> None: """Copy directory using pkexec""" subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) @staticmethod - def remove_file(path): + def remove_file(path) -> None: """Remove file using pkexec""" subprocess.run(["pkexec", "rm", "-f", path], check=False) @staticmethod - def remove_directory(path): + def remove_directory(path) -> None: """Remove directory using pkexec""" subprocess.run(["pkexec", "rm", "-rf", path], check=False) @staticmethod - def create_symlink(target, link_name): + def create_symlink(target, link_name) -> None: """Create symbolic link using pkexec""" subprocess.run(["pkexec", "rm", "-f", link_name], check=False) subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True) @staticmethod - def create_ssl_key(pem_file): + def create_ssl_key(pem_file) -> bool: """Create SSL key using pkexec""" try: subprocess.run( @@ -408,7 +180,7 @@ class Image: def __init__(self): self.images = {} - def load_image(self, image_key, fallback_paths=None): + def load_image(self, image_key, fallback_paths=None) -> None | tk.PhotoImage: """Load PNG image using tk.PhotoImage with fallback options""" if image_key in self.images: return self.images[image_key] @@ -467,15 +239,16 @@ class AppManager: def __init__(self): self.projects = LXToolsAppConfig.PROJECTS - def get_project_info(self, project_key): + def get_project_info(self, project_key) -> dict | None: """Get project information by key""" return self.projects.get(project_key) - def get_all_projects(self): + def get_all_projects(self) -> dict: """Get all project configurations""" return self.projects - def is_installed(self, project_key): + def is_installed(self, project_key) -> bool: + detected_os = Detector.get_os() """Check if project is installed with better detection""" if project_key == "wirepy": # Check for wirepy symlink @@ -487,14 +260,16 @@ class AppManager: # Check for logviewer symlink AND executable file symlink_exists = os.path.exists("/usr/local/bin/logviewer") executable_exists = os.path.exists( - "/usr/local/share/shared_libs/logviewer.py" + f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py" ) executable_is_executable = False if executable_exists: try: # Check if file is executable - file_stat = os.stat("/usr/local/share/shared_libs/logviewer.py") + file_stat = os.stat( + f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py" + ) executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC) except: executable_is_executable = False @@ -515,13 +290,14 @@ class AppManager: return False - def get_installed_version(self, project_key): + def get_installed_version(self, project_key) -> str: + detected_os = Detector.get_os() """Get installed version from config file""" try: if project_key == "wirepy": - config_file = "/usr/local/share/shared_libs/wp_app_config.py" + config_file = f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/wp_app_config.py" elif project_key == "logviewer": - config_file = "/usr/local/share/shared_libs/logview_app_config.py" + config_file = f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py" else: return "Unknown" @@ -537,7 +313,7 @@ class AppManager: print(f"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}") return "Unknown" - def get_latest_version(self, project_key): + def get_latest_version(self, project_key) -> str: """Get latest version from API""" project_info = self.get_project_info(project_key) if not project_info: @@ -545,7 +321,7 @@ class AppManager: return GiteaUpdate.api_down(project_info["api_url"]) - def check_other_apps_installed(self, exclude_key): + def check_other_apps_installed(self, exclude_key) -> bool: """Check if other apps are still installed""" return any( self.is_installed(key) for key in self.projects.keys() if key != exclude_key @@ -554,7 +330,7 @@ class AppManager: class LxTools: @staticmethod - def center_window_cross_platform(window, width, height): + def center_window_cross_platform(window, width, height) -> None: """ Centers a window on the primary monitor in a way that works on both X11 and Wayland @@ -640,3 +416,276 @@ class LxTools: x = (screen_width - width) // 2 y = (screen_height - height) // 2 window.geometry(f"{width}x{height}+{x}+{y}") + + +class LXToolsAppConfig: + + @staticmethod + def extract_data_files() -> None: + if getattr(sys, "_MEIPASS", None) is not None: + # Liste der Quellordner (entspricht dem "datas"-Eintrag in lxtools_installer.spec) + source_dirs = [ + os.path.join(sys._MEIPASS, "locale"), # für locale/... + os.path.join(sys._MEIPASS, "TK-Themes"), # für TK-Themes/... + os.path.join(sys._MEIPASS, "lx-icons"), # für lx-icons/... + ] + + target_dir = os.path.abspath( + os.getcwd() + ) # Zielverzeichnis: aktueller Ordner + + for source_dir in source_dirs: + group_name = os.path.basename( + source_dir + ) # Erhält den Gruppen-Name (z. B. 'locale', 'TK-Themes') + + for root, dirs, files in os.walk(source_dir): + for file in files: + src_path = os.path.join(root, file) + + # Relativer Pfad innerhalb des Quellordners + rel_path_from_source_root = os.path.relpath( + src_path, source_dir + ) + + # Ziel-Pfad unter dem Gruppen-Ordner im aktuellen Verzeichnis + dst_path = os.path.join( + target_dir, group_name, rel_path_from_source_root + ) + + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + shutil.copy2(src_path, dst_path) + + # Set the SSL certificate file path by start as appimage + os.environ["SSL_CERT_FILE"] = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "certs", "cacert.pem" + ) + + @staticmethod + def setup_translations() -> gettext.gettext: + """Initialize translations and set the translation function""" + try: + locale.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.textdomain(LXToolsAppConfig.APP_NAME) + except: + pass + return gettext.gettext + + VERSION = "1.1.5" + APP_NAME = "lxtoolsinstaller" + WINDOW_WIDTH = 450 + WINDOW_HEIGHT = 580 + + # Working directory + WORK_DIR = os.getcwd() + ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") + THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") + + # Locale settings + LOCALE_DIR = "./locale/" + + # Download URLs + WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" + SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" + + # API URLs for version checking + WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" + SHARED_LIBS_API_URL = ( + "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" + ) + + # Project configurations + PROJECTS = { + "wirepy": { + "name": "Wire-Py", + "description": "WireGuard VPN Manager with GUI", + "download_url": WIREPY_URL, + "api_url": WIREPY_API_URL, + "icon_key": "icon_vpn", + "main_executable": "wirepy.py", + "symlink_name": "wirepy", + "config_file": "wp_app_config.py", + "desktop_file": "Wire-Py.desktop", + "policy_file": "org.sslcrypt.policy", + "requires_ssl": True, + }, + "logviewer": { + "name": "LogViewer", + "description": "System Log Viewer with GUI", + "download_url": SHARED_LIBS_URL, + "api_url": SHARED_LIBS_API_URL, + "icon_key": "icon_log", + "main_executable": "logviewer.py", + "symlink_name": "logviewer", + "config_file": "logview_app_config.py", + "desktop_file": "LogViewer.desktop", + "policy_file": None, + "requires_ssl": False, + }, + } + + # OS Detection List (order matters - specific first, generic last) + OS_DETECTION = [ + ("mint", "Linux Mint"), + ("pop", "Pop!_OS"), + ("manjaro", "Manjaro"), + ("garuda", "Garuda Linux"), + ("endeavouros", "EndeavourOS"), + ("fedora", "Fedora"), + ("tumbleweed", "SUSE Tumbleweed"), + ("leap", "SUSE Leap"), + ("arch", "Arch Linux"), + ("ubuntu", "Ubuntu"), + ("debian", "Debian"), + ] + + # Package manager commands for TKinter installation + TKINTER_INSTALL_COMMANDS = { + "Ubuntu": "apt install -y python3-tk", + "Debian": "apt install -y python3-tk", + "Linux Mint": "apt install -y python3-tk", + "Pop!_OS": "apt install -y python3-tk", + "Fedora": "dnf install -y python3-tkinter", + "Arch Linux": "pacman -S --noconfirm tk", + "Manjaro": "pacman -S --noconfirm tk", + "Garuda Linux": "pacman -S --noconfirm tk", + "EndeavourOS": "pacman -S --noconfirm tk", + "SUSE Tumbleweed": "zypper install -y python3-tk", + "SUSE Leap": "zypper install -y python3-tk", + } + + SHARED_LIBS_DESTINATION = { + "Ubuntu": "/usr/lib/python3/dist-packages/shared_libs", + "Debian": "/usr/lib/python3/dist-packages/shared_libs", + "Linux Mint": "/usr/lib/python3/dist-packages/shared_libs", + "Pop!_OS": "/usr/lib/python3/dist-packages/shared_libs", + "Fedora": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "Arch Linux": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "Manjaro": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "Garuda Linux": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "EndeavourOS": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "SUSE Tumbleweed": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + "SUSE Leap": f"/usr/lib64/python{Detector.get_host_python_version()}/site-packages/shared_libs", + } + + +LXToolsAppConfig.extract_data_files() +# Initialize translations +_ = LXToolsAppConfig.setup_translations() + + +class LocaleStrings: + + MSGI = { + "refresh_and_check": _("Refreshing status and checking versions..."), + "start_install": _("Starting installation of "), + "install": _("Installing "), + "install_success": _(" installation successfully!"), + "install_failed": _("Installation failed: "), + "install_create": _("Created install script: "), + "install_script_failed": _("Installation script failed: "), + "install_timeout": _("Installation timed out"), + "installed": _("Installed "), + } + + MSGU = { + "uninstall": _("Uninstalling "), + "uninstall_success": _(" uninstalled successfully!"), + "uninstall_failed": _("Uninstallation failed: "), + "uninstall_create": _("Created uninstall script: "), + "uninstall_script_failed": _("Uninstallation script failed: "), + "uninstall_timeout": _("Uninstallation timed out"), + } + # MSGO = Other messages + MSGO = { + "unknown_project": _("Unknown project: "), + "not_install": _(" is not installed."), + "download_from": _("Downloading from "), + "extract_files": _("Extracting files..."), + "download_failed": _("Download failed: "), + "head_string2": _("System: "), + "head_string3": _("Linux App Installer"), + "ready": _("Ready for installation"), + "no_internet": _("No internet connection"), + "repo_unavailable": _("Repository unavailable"), + "system_check": _("System checking..."), + "applications": _("Applications"), + "progress": _("Progress"), + "refresh2": _("Status refresh completed"), + "python_check": _("Python not installed"), + } + + # MSGC = Strings on Cards + MSGC = { + "checking": _("Checking..."), + "version_check": _("Version: Checking..."), + "latest": _("Latest: "), + "update_available": _("Update available "), + "up_to_date": _("Up to date"), + "latest_unknown": _("Latest unknown"), + "could_not_check": _("Could not check latest version"), + "check_last_failed": _("Latest: Check failed"), + "version_check_failed": _("Version check failed"), + "not_installed": _("Not installed"), + "available": _("Available "), + "available_unknown": _("Available unknown"), + "available_ckeck_failed": _("Available: Check failed"), + } + + # MSGL = Strings on Logmessages + MSGL = { + "selected_app": _("Selected project: "), + "log_name": _("Installation Log"), + "work_dir": _("Working directory: "), + "icons_dir": _("Icons directory: "), + "detected_os": _("Detected OS: "), + "log_cleared": _("Log cleared"), + "working_dir": _("Working directory: "), + "user_interuppt": _("\nApplication interrupted by user."), + "fatal_error": _("Fatal error: "), + "fatal_app_error": _("Fatal Error Application failed to start: "), + } + + # MSGB = Strings on Buttons + MSGB = { + "clear_log": _("Clear Log"), + "install": _("Install/Update"), + "uninstall": _("Uninstall"), + "refresh": _("Refresh Status"), + } + + # MSGM = String on MessagDialogs + MSGM = { + "please_select": _("Please select a project to install."), + "network_error": _( + "No internet connection available.\nPlease check your network connection.", + ), + "repo_error": _( + "Cannot access repository.\nPlease try again later.", + ), + "has_success_update": _("has been successfully installed/updated."), + "please_select_uninstall": _("Please select a project to uninstall."), + } + + # MSGP = Others print strings + MSGP = { + "tk_install": _("Installing tkinter for )"), + "command_string": _("Command: "), + "tk_success": _("TKinter installation completed successfully!"), + "tk_failed": _("TKinter installation failed: "), + "tk_timeout": _("TKinter installation timed out"), + "tk_install_error": _("Error installing tkinter: "), + "tk_command_error": _("No tkinter installation command defined for "), + "fail_load_image": _("Failed to load image from "), + "logviewer_check": _("LogViewer installation check:"), + "symlink_exist": _(" Symlink exists: "), + "executable_exist": _(" Executable exists: "), + "is_executable": _(" Is executable: "), + "final_result": _(" Final result: "), + "get_version_error": _("Error getting version for "), + }