#!/usr/bin/python3 """ this script is a simple GUI for managing Wireguard Tunnels """ import getpass import shutil import sys import tkinter as tk import webbrowser from pathlib import Path from subprocess import CompletedProcess, run from tkinter import TclError, filedialog, ttk from common_tools import ( 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()) class Wirepy(tk.Tk): """ Class Wirepy this is the Main Window of wirepy """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Hide the window initially self.withdraw() self.my_tool_tip = None self.x_width = AppConfig.UI_CONFIG["window_size"][0] self.y_height = AppConfig.UI_CONFIG["window_size"][1] # Set the window size self.geometry(f"{self.x_width}x{self.y_height}") self.resizable( AppConfig.UI_CONFIG["resizable_window"][0], AppConfig.UI_CONFIG["resizable_window"][1], ) self.title(AppConfig.UI_CONFIG["window_title"]) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.tk.call("source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") ConfigManager.init(AppConfig.SETTINGS_FILE) theme = ConfigManager.get("theme") ThemeManager.change_theme(self, theme) # Load the image file from the disk self.wg_icon = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_vpn"]) # Set it as the window icon self.iconphoto(True, self.wg_icon) # Add the widgets FrameWidgets(self).grid() # Center the window on the primary monitor LxTools.center_window_cross_platform(self, self.x_width, self.y_height) # Now show the window after it has been positioned self.after(10, self.deiconify) class FrameWidgets(ttk.Frame): """ ttk frame class for better structure """ def __init__(self, container, **kwargs): super().__init__(container, **kwargs) self.lb_tunnel = None self.btn_stst = None self.endpoint = None self.dns = None self.address = None self.auto_con = None self.style = ttk.Style() self.wg_vpn_start = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_start"]) self.wg_vpn_stop = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_stop"]) self.imp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_import"]) self.tr_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_trash"]) self.exp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_export"]) self.warning_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_error"]) # StringVar-Variables initialization self.tooltip_state = tk.BooleanVar() # Get value from configuration state = ConfigManager.get("tooltips") # NOTE: ConfigManager.get("tooltips") can return either a boolean value or a string, # depending on whether the value was loaded from the file (bool) or the default value is used (string). # The expression 'lines[5].strip() == "True"' in ConfigManager.load() converts the string to a boolean. # Convert to boolean and set if isinstance(state, bool): # If it's already a boolean, use directly self.tooltip_state.set(state) else: # If it's a string or something else self.tooltip_state.set(str(state) == "True") self.tooltip_label = ( tk.StringVar() ) # StringVar-Variable for tooltip label for view Disabled/Enabled self.tooltip_update_label() self.update_label = tk.StringVar() # StringVar-Variable for update label self.update_tooltip = ( tk.StringVar() ) # StringVar-Variable for update tooltip please not remove! self.update_foreground = tk.StringVar(value="red") # Frame for Menu self.menu_frame = ttk.Frame(self) self.menu_frame.configure(relief="flat") self.menu_frame.grid(column=0, row=0, columnspan=4, sticky="w") # App Menu self.version_lb = ttk.Label(self.menu_frame, text=AppConfig.VERSION) self.version_lb.config(font=("Ubuntu", 11), foreground="#00c4ff") self.version_lb.grid(column=0, row=0, rowspan=4, padx=10) Tooltip( self.version_lb, f"Version: {AppConfig.VERSION[2:]}", self.tooltip_state ) self.options_btn = ttk.Menubutton(self.menu_frame, text=_("Options")) self.options_btn.grid(column=1, columnspan=1, row=0) Tooltip(self.options_btn, Msg.TTIP["settings"], self.tooltip_state) self.set_update = tk.IntVar() self.settings = tk.Menu(self, relief="flat") self.options_btn.configure(menu=self.settings, style="Toolbutton") self.settings.add_checkbutton( label=_("Disable Updates"), command=lambda: self.update_setting(self.set_update.get()), variable=self.set_update, ) self.updates_lb = ttk.Label(self.menu_frame, textvariable=self.update_label) self.updates_lb.grid(column=4, columnspan=3, row=0, padx=10) self.updates_lb.grid_remove() self.update_label.trace_add("write", self.update_label_display) self.update_foreground.trace_add("write", self.update_label_display) res = GiteaUpdate.api_down( AppConfig.UPDATE_URL, AppConfig.VERSION, ConfigManager.get("updates") ) self.update_ui_for_update(res) # Tooltip Menu self.settings.add_command( label=self.tooltip_label.get(), command=self.tooltips_toggle ) # Label show dark or light self.theme_label = tk.StringVar() self.update_theme_label() self.settings.add_command( label=self.theme_label.get(), command=self.on_theme_toggle ) # About BTN Menu / Label self.about_btn = ttk.Button( self.menu_frame, text=_("About"), style="Toolbutton", command=self.about ) self.about_btn.grid(column=2, columnspan=2, row=0) self.readme = tk.Menu(self) self.a = Tunnel.get_active() # Label Frame 1 self.lb_frame_btn_lbox = ttk.Frame(self) self.lb_frame_btn_lbox.configure(relief="flat") self.lb_frame_btn_lbox.grid(column=0, rowspan=3, row=1) # Label Frame 2 self.lb_frame = ttk.Frame(self) self.lb_frame.configure(relief="solid") self.lb_frame.grid(column=2, row=2, sticky="snew", padx=20, pady=5) # Label Frame 3 self.lb_frame2 = ttk.Frame(self) self.lb_frame2.configure(relief="solid") self.lb_frame2.grid(column=2, row=3, sticky="snew", padx=20, pady=5) # Bottom Frame 4 self.lb_frame3 = ttk.Frame(self) self.lb_frame3.configure(relief="flat") self.lb_frame3.grid( column=0, row=5, columnspan=4, sticky="snew", padx=2, pady=2 ) # Bottom Frame 5 self.lb_frame4 = ttk.Frame(self) self.lb_frame4.configure(relief="flat") self.lb_frame4.grid(column=2, row=5, columnspan=3, sticky="e", padx=15) # Show active Label self.select_tunnel = None self.lb = ttk.Label(self, text=_("Active: ")) self.lb.config(font=("Ubuntu", 11, "bold")) self.lb.grid(column=2, row=1, padx=15, pady=4, sticky="w") # Label to Show active Tunnel self.str_var = tk.StringVar(value=self.a) self.color_label() # Interface Label self.interface = ttk.Label(self.lb_frame, text=_("Interface")) self.interface.grid(column=0, row=3, sticky="we", padx=120) self.interface.config(font=("Ubuntu", 9)) # Peer Label self.peer = ttk.Label(self.lb_frame2, text=_("Peer")) self.peer.config(font=("Ubuntu", 9)) self.peer.grid(column=0, row=4, sticky="we", padx=130) # Listbox with Scrollbar self.l_box = tk.Listbox(self.lb_frame_btn_lbox, selectmode="single") self.l_box.config(relief="ridge", font=("Ubuntu", 12, "bold")) self.l_box.grid(column=1, rowspan=4, row=0, sticky="ns") self.l_box.event_add("<>", "") self.l_box.bind("<>", self.enable_check_box) self.scrollbar = ttk.Scrollbar( self.lb_frame_btn_lbox, orient="vertical", command=self.l_box.yview ) self.scrollbar.grid(column=1, rowspan=4, row=0, sticky="nse") self.l_box.configure(yscrollcommand=self.scrollbar.set) # Tunnel List self.tl = Tunnel.parse_files_to_dictionary(directory=AppConfig.TEMP_DIR) LxTools.clean_files(AppConfig.TEMP_DIR, file=None) AppConfig.ensure_directories() for tunnels, values in self.tl.items(): self.l_box.insert("end", tunnels) self.l_box.update() # Button Vpn if self.a != "": self.stop() self.handle_tunnel_data(self.a, self.tl) self.show_data() else: self.start() # Address Label self.add = tk.StringVar() self.DNS = tk.StringVar() self.enp = tk.StringVar() self.reset_fields() self.show_data() # Button Import self.btn_i = ttk.Button( self.lb_frame_btn_lbox, image=self.imp_pic, command=self.import_sl, padding=0, ) self.btn_i.grid(column=0, row=1, padx=15, pady=8) Tooltip(self.btn_i, Msg.TTIP["import_tl"], self.tooltip_state) # Button Trash self.btn_tr = ttk.Button( self.lb_frame_btn_lbox, image=self.tr_pic, command=self.delete, padding=0, style="CButton.TButton", ) self.btn_tr.grid(column=0, row=2, padx=15, pady=8) if self.l_box.size() == 0: Tooltip(self.btn_tr, Msg.TTIP["trash_tl_info"], self.tooltip_state) else: Tooltip(self.btn_tr, Msg.TTIP["trash_tl"], self.tooltip_state) # Button Export self.btn_exp = ttk.Button( self.lb_frame_btn_lbox, image=self.exp_pic, command=lambda: Tunnel.export(), padding=0, ) self.btn_exp.grid(column=0, row=3, padx=15, pady=8) if self.l_box.size() == 0: Tooltip(self.btn_exp, Msg.TTIP["export_tl_info"], self.tooltip_state) else: Tooltip(self.btn_exp, Msg.TTIP["export_tl"], self.tooltip_state) # Label Entry self.lb_rename = ttk.Entry(self.lb_frame4, width=20) self.lb_rename.grid(column=2, row=0, padx=8, pady=10, sticky="ne") self.lb_rename.insert(0, _("Max. 12 characters!")) self.lb_rename.config(state="disable") if self.l_box.size() != 0: Tooltip( self.lb_rename, Msg.TTIP["rename_tl"], self.tooltip_state, x_offset=-120, y_offset=-70, ) else: Tooltip( self.lb_rename, Msg.TTIP["rename_tl_info"], self.tooltip_state, x_offset=-180, y_offset=-50, ) # Button Rename self.btn_rename = ttk.Button( self.lb_frame4, text=_("Rename"), state="disable", command=self.tl_rename, padding=4, style="RnButton.TButton", ) self.btn_rename.grid(column=3, row=0, padx=5, pady=10, sticky="ne") # Check Buttons self.selected_option = tk.IntVar() self.autoconnect_var = tk.StringVar() self.autoconnect_var.set(f"{self.auto_con}") # Frame for Labels, Entry and Button self.autoconnect = ttk.Label( self.lb_frame3, textvariable=self.autoconnect_var, width=15 ) self.autoconnect.config(font=("Ubuntu", 11)) self.autoconnect.grid(column=1, row=0, sticky="e", pady=19) self.wg_autostart = ttk.Checkbutton( self.lb_frame3, text=_("Autoconnect on:"), variable=self.selected_option, command=self.box_set, ) self.wg_autostart.grid(column=0, row=0, pady=15, padx=15, sticky="nw") if self.l_box.size() >= 1 and len(self.l_box.curselection()) >= 1: Tooltip( self.wg_autostart, Msg.TTIP["autostart"], self.tooltip_state, x_offset=-10, y_offset=-40, ) if self.l_box.size() == 0: Tooltip( self.wg_autostart, Msg.TTIP["autostart_info"], self.tooltip_state, x_offset=30, y_offset=-50, ) else: Tooltip( self.wg_autostart, Msg.TTIP["autostart"], self.tooltip_state, x_offset=-10, y_offset=-40, ) self.on_off() # Method that is called when the variable changes def update_label_display(self, *args): # Set the foreground color self.updates_lb.configure(foreground=self.update_foreground.get()) # Show or hide the label based on whether it contains text if self.update_label.get(): # Make sure the label is in the correct position every time it's shown self.updates_lb.grid(column=4, columnspan=3, row=0, padx=10) else: self.updates_lb.grid_remove() # Update the labels based on the result def update_ui_for_update(self, res): """Update UI elements based on an update check result""" # First, remove the update button if it exists to avoid conflicts if hasattr(self, "update_btn"): self.update_btn.grid_forget() delattr(self, "update_btn") if res == "False": self.set_update.set(value=1) self.update_label.set(_("Update search off")) self.update_tooltip.set(_("Updates you have disabled")) # Clear the foreground color as requested self.update_foreground.set("") # Set the tooltip for the label Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state) elif res == "No Internet Connection!": self.update_label.set(_("No Server Connection!")) self.update_foreground.set("red") # Set the tooltip for "No Server Connection" Tooltip( self.updates_lb, _("Could not connect to update server"), self.tooltip_state, ) elif res == "No Updates": self.update_label.set(_("No Updates")) self.update_tooltip.set(_("Congratulations! Wire-Py is up to date")) self.update_foreground.set("") # Set the tooltip for the label Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state) else: self.set_update.set(value=0) update_text = f"Update {res} {_('available!')}" # Clear the label text since we'll show the button instead self.update_label.set("") # Create the update button self.update_btn = ttk.Menubutton(self.menu_frame, text=update_text) self.update_btn.grid(column=4, columnspan=3, row=0, padx=0) Tooltip( self.update_btn, _("Click to download new version"), self.tooltip_state ) self.download = tk.Menu(self, relief="flat") self.update_btn.configure(menu=self.download, style="Toolbutton") 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"], ), ) @staticmethod def about() -> None: """ a tk.Toplevel window """ def link_btn() -> None: webbrowser.open("https://git.ilunix.de/punix/Wire-Py") msg_t = _( "Wire-Py a simple Wireguard Gui for Linux systems.\n\n" "Wire-Py is open source software written in Python.\n\n" "Email: polunga40@unity-mail.de also likes for donation.\n\n" "Use without warranty!\n" ) LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_vpn"], AppConfig.IMAGE_PATHS["icon_vpn"], _("Info"), msg_t, _("Go to Wire-Py git"), link_btn, ) def update_setting(self, update_res) -> None: """write off or on in file Args: update_res (int): argument that is passed contains 0 or 1 """ if update_res == 1: # Disable updates ConfigManager.set("updates", "off") # When updates are disabled, we know the result should be "False" self.update_ui_for_update("False") else: # Enable updates ConfigManager.set("updates", "on") # When enabling updates, we need to actually check for updates try: # Force a fresh check by passing "on" as the update setting res = GiteaUpdate.api_down( AppConfig.UPDATE_URL, AppConfig.VERSION, "on" ) # Make sure the UI is updated regardless of the previous state if hasattr(self, "update_btn"): self.update_btn.grid_forget() if hasattr(self, "updates_lb"): self.updates_lb.grid_forget() # Now update the UI with the fresh result self.update_ui_for_update(res) except Exception as e: logging.error(f"Error checking for updates: {e}", exc_info=True) # Fallback to a default message if there's an error self.update_ui_for_update("No Internet Connection!") def tooltip_update_label(self) -> None: """Updates the tooltip menu label based on the current tooltip status""" # Set the menu text based on the current status if self.tooltip_state.get(): # If tooltips are enabled, the menu option should be to disable them self.tooltip_label.set(_("Disable Tooltips")) else: # If tooltips are disabled, the menu option should be to enable them self.tooltip_label.set(_("Enable Tooltips")) def tooltips_toggle(self): """ Toggles the visibility of tooltips (on/off) and updates the corresponding menu label. Inverts the current tooltip state (`self.tooltip_state`), saves the new value in the configuration, and applies the change immediately. Updates the menu entry's label to reflect the new tooltip status (e.g., "Tooltips: On" or "Tooltips: Off"). """ # Toggle the boolean state new_bool_state = not self.tooltip_state.get() # Save the converted value in the configuration ConfigManager.set("tooltips", str(new_bool_state)) # Update the tooltip_state variable for immediate effect self.tooltip_state.set(new_bool_state) # Update the menu label self.tooltip_update_label() # Update the menu entry - find the correct index # This assumes it's the third item (index 2) in your menu self.settings.entryconfigure(1, label=self.tooltip_label.get()) def update_theme_label(self) -> None: """Update the theme label based on the current theme""" current_theme = ConfigManager.get("theme") if current_theme == "light": self.theme_label.set(_("Dark")) else: self.theme_label.set(_("Light")) def on_theme_toggle(self) -> None: """Toggle between light and dark theme""" current_theme = ConfigManager.get("theme") new_theme = "dark" if current_theme == "light" else "light" ThemeManager.change_theme(self, new_theme, new_theme) self.color_label() self.update_theme_label() # Update the theme label # Update Menulfield self.settings.entryconfigure(2, label=self.theme_label.get()) def start(self) -> None: """ Start Button """ self.btn_stst = ttk.Button( self.lb_frame_btn_lbox, image=self.wg_vpn_start, command=lambda: self.wg_switch("start"), padding=0, ) self.btn_stst.grid(column=0, row=0, padx=5, pady=8) if self.l_box.size() == 0: Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state) else: Tooltip(self.btn_stst, Msg.TTIP["start_tl"], self.tooltip_state) def color_label(self) -> None: """ View activ Tunnel in the color green or yellow """ if ConfigManager.get("theme") == "light": self.lb_tunnel = ttk.Label( self, textvariable=self.str_var, foreground="green" ) else: self.lb_tunnel = ttk.Label( self, textvariable=self.str_var, foreground="yellow" ) self.lb_tunnel.config(font=("Ubuntu", 11, "bold")) self.lb_tunnel.grid(column=2, padx=10, row=1) def stop(self) -> None: """ Stop Button """ self.btn_stst = ttk.Button( self.lb_frame_btn_lbox, image=self.wg_vpn_stop, command=lambda: self.wg_switch("stop"), padding=0, ) self.btn_stst.grid(column=0, row=0, padx=5, pady=8) Tooltip(self.btn_stst, Msg.TTIP["stop_tl"], self.tooltip_state) def reset_fields(self) -> None: """ reset data from labels """ fields = [self.add, self.DNS, self.enp] for field in fields: field.set("") def import_sl(self) -> None: """validity check of wireguard config files""" AppConfig.ensure_directories() try: filepath = filedialog.askopenfilename( initialdir=f"{Path.home()}", title="Select Wireguard config File", filetypes=[("WG config files", "*.conf")], ) data_import, key_name = Tunnel.parse_files_to_dictionary(filepath=filepath) if CryptoUtil.find_key(f"{data_import[key_name]["PrivateKey"]}="): LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_error"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["imp_err"], Msg.STR["tl_exist"], ) elif not CryptoUtil.is_valid_base64( f"{data_import[key_name]["PrivateKey"]}=" ): # 2. Second check: Is it valid Base64? LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_error"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["imp_err"], Msg.STR["invalid_base64"], ) else: filepath = Path(filepath) # Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters. original_name = filepath.name truncated_name = ( original_name[-17:] if len(original_name) > 17 else original_name ) import_file = shutil.copy2( filepath, AppConfig.TEMP_DIR / truncated_name ) import_file = Path(import_file) del data_import[key_name]["PrivateKey"] self.tl.update(data_import) if self.a != "": process: CompletedProcess[str] = run( ["nmcli", "connection", "down", self.a], capture_output=True, text=True, check=False, ) if process.stderr: logging.error(f"{process.stderr}: Code {process.returncode}") self.reset_fields() process: CompletedProcess[str] = run( [ "nmcli", "connection", "import", "type", "wireguard", "file", import_file, ], capture_output=True, text=True, check=False, ) if process.stderr: logging.error( f"{process.stderr} Code: {process.returncode}", exc_info=True ) CryptoUtil.encrypt(getpass.getuser()) LxTools.clean_files(AppConfig.TEMP_DIR, file=None) AppConfig.ensure_directories() self.str_var.set("") self.a = Tunnel.get_active() self.l_box.insert(0, self.a) self.wg_autostart.configure(state="normal") self.l_box.selection_clear(0, tk.END) self.l_box.update() self.l_box.selection_set(0) Tooltip( self.wg_autostart, Msg.TTIP["autostart"], self.tooltip_state, x_offset=-10, y_offset=-40, ) Tooltip(self.btn_tr, Msg.TTIP["trash_tl"], self.tooltip_state) Tooltip(self.btn_exp, Msg.TTIP["export_tl"], self.tooltip_state) Tooltip(self.btn_rename, Msg.TTIP["rename_tl"], self.tooltip_state) self.lb_rename.insert(0, "Max. 12 characters!") self.str_var = tk.StringVar() self.str_var.set(self.a) self.color_label() self.stop() self.handle_tunnel_data(self.a, self.tl) self.show_data() process: CompletedProcess[str] = run( ["nmcli", "con", "mod", self.a, "connection.autoconnect", "no"], capture_output=True, text=True, check=False, ) if process.stderr: logging.error(process.stderr, exc_info=True) if process.returncode == 0: print(f">> {import_file.stem} << autostart is disabled by default") except UnboundLocalError: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_error"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["imp_err"], Msg.STR["no_valid_file"], ) except (IsADirectoryError, TypeError, FileNotFoundError): print("File import: abort by user...") except EOFError as e: print(e) def delete(self) -> None: """ delete Wireguard Tunnel """ try: self.select_tunnel = self.l_box.curselection() select_tl = self.l_box.get(self.select_tunnel[0]) process: CompletedProcess[str] = run( ["nmcli", "connection", "delete", select_tl], capture_output=True, text=True, check=False, ) if process.stderr: logging.error( f"{process.stderr} Code: {process.returncode}", exc_info=True ) self.l_box.delete(self.select_tunnel[0]) Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat") if select_tl == ConfigManager.get("autostart"): ConfigManager.set("autostart", "off") self.selected_option.set(0) self.autoconnect_var.set(_("no Autoconnect")) self.wg_autostart.configure(state="disabled") # for disabling checkbox when Listbox empty if self.l_box.size() == 0: self.wg_autostart.configure(state="disabled") self.lb_rename.configure(state="disabled") Tooltip( self.wg_autostart, Msg.TTIP["autostart_info"], self.tooltip_state, x_offset=30, y_offset=-50, ) Tooltip(self.btn_exp, Msg.TTIP["export_tl_info"], self.tooltip_state) Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state) Tooltip(self.lb_rename, Msg.TTIP["rename_tl_info"], self.tooltip_state) self.lb_rename.insert(0, _("Max. 12 characters!")) if self.a != "" and self.a == select_tl: self.str_var.set(value="") self.start() self.l_box.update() self.reset_fields() except IndexError: if self.l_box.size() != 0: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["sel_tl"], Msg.STR["sel_list"], ) else: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["sel_tl"], Msg.STR["tl_first"], ) def enable_check_box(self, _) -> None: """ checkbox for enable autostart Tunnel """ AppConfig.get_autostart_content() if self.l_box.size() != 0: self.wg_autostart.configure(state="normal") self.lb_rename.config(state="normal") self.lb_rename.delete(0, tk.END) self.btn_rename.config(state="normal") def on_off(self) -> None: """ Here it is checked whether the path to the file is there, if not, it is created. Set (on), the selected tunnel is displayed in the label. At (off) the label is first emptied then filled with No Autoconnect """ if ConfigManager.get("autostart") != "off": self.selected_option.set(1) self.autoconnect_var.set("") self.auto_con = ConfigManager.get("autostart") else: self.selected_option.set(0) self.auto_con = _("no Autoconnect") self.autoconnect_var.set("") self.autoconnect_var = tk.StringVar() self.autoconnect_var.set(self.auto_con) self.autoconnect = ttk.Label( self.lb_frame3, textvariable=self.autoconnect_var, foreground="#0071ff", width=15, ) self.autoconnect.config(font=("Ubuntu", 11)) self.autoconnect.grid(column=1, row=0, sticky="e", pady=19) def box_set(self) -> None: """ Configures the autostart for a selected tunnel. This method is called when the user changes the autostart checkbox. It saves the selected tunnel in the configuration file so that it will be automatically connected at system startup. If the checkbox is deactivated, 'off' is written to the configuration file to disable the autostart. """ try: select_tunnel = self.l_box.curselection() select_tl = self.l_box.get(select_tunnel[0]) if self.selected_option.get() == 0: ConfigManager.set("autostart", "off") if self.l_box.size() == 0: self.wg_autostart.configure(state="disabled") if self.selected_option.get() >= 1: ConfigManager.set("autostart", select_tl) except IndexError: self.selected_option.set(1) self.on_off() def tl_rename(self) -> None: """ Method to rename a tunnel. Validates input for length, special characters, and duplicate names, performs the renaming via `nmcli` if valid, updates the configuration file in the directory, and adjusts UI elements such as listboxes and labels. """ special_characters = ["\\", "/", "{", "}", " "] if len(self.lb_rename.get()) > 12: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["ren_err"], Msg.STR["sign_len"], ) elif len(self.lb_rename.get()) == 0: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["ren_err"], Msg.STR["zero_signs"], ) elif any(ch in special_characters for ch in self.lb_rename.get()): LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["ren_err"], Msg.STR["false_signs"], ) elif self.lb_rename.get() in [ file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat") ]: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["ren_err"], Msg.STR["is_in_use"], ) else: try: self.select_tunnel = self.l_box.curselection() select_tl = self.l_box.get(self.select_tunnel[0]) # nmcli connection modify old connection.id iphone process: CompletedProcess[str] = run( [ "nmcli", "connection", "modify", select_tl, "connection.id", self.lb_rename.get(), ], capture_output=True, text=True, check=False, ) if process.stderr: logging.error( f"{process.stderr} Code: {process.returncode}", exc_info=True ) source = Path(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat") destination = AppConfig.CONFIG_DIR / f"{self.lb_rename.get()}.dat" source.replace(destination) self.tl[self.lb_rename.get()] = self.tl.pop(select_tl) if select_tl == ConfigManager.get("autostart"): ConfigManager.set("autostart", self.lb_rename.get()) self.autoconnect_var.set(value=self.lb_rename.get()) self.l_box.delete(self.select_tunnel[0]) self.l_box.insert("end", self.lb_rename.get()) self.l_box.update() self.lb_rename.delete(0, tk.END) self.update_connection_display() except IndexError: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["ren_err"], Msg.STR["sel_list"], ) except EOFError as e: logging.error(e, exc_info=True) def handle_tunnel_data(self, active=None, data=None) -> None: """Processes tunnel data from an active connection and updates UI elements like labels with information about address, DNS, and endpoint. """ tunnel = active values = data[tunnel] # Address Label self.add = tk.StringVar() self.add.set(f"Address: {values['Address']}") self.DNS = tk.StringVar() self.DNS.set(f" DNS: {values['DNS']}") self.enp = tk.StringVar() self.enp.set(f"Endpoint: {values['Endpoint']}") def show_data(self) -> None: """ Displays network-related data (address, DNS, endpoint) in the UI using ttk.Label widgets. Creates three labels for address, DNS, and endpoint with specific styling (color, font), positioning them in a grid layout (`lb_frame` and `lb_frame2`). Each label is linked to a corresponding text variable (`self.add`, `self.DNS`, `self.enp`) for dynamic data updates. """ # Address Label self.address = ttk.Label( self.lb_frame, textvariable=self.add, foreground="#0071ff" ) self.address.grid(column=0, row=5, sticky="w", padx=10, pady=6) self.address.config(font=("Ubuntu", 9)) # DNS Label self.dns = ttk.Label(self.lb_frame, textvariable=self.DNS, foreground="#0071ff") self.dns.grid(column=0, row=7, sticky="w", padx=10, pady=6) self.dns.config(font=("Ubuntu", 9)) # Endpoint Label self.endpoint = ttk.Label( self.lb_frame2, textvariable=self.enp, foreground="#0071ff" ) self.endpoint.grid(column=0, row=8, sticky="w", padx=10, pady=20) self.endpoint.config(font=("Ubuntu", 9)) def wg_switch(self, event=None) -> None: """ Manages switching between active and inactiveVPN connections. If no tunnel is selected (`self.a == ""`), it starts a new connection with the selected tunnel from the listbox (`l_box`). Otherwise, it stops the current connection and updates tunnel data using `handle_tunnel_data`. Handles errors like `IndexError` by displaying appropriate messages if no items are selected or the listbox is empty. """ try: if self.a == "": self.select_tunnel = self.l_box.curselection() select_tl = self.l_box.get(self.select_tunnel[0]) self.handle_connection_state("start", select_tl) else: self.handle_tunnel_data(self.a, self.tl) self.handle_connection_state("stop") except IndexError: if self.l_box.size() != 0: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["sel_tl"], Msg.STR["sel_list"], ) else: LxTools.msg_window( AppConfig.IMAGE_PATHS["icon_info"], AppConfig.IMAGE_PATHS["icon_msg"], Msg.STR["sel_tl"], Msg.STR["tl_first"], ) def handle_connection_state(self, action: str, tunnel_name: str = None) -> None: """ central management for connection states Args: action (str): "start", "stop" or "toggle" tunnel_name (str, optional): name of a tunnel for a start-option. defaults to None. """ if action == "stop": if self.a: process: CompletedProcess[str] = run( ["nmcli", "connection", "down", self.a], capture_output=True, text=True, check=False, ) if process.stderr: logging.error( f"{process.stderr} Code: {process.returncode}", exc_info=True ) self.update_connection_display() self.reset_fields() self.start() elif action == "start": if tunnel_name or self.a: target_tunnel = tunnel_name or self.a process: CompletedProcess[str] = run( ["nmcli", "connection", "up", target_tunnel], capture_output=True, text=True, check=False, ) if process.stderr: logging.error( f"{process.stderr} Code: {process.returncode}", exc_info=True ) self.update_connection_display() self.handle_tunnel_data(self.a, self.tl) self.show_data() self.color_label() self.stop() elif action == "toggle": if self.a: self.handle_connection_state("stop") else: self.handle_connection_state("start") def update_connection_display(self) -> None: """ Updated the display after connection changes """ self.a = Tunnel.get_active() if not hasattr(self, "str_var"): self.str_var = tk.StringVar() self.str_var.set(self.a) self.color_label() self.show_data() if __name__ == "__main__": _ = AppConfig.setup_translations() LxTools.sigi(AppConfig.TEMP_DIR) window = Wirepy() """ the hidden files are hidden in Filedialog """ try: window.tk.call("tk_getOpenFile", "-foobarbaz") except TclError: pass window.tk.call("set", "::tk::dialog::file::showHiddenBtn", "0") window.tk.call("set", "::tk::dialog::file::showHiddenVar", "0") window.mainloop() LxTools.clean_files(AppConfig.TEMP_DIR) sys.exit(0)