Installer divided into several modules and added new MessageDialog module

This commit is contained in:
2025-06-14 22:16:15 +02:00
parent f288a2bd7f
commit 58ca160050
31 changed files with 1178 additions and 712 deletions

493
manager.py Normal file
View File

@ -0,0 +1,493 @@
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}")