6 Commits

5 changed files with 487 additions and 395 deletions

View File

@@ -2,7 +2,39 @@ Changelog for LXTools installer
## [Unreleased]
- replace pack with grid
- language set auto detection
-
### Added
23-06-2025
- Add unzip check, requests check, wget check for Arch Linux
- fix remove config dirs and logfile and ssl privatkey on uninstall
- method the number of users of the system is extended
for information message when a program is uninstalled
and there are more than one user on the system.
### Added
22-06-2025
- ssl certificate integrate in Appinstaller
- Installer now takes into account the current python
version ud the respective recognized system to run with
it on all supported systems.
### Added
21-06-2025
- extract now files needed in the work directory Theme, Icons and translation
- if python is not found, it displays this in red in the header,
with new get_python_version method
### Added
18-06-2025

View File

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
import os
import subprocess
import getpass
from datetime import datetime
import tempfile
import urllib.request
import zipfile
from manager import (
OSDetector,
Detector,
Theme,
LocaleStrings,
LXToolsAppConfig,
@@ -75,14 +76,37 @@ class InstallationManager:
raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}")
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()
result_requests = Detector.get_requests()
else:
result_unzip = None
result_wget = None
result_requests = None
"""Create Wire-Py installation script"""
script = f"""#!/bin/bash
set -e
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
if [ "{result_requests}" = "False" ]; then
pacman -S --noconfirm python-requests
fi
fi
{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface"
# 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/locale/de/LC_MESSAGES
mkdir -p /usr/share/applications
@@ -118,7 +142,7 @@ done
# Install config
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"
fi
@@ -126,15 +150,15 @@ fi
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
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"
fi
done
# Install LogViewer executable
if [ -f "$SHARED_DIR/logviewer.py" ]; then
cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/
chmod 755 /usr/local/share/shared_libs/logviewer.py
cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/
chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
echo "Installed logviewer.py (executable)"
fi
@@ -171,7 +195,7 @@ fi
# Create symlink for Wirepy
ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
# 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"
# Install language files if available
@@ -197,14 +221,35 @@ echo "Wire-Py installation completed!"
return script
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()
else:
result_unzip = None
result_wget = None
result_requests = None
"""Create LogViewer installation script"""
script = f"""#!/bin/bash
set -e
{LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface"
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
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/locale/de/LC_MESSAGES
mkdir -p /usr/share/applications
@@ -244,36 +289,21 @@ fi
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
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"
fi
done
# Install LogViewer executable
if [ -f "$SHARED_DIR/logviewer.py" ]; then
cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/
chmod 755 /usr/local/share/shared_libs/logviewer.py
cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/
chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
echo "Installed logviewer.py (executable)"
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
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"
# Install language files if available
@@ -393,8 +423,9 @@ class UninstallationManager:
raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}")
def _create_wirepy_uninstall_script(self):
detected_os = Detector.get_os()
"""Create Wire-Py uninstallation script"""
script = """#!/bin/bash
script = f"""#!/bin/bash
set -e
echo "=== Wire-Py Uninstallation ==="
@@ -411,7 +442,7 @@ rm -f /usr/local/bin/wirepy
echo "Removed wirepy symlink"
# 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"
# Remove desktop file
@@ -427,13 +458,16 @@ rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo
echo "Removed language files"
# Remove user config directory
if [ -d "$HOME/.config/wire_py" ]; then
rm -rf "$HOME/.config/wire_py"
if [ -d /home/{getpass.getuser()}/.config/wire_py ]; then
rm -rf /home/{getpass.getuser()}/.config/wire_py
echo "Removed user config directory"
fi
# Remove ssl private key
rm -rf /usr/local/etc/ssl
echo "Removed ssl private key"
# 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"
# Check if LogViewer is still installed before removing shared resources
@@ -441,15 +475,14 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then
echo "No other LX apps found, removing shared resources..."
rm -rf /usr/share/icons/lx-icons
rm -rf /usr/share/TK-Themes
rm -rf /usr/local/etc/ssl
# 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
rm -f /usr/local/share/shared_libs/$file
rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file
done
# 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
echo "LogViewer still installed, keeping shared resources"
fi
@@ -459,8 +492,9 @@ echo "Wire-Py uninstallation completed!"
return script
def _create_logviewer_uninstall_script(self):
detected_os = Detector.get_os()
"""Create LogViewer uninstallation script"""
script = """#!/bin/bash
script = f"""#!/bin/bash
set -e
echo "=== LogViewer Uninstallation ==="
@@ -469,22 +503,18 @@ echo "=== LogViewer Uninstallation ==="
rm -f /usr/local/bin/logviewer
echo "Removed logviewer symlink"
# Remove desktop file
rm -f /usr/share/applications/LogViewer.desktop
echo "Removed desktop file"
# Remove language files
rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo
echo "Removed language files"
# Remove user config directory
if [ -d "$HOME/.config/logviewer" ]; then
rm -rf "$HOME/.config/logviewer"
if [ -d /home/{getpass.getuser()}/.config/logviewer ]; then
rm -rf /home/{getpass.getuser()}/.config/logviewer
echo "Removed user config directory"
fi
# 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"
# Check if Wire-Py is still installed before removing shared resources
@@ -495,19 +525,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)
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
# 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
rmdir /usr/local/share/shared_libs 2>/dev/null || true
rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true
else
echo "Wire-Py still installed, keeping shared resources"
# Only remove logviewer-specific files
rm -f /usr/local/share/shared_libs/logview_app_config.py
rm -f /usr/local/share/shared_libs/logviewer.py
rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py
rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py
fi
echo "LogViewer uninstallation completed!"
@@ -614,7 +644,7 @@ class LXToolsGUI:
self.image_manager = Image()
# Detect OS
self.detected_os = OSDetector.detect_os()
self.detected_os = Detector.get_os()
# Color scheme
self.colors = {
@@ -652,7 +682,7 @@ class LXToolsGUI:
self._create_header()
# Create notebook (tabs)
self.notebook = ttk.Notebook(self.root, height=300)
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill="both", expand=True, padx=15, pady=(10, 10))
# Create tabs
@@ -746,8 +776,9 @@ class LXToolsGUI:
# Checks:
internet_ok = NetworkChecker.check_internet_connection()
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
elif not internet_ok:
self.update_header_status(
@@ -757,6 +788,10 @@ class LXToolsGUI:
self.update_header_status(
LocaleStrings.MSGO["repo_unavailable"], "#f39c12"
) # Orange
elif result is None:
self.update_header_status(
LocaleStrings.MSGO["python_check"], "#e74c3c"
) # Red
else:
self.update_header_status(
LocaleStrings.MSGO["system_check"], "#3498db"
@@ -966,6 +1001,9 @@ class LXToolsGUI:
# Log text with scrollbar
log_container = tk.Frame(log_frame)
log_container.pack(fill="both", expand=True, padx=10, pady=10)
# Important! pack_propagate(False) must be set here to display
# the Clear Log button correctly
log_container.pack_propagate(False)
self.log_text = tk.Text(
log_container,
@@ -987,13 +1025,13 @@ class LXToolsGUI:
# Log controls
log_controls = tk.Frame(log_frame)
log_controls.pack(fill="x", padx=10, pady=(0, 0))
log_controls.pack(fill="x", padx=10, pady=(5, 0))
# Clear log button
clear_log_btn = ttk.Button(
log_controls, text=LocaleStrings.MSGB["clear_log"], command=self.clear_log
)
clear_log_btn.pack(side="right")
clear_log_btn.pack(side="right", pady=(0, 10))
# Initial log message
self.log_message(
@@ -1257,6 +1295,7 @@ class LXToolsGUI:
MessageDialog(
"info",
f"{project_info["name"]} {LocaleStrings.MSGM["has_success_update"]}",
wraplength=400,
)
self.refresh_status()
@@ -1288,6 +1327,7 @@ class LXToolsGUI:
MessageDialog(
"info",
f"{project_info["name"]} {LocaleStrings.MSGU["uninstall_success"]}",
wraplength=400,
)
self.refresh_status()
@@ -1317,7 +1357,7 @@ class LXToolsGUI:
"""Clear the log"""
if self.log_text:
self.log_text.delete(1.0, tk.END)
self.log_message(MSGL["log_cleared"])
self.log_message(LocaleStrings.MSGL["log_cleared"])
def run(self):
"""Start the GUI application"""

View File

@@ -2,230 +2,18 @@ import locale
import gettext
import tkinter as tk
from tkinter import ttk
from pathlib import Path
import os
import sys
import shutil
import subprocess
import stat
from network import GiteaUpdate
class LXToolsAppConfig:
VERSION = "1.1.5"
APP_NAME = "lxtoolsinstaller"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 740
# 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"],
}
class Detector:
@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():
def get_os() -> str:
"""Detect operating system using ordered list"""
try:
with open("/etc/os-release", "r") as f:
@@ -241,54 +29,77 @@ class OSDetector:
return "File not found"
@staticmethod
def check_tkinter_available():
"""Check if tkinter is available"""
def get_host_python_version() -> str:
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
except ImportError:
else:
return False
@staticmethod
def install_tkinter():
"""Install tkinter based on detected OS"""
detected_os = OSDetector.detect_os()
def get_wget() -> bool:
"""Check if wget is installed"""
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
result = subprocess.run(
["which", "wget"], capture_output=True, text=True, check=False
)
if result.returncode == 0:
return True
else:
return False
@staticmethod
def get_unzip() -> bool:
"""Check if wget is installed"""
result = subprocess.run(
["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:
print(f"{LocaleStrings.MSGP["tk_command_error"]}{detected_os}")
return False
class Theme:
@staticmethod
def apply_light_theme(root):
def apply_light_theme(root) -> bool:
"""Apply light theme"""
try:
theme_dir = LXToolsAppConfig.THEMES_DIR
@@ -319,41 +130,41 @@ class Theme:
class System:
@staticmethod
def create_directories(directories):
def create_directories(directories) -> None:
"""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):
def copy_file(src, dest, make_executable=False) -> None:
"""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):
def copy_directory(src, dest) -> None:
"""Copy directory using pkexec"""
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
@staticmethod
def remove_file(path):
def remove_file(path) -> None:
"""Remove file using pkexec"""
subprocess.run(["pkexec", "rm", "-f", path], check=False)
@staticmethod
def remove_directory(path):
def remove_directory(path) -> None:
"""Remove directory using pkexec"""
subprocess.run(["pkexec", "rm", "-rf", path], check=False)
@staticmethod
def create_symlink(target, link_name):
def create_symlink(target, link_name) -> None:
"""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):
def create_ssl_key(pem_file) -> bool:
"""Create SSL key using pkexec"""
try:
subprocess.run(
@@ -369,7 +180,7 @@ class Image:
def __init__(self):
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"""
if image_key in self.images:
return self.images[image_key]
@@ -428,15 +239,16 @@ class AppManager:
def __init__(self):
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"""
return self.projects.get(project_key)
def get_all_projects(self):
def get_all_projects(self) -> dict:
"""Get all project configurations"""
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"""
if project_key == "wirepy":
# Check for wirepy symlink
@@ -448,14 +260,16 @@ class AppManager:
# 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"
f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/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")
file_stat = os.stat(
f"{LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py"
)
executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC)
except:
executable_is_executable = False
@@ -476,13 +290,14 @@ class AppManager:
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"""
try:
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":
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:
return "Unknown"
@@ -498,7 +313,7 @@ class AppManager:
print(f"{LocaleStrings.MSGP["get_version_error"]}{project_key}: {e}")
return "Unknown"
def get_latest_version(self, project_key):
def get_latest_version(self, project_key) -> str:
"""Get latest version from API"""
project_info = self.get_project_info(project_key)
if not project_info:
@@ -506,7 +321,7 @@ class AppManager:
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"""
return any(
self.is_installed(key) for key in self.projects.keys() if key != exclude_key
@@ -515,7 +330,7 @@ class AppManager:
class LxTools:
@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
@@ -601,3 +416,276 @@ class LxTools:
x = (screen_width - width) // 2
y = (screen_height - height) // 2
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.6"
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 "),
}