add requests for arch linux

This commit is contained in:
2025-06-23 14:44:33 +02:00
parent 01bd6ab263
commit ed269af1d2
3 changed files with 431 additions and 428 deletions

View File

@ -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()

View File

@ -3,12 +3,13 @@ import tkinter as tk
from tkinter import ttk from tkinter import ttk
import os import os
import subprocess import subprocess
import getpass
from datetime import datetime from datetime import datetime
import tempfile import tempfile
import urllib.request import urllib.request
import zipfile import zipfile
from manager import ( from manager import (
OSDetector, Detector,
Theme, Theme,
LocaleStrings, LocaleStrings,
LXToolsAppConfig, LXToolsAppConfig,
@ -75,14 +76,27 @@ class InstallationManager:
raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}")
def _create_wirepy_install_script(self): 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""" """Create Wire-Py installation script"""
script = f"""#!/bin/bash script = f"""#!/bin/bash
set -e set -e
echo "=== Wire-Py Installation ===" 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 # 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/icons/lx-icons
mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/locale/de/LC_MESSAGES
mkdir -p /usr/share/applications mkdir -p /usr/share/applications
@ -118,7 +132,7 @@ done
# Install config # Install config
if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then 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" echo "Installed wp_app_config.py"
fi fi
@ -126,15 +140,15 @@ fi
echo "Installing shared libraries..." 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 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 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" echo "Installed shared lib: $file"
fi fi
done done
# Install LogViewer executable # Install LogViewer executable
if [ -f "$SHARED_DIR/logviewer.py" ]; then if [ -f "$SHARED_DIR/logviewer.py" ]; then
cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/
chmod 755 /usr/local/share/shared_libs/logviewer.py chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
echo "Installed logviewer.py (executable)" echo "Installed logviewer.py (executable)"
fi fi
@ -171,7 +185,7 @@ fi
# Create symlink for Wirepy # Create symlink for Wirepy
ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
# Create symlink for LogViewer # 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" echo "Created Wirepy and LogViewer symlink"
# Install language files if available # Install language files if available
@ -197,14 +211,32 @@ echo "Wire-Py installation completed!"
return script return script
def _create_logviewer_install_script(self): 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""" """Create LogViewer installation script"""
script = f"""#!/bin/bash script = f"""#!/bin/bash
set -e set -e
{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface"
echo "=== LogViewer Installation ===" 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 # 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/icons/lx-icons
mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/locale/de/LC_MESSAGES
mkdir -p /usr/share/applications mkdir -p /usr/share/applications
@ -244,36 +276,21 @@ fi
echo "Installing shared libraries..." 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 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 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" echo "Installed shared lib: $file"
fi fi
done done
# Install LogViewer executable # Install LogViewer executable
if [ -f "$SHARED_DIR/logviewer.py" ]; then if [ -f "$SHARED_DIR/logviewer.py" ]; then
cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/
chmod 755 /usr/local/share/shared_libs/logviewer.py chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
echo "Installed logviewer.py (executable)" echo "Installed logviewer.py (executable)"
fi 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 # 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" echo "Created LogViewer symlink"
# Install language files if available # Install language files if available
@ -393,8 +410,9 @@ class UninstallationManager:
raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}")
def _create_wirepy_uninstall_script(self): def _create_wirepy_uninstall_script(self):
detected_os = Detector.get_os()
"""Create Wire-Py uninstallation script""" """Create Wire-Py uninstallation script"""
script = """#!/bin/bash script = f"""#!/bin/bash
set -e set -e
echo "=== Wire-Py Uninstallation ===" echo "=== Wire-Py Uninstallation ==="
@ -411,7 +429,7 @@ rm -f /usr/local/bin/wirepy
echo "Removed wirepy symlink" echo "Removed wirepy symlink"
# Remove config # 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" echo "Removed wp_app_config.py"
# Remove desktop file # Remove desktop file
@ -427,13 +445,16 @@ rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo
echo "Removed language files" echo "Removed language files"
# Remove user config directory # Remove user config directory
if [ -d "$HOME/.config/wire_py" ]; then if [ -d /home/{getpass.getuser()}/.config/wire_py ]; then
rm -rf "$HOME/.config/wire_py" rm -rf /home/{getpass.getuser()}/.config/wire_py
echo "Removed user config directory" echo "Removed user config directory"
fi fi
# Remove ssl private key
rm -rf /usr/local/etc/ssl
echo "Removed ssl private key"
# Remove log file # 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" echo "Removed log file"
# Check if LogViewer is still installed before removing shared resources # 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..." echo "No other LX apps found, removing shared resources..."
rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/icons/lx-icons
rm -rf /usr/share/TK-Themes rm -rf /usr/share/TK-Themes
rm -rf /usr/local/etc/ssl
# Remove shared libs # 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 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 done
# Try to remove shared_libs directory if empty # 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 else
echo "LogViewer still installed, keeping shared resources" echo "LogViewer still installed, keeping shared resources"
fi fi
@ -459,8 +479,9 @@ echo "Wire-Py uninstallation completed!"
return script return script
def _create_logviewer_uninstall_script(self): def _create_logviewer_uninstall_script(self):
detected_os = Detector.get_os()
"""Create LogViewer uninstallation script""" """Create LogViewer uninstallation script"""
script = """#!/bin/bash script = f"""#!/bin/bash
set -e set -e
echo "=== LogViewer Uninstallation ===" echo "=== LogViewer Uninstallation ==="
@ -469,22 +490,18 @@ echo "=== LogViewer Uninstallation ==="
rm -f /usr/local/bin/logviewer rm -f /usr/local/bin/logviewer
echo "Removed logviewer symlink" echo "Removed logviewer symlink"
# Remove desktop file
rm -f /usr/share/applications/LogViewer.desktop
echo "Removed desktop file"
# Remove language files # Remove language files
rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo
echo "Removed language files" echo "Removed language files"
# Remove user config directory # Remove user config directory
if [ -d "$HOME/.config/logviewer" ]; then if [ -d /home/{getpass.getuser()}/.config/logviewer ]; then
rm -rf "$HOME/.config/logviewer" rm -rf /home/{getpass.getuser()}/.config/logviewer
echo "Removed user config directory" echo "Removed user config directory"
fi fi
# Remove log file # 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" echo "Removed log file"
# Check if Wire-Py is still installed before removing shared resources # 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) # 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 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 done
# Remove logviewer.py last # 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 # 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 else
echo "Wire-Py still installed, keeping shared resources" echo "Wire-Py still installed, keeping shared resources"
# Only remove logviewer-specific files # Only remove logviewer-specific files
rm -f /usr/local/share/shared_libs/logview_app_config.py rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py
rm -f /usr/local/share/shared_libs/logviewer.py rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
fi fi
echo "LogViewer uninstallation completed!" echo "LogViewer uninstallation completed!"
@ -614,7 +631,7 @@ class LXToolsGUI:
self.image_manager = Image() self.image_manager = Image()
# Detect OS # Detect OS
self.detected_os = OSDetector.detect_os() self.detected_os = Detector.get_os()
# Color scheme # Color scheme
self.colors = { self.colors = {
@ -746,8 +763,9 @@ class LXToolsGUI:
# Checks: # Checks:
internet_ok = NetworkChecker.check_internet_connection() internet_ok = NetworkChecker.check_internet_connection()
repo_ok = NetworkChecker.check_repository_access() 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 self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green
elif not internet_ok: elif not internet_ok:
self.update_header_status( self.update_header_status(
@ -757,6 +775,10 @@ class LXToolsGUI:
self.update_header_status( self.update_header_status(
LocaleStrings.MSGO["repo_unavailable"], "#f39c12" LocaleStrings.MSGO["repo_unavailable"], "#f39c12"
) # Orange ) # Orange
elif result is None:
self.update_header_status(
LocaleStrings.MSGO["python_check"], "#e74c3c"
) # Red
else: else:
self.update_header_status( self.update_header_status(
LocaleStrings.MSGO["system_check"], "#3498db" LocaleStrings.MSGO["system_check"], "#3498db"

View File

@ -2,6 +2,7 @@ import locale
import gettext import gettext
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from pathlib import Path
import os import os
import sys import sys
import shutil import shutil
@ -10,261 +11,9 @@ import stat
from network import GiteaUpdate from network import GiteaUpdate
class LXToolsAppConfig: class Detector:
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 @staticmethod
def extract_data_files(): def get_os() -> str:
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():
"""Detect operating system using ordered list""" """Detect operating system using ordered list"""
try: try:
with open("/etc/os-release", "r") as f: with open("/etc/os-release", "r") as f:
@ -280,54 +29,77 @@ class OSDetector:
return "File not found" return "File not found"
@staticmethod @staticmethod
def check_tkinter_available(): def get_host_python_version() -> str:
"""Check if tkinter is available"""
try: 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 return True
except ImportError: else:
return False return False
@staticmethod @staticmethod
def install_tkinter(): def get_wget() -> bool:
"""Install tkinter based on detected OS""" """Check if wget is installed"""
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( result = subprocess.run(
full_command, capture_output=True, text=True, timeout=300 ["which", "wget"], capture_output=True, text=True, check=False
) )
if result.returncode == 0: if result.returncode == 0:
print(f"{LocaleStrings.MSGP["tk_succcess"]}")
return True return True
else: else:
print(f"{LocaleStrings.MSGP["tk_failed"]}{result.stderr}")
return False return False
except subprocess.TimeoutExpired: @staticmethod
print(LocaleStrings.MSGP["tk_timeout"]) def get_unzip() -> bool:
return False """Check if wget is installed"""
except Exception as e:
print(f"{LocaleStrings.MSGP['tk_install_error']}{str(e)}") result = subprocess.run(
return False ["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: else:
print(f"{LocaleStrings.MSGP["tk_command_error"]}{detected_os}")
return False return False
class Theme: class Theme:
@staticmethod @staticmethod
def apply_light_theme(root): def apply_light_theme(root) -> bool:
"""Apply light theme""" """Apply light theme"""
try: try:
theme_dir = LXToolsAppConfig.THEMES_DIR theme_dir = LXToolsAppConfig.THEMES_DIR
@ -358,41 +130,41 @@ class Theme:
class System: class System:
@staticmethod @staticmethod
def create_directories(directories): def create_directories(directories) -> None:
"""Create system directories using pkexec""" """Create system directories using pkexec"""
for directory in directories: for directory in directories:
subprocess.run(["pkexec", "mkdir", "-p", directory], check=True) subprocess.run(["pkexec", "mkdir", "-p", directory], check=True)
@staticmethod @staticmethod
def copy_file(src, dest, make_executable=False): def copy_file(src, dest, make_executable=False) -> None:
"""Copy file using pkexec""" """Copy file using pkexec"""
subprocess.run(["pkexec", "cp", src, dest], check=True) subprocess.run(["pkexec", "cp", src, dest], check=True)
if make_executable: if make_executable:
subprocess.run(["pkexec", "chmod", "755", dest], check=True) subprocess.run(["pkexec", "chmod", "755", dest], check=True)
@staticmethod @staticmethod
def copy_directory(src, dest): def copy_directory(src, dest) -> None:
"""Copy directory using pkexec""" """Copy directory using pkexec"""
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
@staticmethod @staticmethod
def remove_file(path): def remove_file(path) -> None:
"""Remove file using pkexec""" """Remove file using pkexec"""
subprocess.run(["pkexec", "rm", "-f", path], check=False) subprocess.run(["pkexec", "rm", "-f", path], check=False)
@staticmethod @staticmethod
def remove_directory(path): def remove_directory(path) -> None:
"""Remove directory using pkexec""" """Remove directory using pkexec"""
subprocess.run(["pkexec", "rm", "-rf", path], check=False) subprocess.run(["pkexec", "rm", "-rf", path], check=False)
@staticmethod @staticmethod
def create_symlink(target, link_name): def create_symlink(target, link_name) -> None:
"""Create symbolic link using pkexec""" """Create symbolic link using pkexec"""
subprocess.run(["pkexec", "rm", "-f", link_name], check=False) subprocess.run(["pkexec", "rm", "-f", link_name], check=False)
subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True) subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True)
@staticmethod @staticmethod
def create_ssl_key(pem_file): def create_ssl_key(pem_file) -> bool:
"""Create SSL key using pkexec""" """Create SSL key using pkexec"""
try: try:
subprocess.run( subprocess.run(
@ -408,7 +180,7 @@ class Image:
def __init__(self): def __init__(self):
self.images = {} 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""" """Load PNG image using tk.PhotoImage with fallback options"""
if image_key in self.images: if image_key in self.images:
return self.images[image_key] return self.images[image_key]
@ -467,15 +239,16 @@ class AppManager:
def __init__(self): def __init__(self):
self.projects = LXToolsAppConfig.PROJECTS 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""" """Get project information by key"""
return self.projects.get(project_key) return self.projects.get(project_key)
def get_all_projects(self): def get_all_projects(self) -> dict:
"""Get all project configurations""" """Get all project configurations"""
return self.projects 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""" """Check if project is installed with better detection"""
if project_key == "wirepy": if project_key == "wirepy":
# Check for wirepy symlink # Check for wirepy symlink
@ -487,14 +260,16 @@ class AppManager:
# Check for logviewer symlink AND executable file # Check for logviewer symlink AND executable file
symlink_exists = os.path.exists("/usr/local/bin/logviewer") symlink_exists = os.path.exists("/usr/local/bin/logviewer")
executable_exists = os.path.exists( 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 executable_is_executable = False
if executable_exists: if executable_exists:
try: try:
# Check if file is executable # 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) executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC)
except: except:
executable_is_executable = False executable_is_executable = False
@ -515,13 +290,14 @@ class AppManager:
return False 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""" """Get installed version from config file"""
try: try:
if project_key == "wirepy": 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": 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: else:
return "Unknown" return "Unknown"
@ -537,7 +313,7 @@ class AppManager:
print(f"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}") print(f"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}")
return "Unknown" return "Unknown"
def get_latest_version(self, project_key): def get_latest_version(self, project_key) -> str:
"""Get latest version from API""" """Get latest version from API"""
project_info = self.get_project_info(project_key) project_info = self.get_project_info(project_key)
if not project_info: if not project_info:
@ -545,7 +321,7 @@ class AppManager:
return GiteaUpdate.api_down(project_info["api_url"]) 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""" """Check if other apps are still installed"""
return any( return any(
self.is_installed(key) for key in self.projects.keys() if key != exclude_key self.is_installed(key) for key in self.projects.keys() if key != exclude_key
@ -554,7 +330,7 @@ class AppManager:
class LxTools: class LxTools:
@staticmethod @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 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 x = (screen_width - width) // 2
y = (screen_height - height) // 2 y = (screen_height - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}") 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 "),
}