import locale import gettext import tkinter as tk from tkinter import ttk import os import subprocess 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"], } @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 # 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(): """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 LXToolsAppConfig.OS_DETECTION: if keyword in content: return os_name return "Unknown System" except FileNotFoundError: return "File not found" @staticmethod def check_tkinter_available(): """Check if tkinter is available""" try: import tkinter return True except ImportError: return False @staticmethod def install_tkinter(): """Install tkinter based on detected OS""" detected_os = OSDetector.detect_os() 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 else: print(f"{LocaleStrings.MSGP["tk_command_error"]}{detected_os}") return False class Theme: @staticmethod def apply_light_theme(root): """Apply light theme""" try: theme_dir = LXToolsAppConfig.THEMES_DIR water_theme_path = os.path.join(theme_dir, "water.tcl") if os.path.exists(water_theme_path): try: root.tk.call("source", water_theme_path) root.tk.call("set_theme", "light") return True except tk.TclError: pass # System theme fallback try: style = ttk.Style() if "clam" in style.theme_names(): style.theme_use("clam") return True except: pass except Exception: pass return False class System: @staticmethod def create_directories(directories): """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): """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): """Copy directory using pkexec""" subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) @staticmethod def remove_file(path): """Remove file using pkexec""" subprocess.run(["pkexec", "rm", "-f", path], check=False) @staticmethod def remove_directory(path): """Remove directory using pkexec""" subprocess.run(["pkexec", "rm", "-rf", path], check=False) @staticmethod def create_symlink(target, link_name): """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): """Create SSL key using pkexec""" try: subprocess.run( ["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True ) subprocess.run(["pkexec", "chmod", "600", pem_file], check=True) return True except subprocess.CalledProcessError: return False class Image: def __init__(self): self.images = {} def load_image(self, image_key, fallback_paths=None): """Load PNG image using tk.PhotoImage with fallback options""" if image_key in self.images: return self.images[image_key] # Define image paths based on key image_paths = { "app_icon": [ "./lx-icons/48/wg_vpn.png", "/usr/share/icons/lx-icons/48/wg_vpn.png", ], "download_icon": [ "./lx-icons/32/download.png", "/usr/share/icons/lx-icons/32/download.png", ], "download_error_icon": [ "./lx-icons/32/download_error.png", "/usr/share/icons/lx-icons/32/download_error.png", ], "success_icon": [ "./lx-icons/32/download.png", "/usr/share/icons/lx-icons/32/download.png", ], "icon_vpn": [ "./lx-icons/48/wg_vpn.png", "/usr/share/icons/lx-icons/48/wg_vpn.png", ], "icon_log": [ "./lx-icons/48/log.png", "/usr/share/icons/lx-icons/48/log.png", ], } # Get paths to try paths_to_try = image_paths.get(image_key, []) # Add fallback paths if provided 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[image_key] = photo return photo except tk.TclError as e: print(f"{LocaleStrings.MSGP["fail_load_image"]}{path}: {e}") continue # Return None if no image found return None class AppManager: def __init__(self): self.projects = LXToolsAppConfig.PROJECTS def get_project_info(self, project_key): """Get project information by key""" return self.projects.get(project_key) def get_all_projects(self): """Get all project configurations""" return self.projects def is_installed(self, project_key): """Check if project is installed with better detection""" if project_key == "wirepy": # Check for wirepy symlink return os.path.exists("/usr/local/bin/wirepy") and os.path.islink( "/usr/local/bin/wirepy" ) elif project_key == "logviewer": # 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" ) executable_is_executable = False if executable_exists: try: # Check if file is executable file_stat = os.stat("/usr/local/share/shared_libs/logviewer.py") executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC) except: executable_is_executable = False # LogViewer is installed if symlink exists AND executable file exists AND is executable is_installed = ( symlink_exists and executable_exists and executable_is_executable ) # Debug logging print(LocaleStrings.MSGP["logviewer_check"]) print(f"{LocaleStrings.MSGP["symlink_exist"]}{symlink_exists}") print(f"{LocaleStrings.MSGP["executable_exist"]}{executable_exists}") print(f"{LocaleStrings.MSGP["is_executable"]}{executable_is_executable}") print(f"{LocaleStrings.MSGP["final_result"]}{is_installed}") return is_installed return False def get_installed_version(self, project_key): """Get installed version from config file""" try: if project_key == "wirepy": config_file = "/usr/local/share/shared_libs/wp_app_config.py" elif project_key == "logviewer": config_file = "/usr/local/share/shared_libs/logview_app_config.py" else: return "Unknown" 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"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}") return "Unknown" def get_latest_version(self, project_key): """Get latest version from API""" project_info = self.get_project_info(project_key) if not project_info: return "Unknown" return GiteaUpdate.api_down(project_info["api_url"]) def check_other_apps_installed(self, exclude_key): """Check if other apps are still installed""" return any( self.is_installed(key) for key in self.projects.keys() if key != exclude_key ) class LxTools: @staticmethod def center_window_cross_platform(window, width, height): """ Centers a window on the primary monitor in a way that works on both X11 and Wayland Args: window: The tkinter window to center width: Window width height: Window height """ # Calculate the position before showing the window # First attempt: Try to use GDK if available (works on both X11 and Wayland) try: import gi gi.require_version("Gdk", "3.0") from gi.repository import Gdk display = Gdk.Display.get_default() monitor = display.get_primary_monitor() or display.get_monitor(0) geometry = monitor.get_geometry() scale_factor = monitor.get_scale_factor() # Calculate center position on the primary monitor x = geometry.x + (geometry.width - width // scale_factor) // 2 y = geometry.y + (geometry.height - height // scale_factor) // 2 # Set window geometry window.geometry(f"{width}x{height}+{x}+{y}") return except (ImportError, AttributeError): pass # Second attempt: Try xrandr for X11 try: import subprocess output = subprocess.check_output( ["xrandr", "--query"], universal_newlines=True ) # Parse the output to find the primary monitor primary_info = None for line in output.splitlines(): if "primary" in line: parts = line.split() for part in parts: if "x" in part and "+" in part: primary_info = part break break if primary_info: # Parse the geometry: WIDTH x HEIGHT+X+Y geometry = primary_info.split("+") dimensions = geometry[0].split("x") primary_width = int(dimensions[0]) primary_height = int(dimensions[1]) primary_x = int(geometry[1]) primary_y = int(geometry[2]) # Calculate center position on the primary monitor x = primary_x + (primary_width - width) // 2 y = primary_y + (primary_height - height) // 2 # Set window geometry window.geometry(f"{width}x{height}+{x}+{y}") return except (ImportError, IndexError, ValueError): pass # Final fallback: Use standard Tkinter method screen_width = window.winfo_screenwidth() screen_height = window.winfo_screenheight() # Try to make an educated guess for multi-monitor setups # If screen width is much larger than height, assume multiple monitors side by side if ( screen_width > screen_height * 1.8 ): # Heuristic for detecting multiple monitors # Assume the primary monitor is on the left half screen_width = screen_width // 2 x = (screen_width - width) // 2 y = (screen_height - height) // 2 window.geometry(f"{width}x{height}+{x}+{y}")