import locale import gettext import tkinter as tk from pathlib import Path from tkinter import ttk import os import subprocess import stat from network import GiteaUpdate class LXToolsAppConfig: VERSION = "1.1.4" APP_NAME = "Lunix Tools Installer" 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 = Path("/usr/share/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(): """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 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"Installing tkinter for {detected_os}...") print(_(f"Command: {' '.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(_("TKinter installation completed successfully!")) return True else: print(_(f"TKinter installation failed: {result.stderr}")) return False except subprocess.TimeoutExpired: print(_("TKinter installation timed out")) return False except Exception as e: print(_(f"Error installing tkinter: {e}")) return False else: print(_(f"No tkinter installation command defined for {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"Failed to load image from {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(_("LogViewer installation check:")) print(_(f" Symlink exists: {symlink_exists}")) print(_(f" Executable exists: {executable_exists}")) print(_(f" Is executable: {executable_is_executable}")) print(_(f" 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"Error getting version for {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 Center: @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}")