diff --git a/Changelog b/Changelog index 8c67e92..b3414ab 100644 --- a/Changelog +++ b/Changelog @@ -7,23 +7,26 @@ My standard System: Linux Mint 22 Cinnamon - If Wire-Py already runs, prevent further start - for loops with lists replaced by List Comprehensions - ### Added -13-04-0725 +03-06-2025 + + - + ### Added +13-04-20255 - Installer update for Open Suse Tumbleweed and Leap - add symbolic link wirepy.py ### Added -09-04-0725 +09-04-2025 - Installer now with query and remove - Icons merged ### Added -07-04-0725 +07-04-2025 - Installers will support other systems again - Installer is now finished clean with wrong password diff --git a/common_tools.py b/common_tools.py deleted file mode 100755 index f9686fa..0000000 --- a/common_tools.py +++ /dev/null @@ -1,883 +0,0 @@ -""" Classes Method and Functions for lx Apps """ - -import getpass -import shutil -import signal -import base64 -import secrets -import subprocess -from subprocess import CompletedProcess, run -import re -import sys -import tkinter as tk -from typing import Optional, Dict, Any, NoReturn -import zipfile -from datetime import datetime -from pathlib import Path -from tkinter import ttk, Toplevel -from wp_app_config import AppConfig, Msg, logging -import requests - -# Translate -_ = AppConfig.setup_translations() - - -class CryptoUtil: - """ - This class is for the creation of the folders and files - required by Wire-Py, as well as for decryption - the tunnel from the user's home directory - """ - - @staticmethod - def decrypt(user) -> None: - """ - Starts SSL dencrypt - """ - if len([file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat")]) == 0: - pass - else: - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully decrypted...", exc_info=True) - else: - logging.error( - f"Error process decrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def encrypt(user) -> None: - """ - Starts SSL encryption - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully encrypted...", exc_info=True) - else: - logging.error( - f"Error process encrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def find_key(key: str = "") -> bool: - """ - Checks if the private key already exists in the system using an external script. - Returns True only if the full key is found exactly (no partial match). - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/match_found.py", key], - capture_output=True, - text=True, - check=False, - ) - if "True" in process.stdout: - return True - elif "False" in process.stdout: - return False - logging.error( - f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}", - exc_info=True, - ) - return False - - @staticmethod - def is_valid_base64(key: str) -> bool: - """ - Validates if the input is a valid Base64 string (WireGuard private key format). - Returns True only for non-empty strings that match the expected length. - """ - # Check for empty string - if not key or key.strip() == "": - return False - - # Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2} - base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$" - if not re.match(base64_pattern, key): - return False - - try: - # Decode and check length (WireGuard private keys are 32 bytes long) - decoded = base64.b64decode(key) - if len(decoded) != 32: # 32 bytes = 256 bits - return False - except Exception as e: - logging.error(f"Error on decode Base64: {e}", exc_info=True) - return False - - return True - - -class LxTools: - """ - Class LinuxTools methods that can also be used for other apps - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - @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}") - - @staticmethod - def clean_files(tmp_dir: Path = AppConfig.TEMP_DIR, file: Path = None) -> None: - """ - Deletes temporary files and directories for cleanup when exiting the application. - - This method safely removes an optional directory defined by `AppConfig.TEMP_DIR` - and a single file to free up resources at the end of the program's execution. - All operations are performed securely, and errors such as `FileNotFoundError` - are ignored if the target files or directories do not exist. - :param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted. - If `None`, the value of `AppConfig.TEMP_DIR` is used. - :param file: (Path, optional): Path to the file that should be deleted. - If `None`, no additional file will be deleted. - - Returns: - None: The method does not return any value. - """ - - if AppConfig.TEMP_DIR is not None: - shutil.rmtree(AppConfig.TEMP_DIR) - try: - if file is not None: - Path.unlink(file) - - except FileNotFoundError: - pass - - @staticmethod - def msg_window( - image_path: Path, - image_path2: Path, - w_title: str, - w_txt: str, - txt2: Optional[str] = None, - com: Optional[str] = None, - ) -> None: - """ - Creates message windows - - :param image_path2: - :param image_path: - AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text - AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon - :argument w_title = Windows Title - :argument w_txt = Text for Tk Window - :argument txt2 = Text for Button two - :argument com = function for Button command - """ - msg: tk.Toplevel = tk.Toplevel() - msg.resizable(width=False, height=False) - msg.title(w_title) - msg.configure(pady=15, padx=15) - - # load first image for a window - try: - msg.img = tk.PhotoImage(file=image_path) - msg.i_window = tk.Label(msg, image=msg.img) - except Exception as e: - logging.error(f"Error on load Window Image: {e}", exc_info=True) - msg.i_window = tk.Label(msg, text="Image not found") - - label: tk.Label = tk.Label(msg, text=w_txt) - label.grid(column=1, row=0) - - if txt2 is not None and com is not None: - label.config(font=("Ubuntu", 11), padx=15, justify="left") - msg.i_window.grid(column=0, row=0, sticky="nw") - button2: ttk.Button = ttk.Button( - msg, text=f"{txt2}", command=com, padding=4 - ) - button2.grid(column=0, row=1, sticky="e", columnspan=2) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, row=1, sticky="w", columnspan=2) - else: - label.config(font=("Ubuntu", 11), padx=15) - msg.i_window.grid(column=0, row=0) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, columnspan=2, row=1) - - try: - icon = tk.PhotoImage(file=image_path2) - msg.iconphoto(True, icon) - except Exception as e: - logging.error(f"Error loading the window icon: {e}", exc_info=True) - - msg.columnconfigure(0, weight=1) - msg.rowconfigure(0, weight=1) - msg.winfo_toplevel() - - @staticmethod - def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: - """ - Function for cleanup after a program interruption - - :param file: Optional - File to be deleted - :param file_path: Optional - Directory to be deleted - """ - - def signal_handler(signum: int, frame: Any) -> NoReturn: - """ - Determines clear text names for signal numbers and handles signals - - Args: - signum: The signal number - frame: The current stack frame - - Returns: - NoReturn since the function either exits the program or continues execution - """ - - signals_to_names_dict: Dict[int, str] = dict( - (getattr(signal, n), n) - for n in dir(signal) - if n.startswith("SIG") and "_" not in n - ) - - signal_name: str = signals_to_names_dict.get( - signum, f"Unnamed signal: {signum}" - ) - - # End program for certain signals, report to others only reception - if signum in (signal.SIGINT, signal.SIGTERM): - exit_code: int = 1 - logging.error( - f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.", - exc_info=True, - ) - LxTools.clean_files(file_path, file) - logging.info("Breakdown by user...") - sys.exit(exit_code) - else: - logging.info(f"Signal {signum} received and ignored.") - LxTools.clean_files(file_path, file) - logging.error("Process unexpectedly ended...") - - # Register signal handlers for various signals - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) - - -class Tunnel: - """ - Class of Methods for Wire-Py - """ - - @staticmethod - def parse_files_to_dictionary( - directory: Path = None, filepath: str = None, content: str = None - ) -> tuple[dict, str] | dict | None: - data = {} - - if filepath is not None: - filepath = Path(filepath) - try: - content = filepath.read_text() - - # parse the content - address_line = next( - line for line in content.splitlines() if line.startswith("Address") - ) - dns_line = next( - line for line in content.splitlines() if line.startswith("DNS") - ) - endpoint_line = next( - line for line in content.splitlines() if line.startswith("Endpoint") - ) - private_key_line = next( - line - for line in content.splitlines() - if line.startswith("PrivateKey") - ) - - content = secrets.token_bytes(len(content)) - - # extract the values - address = address_line.split("=")[1].strip() - dns = dns_line.split("=")[1].strip() - endpoint = endpoint_line.split("=")[1].strip() - private_key = private_key_line.split("=")[1].strip() - - # Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters. - original_stem = filepath.stem - truncated_stem = ( - original_stem[-12:] if len(original_stem) > 12 else original_stem - ) - - # save in the dictionary - data[truncated_stem] = { - "Address": address, - "DNS": dns, - "Endpoint": endpoint, - "PrivateKey": private_key, - } - - content = secrets.token_bytes(len(content)) - - except StopIteration: - pass - - elif directory is not None: - - if not directory.exists() or not directory.is_dir(): - logging.error( - "Temp directory does not exist or is not a directory.", - exc_info=True, - ) - return None - - # Get a list of all files in the directory - files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()] - - # Search for the string in the files - for file in files: - try: - content = file.read_text() - # parse the content - address_line = next( - line - for line in content.splitlines() - if line.startswith("Address") - ) - dns_line = next( - line for line in content.splitlines() if line.startswith("DNS") - ) - endpoint_line = next( - line - for line in content.splitlines() - if line.startswith("Endpoint") - ) - - # extract values - address = address_line.split("=")[1].strip() - dns = dns_line.split("=")[1].strip() - endpoint = endpoint_line.split("=")[1].strip() - - # save values to dictionary - data[file.stem] = { - "Address": address, - "DNS": dns, - "Endpoint": endpoint, - } - - except Exception: - # Ignore errors and continue to the next file - continue - if content is not None: - content = secrets.token_bytes(len(content)) - if filepath is not None: - return data, truncated_stem - else: - return data - - @staticmethod - def get_active() -> str: - """ - Shows the Active Tunnel - """ - active = None - try: - process: CompletedProcess[str] = run( - ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], - capture_output=True, - text=True, - check=False, - ) - - active = next( - line.split(":")[0].strip() - for line in process.stdout.splitlines() - if line.endswith("wireguard") - ) - - if process.stderr and "error" in process.stderr.lower(): - logging.error(f"Error output on nmcli: {process.stderr}") - - except StopIteration: - active = None - except Exception as e: - logging.error(f"Error on nmcli: {e}") - active = None - - return active if active is not None else "" - - @staticmethod - def export() -> bool | None: - """ - This will export the tunnels. - A zipfile with the current date and time is created - in the user's home directory with the correct right - """ - now_time: datetime = datetime.now() - now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M") - - try: - AppConfig.ensure_directories() - CryptoUtil.decrypt(getpass.getuser()) - if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0: - - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["sel_tl"], - Msg.STR["tl_first"], - ) - return False - else: - wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}" - try: - shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR) - with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf: - if zf.namelist(): - - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - Msg.STR["exp_succ"], - Msg.STR["exp_in_home"], - ) - else: - logging.error( - "There was a mistake at creating the Zip file. File is empty." - ) - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["exp_err"], - Msg.STR["exp_zip"], - ) - return False - return True - except PermissionError: - logging.error( - f"Permission denied when creating archive in {wg_tar}" - ) - return False - - except zipfile.BadZipFile as e: - logging.error(f"Invalid ZIP file: {e}") - return False - except TypeError: - pass - except Exception as e: - logging.error(f"Export failed: {str(e)}") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - Msg.STR["exp_err"], - Msg.STR["exp_try"], - ) - return False - - finally: - LxTools.clean_files(AppConfig.TEMP_DIR) - AppConfig.ensure_directories() - - -# ConfigManager with caching -class ConfigManager: - """ - Universal class for managing configuration files with caching support. - - This class provides a general solution to load, save, and manage configuration - files across different projects. It uses a caching system to optimize access efficiency. - The `init()` method initializes the configuration file path, while `load()` and `save()` - synchronize data between the file and internal memory structures. - - Key Features: - - Caching to minimize I/O operations. - - Default values for missing or corrupted configuration files. - - Reusability across different projects and use cases. - - The class is designed for central application configuration management, working closely - with `ThemeManager` to dynamically manage themes or other settings. - """ - - _config = None - _config_file = None - - @classmethod - def init(cls, config_file): - """Initial the Configmanager with the given config file""" - cls._config_file = config_file - cls._config = None # Reset the cache - - @classmethod - def load(cls): - """Load the config file and return the config as dict""" - if not cls._config: - try: - lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines() - cls._config = { - "updates": lines[1].strip(), - "theme": lines[3].strip(), - "tooltips": lines[5].strip() - == "True", # is converted here to boolean!!! - "autostart": lines[7].strip() if len(lines) > 7 else "off", - } - except (IndexError, FileNotFoundError): - # DeDefault values in case of error - cls._config = { - "updates": "on", - "theme": "light", - "tooltips": "True", # Default Value as string! - "autostart": "off", - } - return cls._config - - @classmethod - def save(cls): - """Save the config to the config file""" - if cls._config: - lines = [ - "# Configuration\n", - f"{cls._config['updates']}\n", - "# Theme\n", - f"{cls._config['theme']}\n", - "# Tooltips\n", - f"{str(cls._config['tooltips'])}\n", - "# Autostart\n", - f"{cls._config['autostart']}\n", - ] - Path(cls._config_file).write_text("".join(lines), encoding="utf-8") - - @classmethod - def set(cls, key, value): - """Sets a configuration value and saves the change""" - cls.load() - cls._config[key] = value - cls.save() - - @classmethod - def get(cls, key, default=None): - """Returns a configuration value""" - config = cls.load() - return config.get(key, default) - - -class ThemeManager: - """ - Class for central theme management and UI customization. - - This static class allows dynamic adjustment of the application's appearance. - The method `change_theme()` updates the current theme and saves - the selection in the configuration file via `ConfigManager`. - It ensures a consistent visual design across the entire project. - - Key Features: - - Central control over themes. - - Automatic saving of theme settings to the configuration file. - - Tight integration with `ConfigManager` for persistent storage of preferences. - - The class is designed to apply themes consistently throughout the application, - ensuring that changes are traceable and uniform across all parts of the project. - """ - - @staticmethod - def change_theme(root, theme_in_use, theme_name=None): - """Change application theme centrally""" - root.tk.call("set_theme", theme_in_use) - if theme_in_use == theme_name: - ConfigManager.set("theme", theme_in_use) - - -class GiteaUpdate: - """ - Calling download requests the download URL of the running script, - the taskbar image for the “Download OK” window, the taskbar image for the - “Download error” window, and the variable res - """ - - @staticmethod - def api_down(update_api_url: str, version: str, update_setting: str = None) -> str: - """ - Checks for updates via API - - Args: - update_api_url: Update API URL - version: Current version - update_setting: Update setting from ConfigManager (on/off) - - Returns: - New version or status message - """ - # If updates are disabled, return immediately - if update_setting != "on": - return "False" - - try: - response: requests.Response = requests.get(update_api_url, timeout=10) - response.raise_for_status() # Raise exception for HTTP errors - - response_data = response.json() - if not response_data: - return "No Updates" - - latest_version = response_data[0].get("tag_name") - if not latest_version: - return "Invalid API Response" - - # Compare versions (strip 'v. ' prefix if present) - current_version = version[3:] if version.startswith("v. ") else version - - if current_version != latest_version: - return latest_version - else: - return "No Updates" - - except requests.exceptions.RequestException: - return "No Internet Connection!" - except (ValueError, KeyError, IndexError): - return "Invalid API Response" - - @staticmethod - def download( - urld: str, - res: str, - image_path: Path = None, - image_path2: Path = None, - image_path3: Path = None, - image_path4: Path = None, - ) -> None: - """ - Downloads new version of wirepy - - :param urld: Download URL - :param res: Result filename - :param image_path: AppConfig.IMAGE_PATHS["icon_info"]: Image for TK window which is displayed to the left of the text - :param image_path2: AppConfig.IMAGE_PATHS["icon_vpn"]: Image for Task Icon - :param image_path3: AppConfig.IMAGE_PATHS["icon_error"]: Image for TK window which is displayed to the left of the text - :param image_path4: AppConfig.IMAGE_PATHS["icon_msg"]: Image for Task Icon - """ - try: - to_down: str = f"wget -qP {Path.home()} {" "} {urld}" - result: int = subprocess.call(to_down, shell=True) - if result == 0: - shutil.chown(f"{Path.home()}/{res}.zip", 1000, 1000) - - wt: str = _("Download Successful") - msg_t: str = _("Your zip file is in home directory") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - wt, - msg_t, - ) - - else: - - wt: str = _("Download error") - msg_t: str = _("Download failed! Please try again") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - wt, - msg_t, - ) - - except subprocess.CalledProcessError: - - wt: str = _("Download error") - msg_t: str = _("Download failed! No internet connection!") - LxTools.msg_window( - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], - wt, - msg_t, - ) - - -class Tooltip: - """Class for Tooltip - from common_tools.py import Tooltip - example: Tooltip(label, "Show tooltip on label") - example: Tooltip(button, "Show tooltip on button") - example: Tooltip(widget, "Text", state_var=tk.BooleanVar()) - example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10) - - info: label and button are parent widgets. - NOTE: When using with state_var, pass the tk.BooleanVar object directly, - NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get() - """ - - def __init__( - self, - widget: Any, - text: str, - state_var: Optional[tk.BooleanVar] = None, - x_offset: int = 65, - y_offset: int = 40, - ) -> None: - """Tooltip Class""" - self.widget: Any = widget - self.text: str = text - self.tooltip_window: Optional[Toplevel] = None - self.state_var = state_var - self.x_offset = x_offset - self.y_offset = y_offset - - # Initial binding based on the current state - self.update_bindings() - - # Add trace to the state_var if provided - if self.state_var is not None: - self.state_var.trace_add("write", self.update_bindings) - - def update_bindings(self, *args) -> None: - """Updates the bindings based on the current state""" - # Remove existing bindings first - self.widget.unbind("") - self.widget.unbind("") - - # Add new bindings if tooltips are enabled - if self.state_var is None or self.state_var.get(): - self.widget.bind("", self.show_tooltip) - self.widget.bind("", self.hide_tooltip) - - def show_tooltip(self, event: Optional[Any] = None) -> None: - """Shows the tooltip""" - if self.tooltip_window or not self.text: - return - - x: int - y: int - cx: int - cy: int - - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + self.x_offset - y += self.widget.winfo_rooty() + self.y_offset - - self.tooltip_window = tw = tk.Toplevel(self.widget) - tw.wm_overrideredirect(True) - tw.wm_geometry(f"+{x}+{y}") - - label: tk.Label = tk.Label( - tw, - text=self.text, - background="lightgreen", - foreground="black", - relief="solid", - borderwidth=1, - padx=5, - pady=5, - ) - label.grid() - - self.tooltip_window.after(2200, lambda: tw.destroy()) - - def hide_tooltip(self, event: Optional[Any] = None) -> None: - """Hides the tooltip""" - if self.tooltip_window: - self.tooltip_window.destroy() - self.tooltip_window = None diff --git a/install b/install deleted file mode 100755 index cb4818f..0000000 --- a/install +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/bash -NORMAL='\033[0m' -GREEN='\033[1;32m' -RED='\033[31;1;42m' -BLUE='\033[30;1;34m' - -install_file_with(){ - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo apt install python3-tk && \ - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - fi - } - -install_arch_d(){ - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo pacman -S --noconfirm tk python3 python-requests && \ - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - - fi - } - -install(){ - if grep -i 'debian' /etc/os-release > /dev/null 2>&1 - then - groups > /tmp/isgroup - if grep 'sudo' /tmp/isgroup - then - install_file_with - else - echo -e "$BLUE"The installer found that they are not in the group sudo."" - echo -e "with "$RED"su -"$BLUE" "they can enter the root shell in which they then"" - echo -e "enter "$GREEN""usermod -aG sudo $USER.""$BLUE"" - echo -e ""after logging in from the system, they can then run Wire-Py install again." $NORMAL" - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - - elif grep -i 'mint\|ubuntu\|pop|' /etc/os-release > /dev/null 2>&1 - then - install_file_with - - elif grep -i 'arch' /etc/os-release > /dev/null 2>&1 - then - groups > /tmp/isgroup - clear - if grep 'wheel' /tmp/isgroup - then - install_arch_d - else - echo "The installer found that they are not in the group sudo." - echo "The sudoers file must be edited with" - echo -e "$RED""su -""$NORMAL" - echo -e "$GREEN"""EDITOR=nano visudo"""$NORMAL" - echo "Find the line:" - echo "## Uncomment to allow members of group wheel to execute any command" - echo "remove '#' on # %wheel ALL=(ALL) ALL and save the file" - echo -e "then enter "$GREEN"gpasswd -a $USER wheel.""$NORMAL" - echo "after logging in from the system, they can then run Wire-Py install again." - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - - elif grep -i '|manjaro\|garuda\|endeavour|' /etc/os-release > /dev/null 2>&1 - then - install_arch_d - - elif grep -i 'fedora' /etc/os-release > /dev/null 2>&1 - then - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo dnf install python3-tkinter -y - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - - fi - elif grep -i 'suse' /etc/os-release > /dev/null 2>&1 - then - clear - mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \ - mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \ - systemctl --user enable wg_start.service >/dev/null 2>&1 - sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/ - if [ $? -ne 0 ] - then - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - exit 0 - else - sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \ - sudo cp -fv match_found.py /usr/local/bin/ && \ - sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \ - sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \ - sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \ - sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - sudo mkdir -p /usr/local/etc/ssl - if [ ! -f /usr/local/etc/ssl/pwgk.pem ] - then - sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 - fi - if grep -i 'Tumbleweed' /etc/os-release > /dev/null 2>&1 - then - sudo zypper install python313-tk - else - sudo zypper install python36-tk - fi - - fi - - else - clear - echo $"Your System could not be determined." - echo - read -n 1 -s -r -p $"Press Enter to exit" - clear - exit 0 - - fi - #clear - read -n 1 -s -r -p $"Press Enter to exit" - clear - - } - -remove(){ - sudo rm -f /usr/local/bin/wirepy /usr/local/bin/wirepy.py /usr/local/bin/start_wg.py \ - /usr/local/bin/wp_app_config.py common_tools.py /usr/local/bin/ssl_encrypt.py \ - /usr/local/bin/ssl_decrypt.py /usr/local/bin/match_found.py - if [ $? -ne 0 ] - then - exit 0 - else - systemctl --user disable wg_start.service - rm -r ~/.config/wire_py && rm -r ~/.config/systemd - sudo rm /usr/share/applications/Wire-Py.desktop - sudo rm /usr/share/locale/de/LC_MESSAGES/languages/de/wirepy.mo - sudo rm -r /usr/local/etc/ssl - which syncpy >/dev/null - if [ $? -ne 0 ] - then - sudo rm -r /usr/share/icons/lx-icons && sudo rm -r /usr/share/TK-Themes - - fi - - echo - read -p "Press Enter to exit..." - - fi - - } - -which wirepy >/dev/null -if [ $? -eq 0 ] - then - echo "Do you want to update/reinstall or uninstall wirepy?" - echo - echo "Update/reinstall: press y, uninstall press r" - echo - read -n 1 -s -r -p "Cancel with any other key..." result - case $result in - [y]* ) clear; install; exit;; - [Y]* ) clear; install; exit;; - [j]* ) clear; install; exit;; - [J]* ) clear; install; exit;; - [r]* ) clear; remove; exit;; - [R]* ) clear; remove; exit;; - esac - clear -else - install - -fi \ No newline at end of file diff --git a/lx-icons/128/download.png b/lx-icons/128/download.png new file mode 100644 index 0000000..1589350 Binary files /dev/null and b/lx-icons/128/download.png differ diff --git a/lx-icons/128/download_error.png b/lx-icons/128/download_error.png new file mode 100644 index 0000000..011c99e Binary files /dev/null and b/lx-icons/128/download_error.png differ diff --git a/lx-icons/128/log.png b/lx-icons/128/log.png new file mode 100644 index 0000000..06de63c Binary files /dev/null and b/lx-icons/128/log.png differ diff --git a/lx-icons/256/download.png b/lx-icons/256/download.png new file mode 100644 index 0000000..614bf40 Binary files /dev/null and b/lx-icons/256/download.png differ diff --git a/lx-icons/256/download_error.png b/lx-icons/256/download_error.png new file mode 100644 index 0000000..e75bb67 Binary files /dev/null and b/lx-icons/256/download_error.png differ diff --git a/lx-icons/256/log.png b/lx-icons/256/log.png new file mode 100644 index 0000000..3921edb Binary files /dev/null and b/lx-icons/256/log.png differ diff --git a/lx-icons/32/download.png b/lx-icons/32/download.png new file mode 100644 index 0000000..e209fbd Binary files /dev/null and b/lx-icons/32/download.png differ diff --git a/lx-icons/32/download_error.png b/lx-icons/32/download_error.png new file mode 100644 index 0000000..595d04d Binary files /dev/null and b/lx-icons/32/download_error.png differ diff --git a/lx-icons/32/log.png b/lx-icons/32/log.png new file mode 100644 index 0000000..ebc7be1 Binary files /dev/null and b/lx-icons/32/log.png differ diff --git a/lx-icons/32/wg_vpn.png b/lx-icons/32/wg_vpn.png deleted file mode 100644 index 55df4dd..0000000 Binary files a/lx-icons/32/wg_vpn.png and /dev/null differ diff --git a/lx-icons/48/download.png b/lx-icons/48/download.png new file mode 100644 index 0000000..4302a7c Binary files /dev/null and b/lx-icons/48/download.png differ diff --git a/lx-icons/48/download_error.png b/lx-icons/48/download_error.png new file mode 100644 index 0000000..96ec900 Binary files /dev/null and b/lx-icons/48/download_error.png differ diff --git a/lx-icons/48/log.png b/lx-icons/48/log.png new file mode 100644 index 0000000..971a013 Binary files /dev/null and b/lx-icons/48/log.png differ diff --git a/lx-icons/64/download.png b/lx-icons/64/download.png new file mode 100644 index 0000000..cc12d8a Binary files /dev/null and b/lx-icons/64/download.png differ diff --git a/lx-icons/64/download_error.png b/lx-icons/64/download_error.png new file mode 100644 index 0000000..0cd4161 Binary files /dev/null and b/lx-icons/64/download_error.png differ diff --git a/lx-icons/64/log.png b/lx-icons/64/log.png new file mode 100644 index 0000000..e1eb8dc Binary files /dev/null and b/lx-icons/64/log.png differ diff --git a/match_found.py b/match_found.py index 1a73de3..e80954b 100755 --- a/match_found.py +++ b/match_found.py @@ -47,7 +47,8 @@ def search_string_in_directory( def main() -> None: parser = argparse.ArgumentParser( - description="Script only for use to compare the private key in the Network configurations to avoid errors with the network manager." + description="Script only for use to compare the private key in the" + "Network configurations to avoid errors with the network manager." ) parser.add_argument("search_string", help="Search string") args = parser.parse_args() diff --git a/settings b/settings deleted file mode 100644 index 6ef82b9..0000000 --- a/settings +++ /dev/null @@ -1,9 +0,0 @@ -# Configuration -on -# Theme -dark -# Tooltips -True -# Autostart -off - diff --git a/ssl_decrypt.py b/ssl_decrypt.py index 4ce87c2..c3cef08 100755 --- a/ssl_decrypt.py +++ b/ssl_decrypt.py @@ -5,7 +5,7 @@ from pathlib import Path import pwd import shutil from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging +from shared_libs.wp_app_config import AppConfig, logging parser = argparse.ArgumentParser() parser.add_argument("--user", required=True, help="Username of the target file system") diff --git a/ssl_encrypt.py b/ssl_encrypt.py index f3068ba..0a00e7f 100755 --- a/ssl_encrypt.py +++ b/ssl_encrypt.py @@ -6,7 +6,7 @@ from pathlib import Path import pwd import shutil from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging +from shared_libs.wp_app_config import AppConfig, logging parser = argparse.ArgumentParser() parser.add_argument("--user", required=True, help="Username of the target file system") diff --git a/start_wg.py b/start_wg.py index 7583eec..3336915 100755 --- a/start_wg.py +++ b/start_wg.py @@ -2,13 +2,13 @@ """ This script belongs to wirepy and is for the auto start of the tunnel """ - +import logging from subprocess import CompletedProcess, run -from wp_app_config import AppConfig, logging -from common_tools import ConfigManager +from shared_libs.wp_app_config import AppConfig +from shared_libs.common_tools import ConfigManager, LogConfig ConfigManager.init(AppConfig.SETTINGS_FILE) - +LogConfig.logger(ConfigManager.get("logfile")) if ConfigManager.get("autostart") != "off": process: CompletedProcess[str] = run( ["nmcli", "connection", "up", ConfigManager.get("autostart")], diff --git a/tunnel.py b/tunnel.py new file mode 100644 index 0000000..fe4cf9a --- /dev/null +++ b/tunnel.py @@ -0,0 +1,230 @@ +#!/usr/bin/python3 +import logging +import getpass +import zipfile +from datetime import datetime +from pathlib import Path +import shutil +from subprocess import run, CompletedProcess +import secrets +from shared_libs.wp_app_config import AppConfig, Msg +from shared_libs.common_tools import LxTools, CryptoUtil + +# Translate +_ = AppConfig.setup_translations() + + +class Tunnel: + """ + Class of Methods for Wire-Py + """ + + @staticmethod + def parse_files_to_dictionary( + directory: Path = None, filepath: str = None, content: str = None + ) -> tuple[dict, str] | dict | None: + data = {} + + if filepath is not None: + filepath = Path(filepath) + try: + content = filepath.read_text() + + # parse the content + address_line = next( + line for line in content.splitlines() if line.startswith("Address") + ) + dns_line = next( + line for line in content.splitlines() if line.startswith("DNS") + ) + endpoint_line = next( + line for line in content.splitlines() if line.startswith("Endpoint") + ) + private_key_line = next( + line + for line in content.splitlines() + if line.startswith("PrivateKey") + ) + + content = secrets.token_bytes(len(content)) + + # extract the values + address = address_line.split("=")[1].strip() + dns = dns_line.split("=")[1].strip() + endpoint = endpoint_line.split("=")[1].strip() + private_key = private_key_line.split("=")[1].strip() + + # Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters. + original_stem = filepath.stem + truncated_stem = ( + original_stem[-12:] if len(original_stem) > 12 else original_stem + ) + + # save in the dictionary + data[truncated_stem] = { + "Address": address, + "DNS": dns, + "Endpoint": endpoint, + "PrivateKey": private_key, + } + + content = secrets.token_bytes(len(content)) + + except StopIteration: + pass + + elif directory is not None: + + if not directory.exists() or not directory.is_dir(): + logging.error( + "Temp directory does not exist or is not a directory.", + exc_info=True, + ) + return None + + # Get a list of all files in the directory + files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()] + + # Search for the string in the files + for file in files: + try: + content = file.read_text() + # parse the content + address_line = next( + line + for line in content.splitlines() + if line.startswith("Address") + ) + dns_line = next( + line for line in content.splitlines() if line.startswith("DNS") + ) + endpoint_line = next( + line + for line in content.splitlines() + if line.startswith("Endpoint") + ) + + # extract values + address = address_line.split("=")[1].strip() + dns = dns_line.split("=")[1].strip() + endpoint = endpoint_line.split("=")[1].strip() + + # save values to dictionary + data[file.stem] = { + "Address": address, + "DNS": dns, + "Endpoint": endpoint, + } + + except Exception: + # Ignore errors and continue to the next file + continue + if content is not None: + content = secrets.token_bytes(len(content)) + if filepath is not None: + return data, truncated_stem + else: + return data + + @staticmethod + def get_active() -> str: + """ + Shows the Active Tunnel + """ + active = None + try: + process: CompletedProcess[str] = run( + ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"], + capture_output=True, + text=True, + check=False, + ) + + active = next( + line.split(":")[0].strip() + for line in process.stdout.splitlines() + if line.endswith("wireguard") + ) + + if process.stderr and "error" in process.stderr.lower(): + logging.error(f"Error output on nmcli: {process.stderr}") + + except StopIteration: + active = None + except Exception as e: + logging.error(f"Error on nmcli: {e}") + active = None + + return active if active is not None else "" + + @staticmethod + def export() -> bool | None: + """ + This will export the tunnels. + A zipfile with the current date and time is created + in the user's home directory with the correct right + """ + now_time: datetime = datetime.now() + now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M") + + try: + AppConfig.ensure_directories() + CryptoUtil.decrypt(getpass.getuser()) + if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0: + + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_info"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["sel_tl"], + Msg.STR["tl_first"], + ) + return False + else: + wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}" + try: + shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR) + with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf: + if zf.namelist(): + + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_info"], + AppConfig.IMAGE_PATHS["icon_vpn"], + Msg.STR["exp_succ"], + Msg.STR["exp_in_home"], + ) + else: + logging.error( + "There was a mistake at creating the Zip file. File is empty." + ) + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_error"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["exp_err"], + Msg.STR["exp_zip"], + ) + return False + return True + except PermissionError: + logging.error( + f"Permission denied when creating archive in {wg_tar}" + ) + return False + + except zipfile.BadZipFile as e: + logging.error(f"Invalid ZIP file: {e}") + return False + except TypeError: + pass + except Exception as e: + logging.error(f"Export failed: {str(e)}") + LxTools.msg_window( + AppConfig.IMAGE_PATHS["icon_error"], + AppConfig.IMAGE_PATHS["icon_msg"], + Msg.STR["exp_err"], + Msg.STR["exp_try"], + ) + return False + + finally: + LxTools.clean_files(AppConfig.TEMP_DIR) + AppConfig.ensure_directories() diff --git a/wg_start.service b/wg_start.service deleted file mode 100644 index 5d41844..0000000 --- a/wg_start.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Automatic Tunnel Start -After=network-online.target - -[Service] -Type=oneshot -ExecStartPre=/bin/sleep 5 -ExecStart=/usr/local/bin/start_wg.py - -[Install] -WantedBy=default.target diff --git a/wirepy.py b/wirepy.py index ca0f411..3edc8dc 100755 --- a/wirepy.py +++ b/wirepy.py @@ -2,7 +2,7 @@ """ this script is a simple GUI for managing Wireguard Tunnels """ - +import logging import getpass import shutil import sys @@ -11,21 +11,19 @@ import webbrowser from pathlib import Path from subprocess import CompletedProcess, run from tkinter import TclError, filedialog, ttk +from tunnel import Tunnel -from common_tools import ( +from shared_libs.gitea import GiteaUpdate +from shared_libs.common_tools import ( + LxTools, + CryptoUtil, + LogConfig, ConfigManager, ThemeManager, - CryptoUtil, - GiteaUpdate, - Tunnel, Tooltip, - LxTools, ) -from wp_app_config import AppConfig, Msg, logging -AppConfig.ensure_directories() -AppConfig.create_default_settings() -CryptoUtil.decrypt(getpass.getuser()) +from shared_libs.wp_app_config import AppConfig, Msg class Wirepy(tk.Tk): @@ -169,7 +167,11 @@ class FrameWidgets(ttk.Frame): self.settings.add_command( label=self.theme_label.get(), command=self.on_theme_toggle ) - + # Logviewer Menu + self.settings.add_command( + label="Log Viewer", + command=lambda: run(["logviewer", "--modul=wp_app_config"]), + ) # About BTN Menu / Label self.about_btn = ttk.Button( self.menu_frame, text=_("About"), style="Toolbutton", command=self.about @@ -451,12 +453,7 @@ class FrameWidgets(ttk.Frame): self.download.add_command( label=_("Download"), command=lambda: GiteaUpdate.download( - f"{AppConfig.DOWNLOAD_URL}/{res}.zip", - res, - AppConfig.IMAGE_PATHS["icon_info"], - AppConfig.IMAGE_PATHS["icon_vpn"], - AppConfig.IMAGE_PATHS["icon_error"], - AppConfig.IMAGE_PATHS["icon_msg"], + f"{AppConfig.DOWNLOAD_URL}/{res}.zip", res ), ) @@ -1145,10 +1142,14 @@ class FrameWidgets(ttk.Frame): if __name__ == "__main__": - + AppConfig.ensure_directories() + AppConfig.create_default_settings() + CryptoUtil.decrypt(getpass.getuser(), AppConfig.CONFIG_DIR) _ = AppConfig.setup_translations() LxTools.sigi(AppConfig.TEMP_DIR) + window = Wirepy() + LogConfig.logger(ConfigManager.get("logfile")) """ the hidden files are hidden in Filedialog """ diff --git a/wp_app_config.py b/wp_app_config.py old mode 100644 new mode 100755 index e678661..435f9cf --- a/wp_app_config.py +++ b/wp_app_config.py @@ -29,16 +29,10 @@ class AppConfig: """ # Logging - LOG_DIR = Path.home() / ".local/share/wirepy" + LOG_DIR = Path.home() / ".local/share/lxlogs" Path(LOG_DIR).mkdir(parents=True, exist_ok=True) LOG_FILE_PATH = LOG_DIR / "wirepy.log" - logging.basicConfig( - filename=f"{LOG_FILE_PATH}", - level=logging.ERROR, - format="%(asctime)s - %(levelname)s - %(message)s", - ) - # Localization APP_NAME: str = "wirepy" LOCALE_DIR: Path = Path("/usr/share/locale/") @@ -58,6 +52,7 @@ class AppConfig: "# Theme": "dark", "# Tooltips": True, "# Autostart": "off", + "# Logfile": LOG_FILE_PATH, } # Updates @@ -69,6 +64,7 @@ class AppConfig: # UI configuration UI_CONFIG: Dict[str, Any] = { "window_title": "Wire-Py", + "window_title2": "LogViewer", "window_size": (600, 383), "font_family": "Ubuntu", "font_size": 11, @@ -94,6 +90,7 @@ class AppConfig: "icon_stop": "/usr/share/icons/lx-icons/48/wg_vpn-stop.png", "icon_info": "/usr/share/icons/lx-icons/64/info.png", "icon_error": "/usr/share/icons/lx-icons/64/error.png", + "icon_log": "/usr/share/icons/lx-icons/48/log.png", } @staticmethod @@ -160,6 +157,12 @@ class AppConfig: if process.stderr: logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True) + @classmethod + def ensure_log(cls) -> None: + """Ensures that the log file exists""" + if not cls.LOG_FILE_PATH.exists(): + cls.LOG_FILE_PATH.touch() + # here is initializing the class for translation strings _ = AppConfig.setup_translations()