import getpass import shutil import tkinter as tk from functools import partial from pathlib import Path from subprocess import CompletedProcess, run from tkinter import ttk from shared_libs.custom_file_dialog import CustomFileDialog from shared_libs.common_tools import ( LxTools, CryptoUtil, ConfigManager, ThemeManager, ) from shared_libs.message import MessageDialog from shared_libs.wp_app_config import AppConfig, Msg from tunnel import Tunnel from .header import Header from .menu_bar import MenuBar from .controls import Controls from .tunnel_list import TunnelList from .tunnel_details import TunnelDetails from .status_panel import StatusPanel from logger import app_logger class MainFrame(ttk.Frame): def __init__(self, container, image_manager, toggle_log_window, **kwargs): super().__init__(container, **kwargs) self.image_manager = image_manager self.columnconfigure(1, weight=0) self.columnconfigure(2, weight=18) self.rowconfigure(2, weight=1) self.tooltip_state = tk.BooleanVar() state = ConfigManager.get("tooltips") self.tooltip_state.set(str(state) == "True") self.active_tunnel_name = Tunnel.get_active() self.tunnels = Tunnel.parse_files_to_dictionary( directory=AppConfig.TEMP_DIR) if self.tunnels is None: self.tunnels = {} LxTools.clean_files(AppConfig.TEMP_DIR, file=None) AppConfig.ensure_directories() # Create components self.header = Header(self, self.image_manager) self.menu_bar = MenuBar(self, self.image_manager, self.tooltip_state, self.on_theme_toggle, toggle_log_window) self.controls = Controls( self, self.image_manager, self.tooltip_state, self.import_sl, self.delete, self.wg_switch) self.tunnel_list = TunnelList(self, self.on_tunnel_select) self.tunnel_details = TunnelDetails(self) self.status_panel = StatusPanel( self, self.tooltip_state, self.tl_rename, self.box_set) # Layout components self.header.grid(column=0, columnspan=3, row=0, sticky="nsew") self.menu_bar.grid(column=0, columnspan=3, row=1, sticky="we") self.controls.grid(column=0, row=2, sticky="nsew", padx=(15, 0)) self.tunnel_list.grid(column=1, row=2, sticky="nsew") self.tunnel_details.grid(column=2, row=2, sticky="nsew") self.status_panel.grid(column=0, row=3, columnspan=3, sticky="nsew") self.tunnel_list.populate(self.tunnels.keys()) self.update_ui_state() def on_theme_toggle(self): current_theme = ConfigManager.get("theme") new_theme = "dark" if current_theme == "light" else "light" ThemeManager.change_theme(self, new_theme, new_theme) self.header.header_label.config(fg="#ffffff") self.tunnel_details.color_label() self.menu_bar.update_theme() self.menu_bar.settings.entryconfigure( 2, label=self.menu_bar.theme_label.get()) def update_ui_state(self): self.active_tunnel_name = Tunnel.get_active() self.controls.set_start_stop_button_state( bool(self.active_tunnel_name), self.tunnel_list.get_size()) self.controls.update_tooltips(self.tunnel_list.get_size()) self.tunnel_details.update_details( self.active_tunnel_name, self.tunnels) self.status_panel.enable_controls( self.tunnel_list.get_size(), bool(self.tunnel_list.get_selected())) self.status_panel.update_autoconnect_display() def on_tunnel_select(self, event=None): self.status_panel.enable_controls(self.tunnel_list.get_size(), True) def wg_switch(self): try: if not self.active_tunnel_name: selected_tunnel = self.tunnel_list.get_selected() if not selected_tunnel: MessageDialog( "info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) return self.handle_connection_state("start", selected_tunnel) else: self.handle_connection_state("stop") except IndexError: MessageDialog("info", Msg.STR["tl_first"], title=Msg.STR["sel_tl"]) def handle_connection_state(self, action: str, tunnel_name: str = None): cmd = [] if action == "stop": if self.active_tunnel_name: cmd = ["nmcli", "connection", "down", self.active_tunnel_name] elif action == "start" and tunnel_name: cmd = ["nmcli", "connection", "up", tunnel_name] if cmd: process: CompletedProcess[str] = run( cmd, capture_output=True, text=True, check=False) if process.stderr: app_logger.log(f"{process.stderr} Code: {process.returncode}") self.update_ui_state() def import_sl(self): try: dialog = CustomFileDialog( self, initial_dir=f"{Path.home()}", title="Select Wireguard config File", filetypes=[("WG config files", "*.conf")], dialog_mode="open" ) self.wait_window(dialog) filepath = dialog.get_selected_file() if not filepath: return data_import, key_name = Tunnel.parse_files_to_dictionary( filepath=filepath) if CryptoUtil.find_key(f"{data_import[key_name]['PrivateKey']}="): MessageDialog( "error", Msg.STR["tl_exist"], title=Msg.STR["imp_err"]) return if not CryptoUtil.is_valid_base64(f"{data_import[key_name]['PrivateKey']}="): MessageDialog( "error", Msg.STR["invalid_base64"], title=Msg.STR["imp_err"]) return filepath = Path(filepath) truncated_name = ( filepath.name[-17:] if len(filepath.name) > 17 else filepath.name) import_file = shutil.copy2( filepath, AppConfig.TEMP_DIR / truncated_name) import_file = Path(import_file) if self.active_tunnel_name: self.handle_connection_state("stop") process: CompletedProcess[str] = run( ["nmcli", "connection", "import", "type", "wireguard", "file", import_file], capture_output=True, text=True, check=False) if process.stderr: app_logger.log(f"{process.stderr} Code: {process.returncode}") CryptoUtil.encrypt(getpass.getuser()) CryptoUtil.decrypt(getpass.getuser()) # Decrypt all files again self.active_tunnel_name = Tunnel.get_active() self.tunnels = Tunnel.parse_files_to_dictionary( directory=AppConfig.TEMP_DIR) # Read from TEMP_DIR if self.tunnels is None: self.tunnels = {} self.tunnel_list.populate(self.tunnels.keys()) self.tunnel_list.set_selection(0) run(["nmcli", "con", "mod", self.active_tunnel_name, "connection.autoconnect", "no"], check=False) self.update_ui_state() LxTools.clean_files(AppConfig.TEMP_DIR, file=None) except (UnboundLocalError, TypeError, FileNotFoundError): MessageDialog( "error", Msg.STR["no_valid_file"], title=Msg.STR["imp_err"]) except Exception as e: app_logger.log(f"Import failed: {e}") def delete(self): try: select_tl = self.tunnel_list.get_selected() if not select_tl: MessageDialog( "info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) return run(["nmcli", "connection", "delete", select_tl], check=False) Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat") if select_tl == ConfigManager.get("autostart"): ConfigManager.set("autostart", "off") self.tunnel_list.delete_selected() del self.tunnels[select_tl] self.update_ui_state() except IndexError: MessageDialog("info", Msg.STR["tl_first"], title=Msg.STR["sel_tl"]) def box_set(self): try: select_tl = self.tunnel_list.get_selected() if self.status_panel.selected_option.get() == 1 and select_tl: ConfigManager.set("autostart", select_tl) else: ConfigManager.set("autostart", "off") except IndexError: self.status_panel.selected_option.set(0) self.update_ui_state() def tl_rename(self): special_characters = ["\\", "/", "{", "}", " "] new_name = self.status_panel.get_rename_value() if len(new_name) > 12: MessageDialog("info", Msg.STR["sign_len"], title=Msg.STR["ren_err"]) return if len(new_name) == 0: MessageDialog( "info", Msg.STR["zero_signs"], title=Msg.STR["ren_err"]) return if any(ch in special_characters for ch in new_name): MessageDialog( "info", Msg.STR["false_signs"], title=Msg.STR["ren_err"]) return if new_name in self.tunnels: MessageDialog( "info", Msg.STR["is_in_use"], title=Msg.STR["ren_err"]) return try: old_name = self.tunnel_list.get_selected() if not old_name: MessageDialog( "info", Msg.STR["sel_list"], title=Msg.STR["ren_err"]) return run(["nmcli", "connection", "modify", old_name, "connection.id", new_name], check=False) source = Path(f"{AppConfig.CONFIG_DIR}/{old_name}.dat") destination = AppConfig.CONFIG_DIR / f"{new_name}.dat" source.replace(destination) self.tunnels[new_name] = self.tunnels.pop(old_name) if old_name == ConfigManager.get("autostart"): ConfigManager.set("autostart", new_name) self.tunnel_list.delete_selected() self.tunnel_list.populate(self.tunnels.keys()) self.status_panel.clear_rename_entry() self.update_ui_state() except IndexError: MessageDialog("info", Msg.STR["sel_list"], title=Msg.STR["ren_err"])