Files
lxtools_installer/manager.py
2025-06-15 11:50:25 +02:00

494 lines
17 KiB
Python

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 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}")