ui fixes expand widget on x and y works

This commit is contained in:
2025-08-10 20:51:12 +02:00
parent 8fcad26789
commit 052ed0a828
19 changed files with 930 additions and 1181 deletions

12
logger.py Normal file
View File

@@ -0,0 +1,12 @@
class Logger:
def __init__(self):
self._log_func = print # Default to print if not initialized
def init_logger(self, log_func):
self._log_func = log_func
def log(self, message):
self._log_func(message)
# Global instance
app_logger = Logger()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -2,27 +2,27 @@
""" This Script encrypt Wireguardfiles for Wirepy users for more Security """ """ This Script encrypt Wireguardfiles for Wirepy users for more Security """
import argparse import argparse
import logging
from pathlib import Path from pathlib import Path
import pwd import pwd
import shutil import shutil
from subprocess import CompletedProcess, run from subprocess import CompletedProcess, run
from shared_libs.wp_app_config import AppConfig from shared_libs.wp_app_config import AppConfig
from shared_libs.common_tools import LogConfig
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--user", required=True, parser.add_argument("--user", required=True,
help="Username of the target file system") help="Username of the target file system")
args = parser.parse_args() args = parser.parse_args()
LogConfig.logger(f"/home/{args.user}/.local/share/lxlogs/wirepy.log")
try: try:
# Retrieve UID and GID # Retrieve UID and GID
user_info = pwd.getpwnam(args.user) user_info = pwd.getpwnam(args.user)
uid = user_info.pw_uid # User ID (e.g., 1000) uid = user_info.pw_uid # User ID (e.g., 1000)
gid = user_info.pw_gid # Group ID (e.g., 1000) gid = user_info.pw_gid # Group ID (e.g., 1000)
except KeyError: except KeyError:
logging.error(f"User '{args.user}' not found.")
exit(1) exit(1)
keyfile: Path = Path(f"/home/{args.user}/.config/wire_py/pbwgk.pem") keyfile: Path = Path(f"/home/{args.user}/.config/wire_py/pbwgk.pem")
@@ -50,13 +50,7 @@ if not keyfile.is_file():
# Output from Openssl Error # Output from Openssl Error
if process.stderr: if process.stderr:
if "writing RSA key" in process.stderr: pass
logging.info(f"{process.stderr}")
else:
logging.error(f"{process.stderr}")
if process.returncode == 0:
logging.info("Public key generated successfully.")
shutil.chown(keyfile, uid, gid) shutil.chown(keyfile, uid, gid)
@@ -86,4 +80,4 @@ if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
# Output from Openssl Error # Output from Openssl Error
if process.stderr: if process.stderr:
logging.error(process.stderr) pass

View File

@@ -2,13 +2,13 @@
""" """
This script belongs to wirepy and is for the auto start of the tunnel This script belongs to wirepy and is for the auto start of the tunnel
""" """
import logging
from subprocess import CompletedProcess, run from subprocess import CompletedProcess, run
from shared_libs.wp_app_config import AppConfig from shared_libs.wp_app_config import AppConfig
from shared_libs.common_tools import ConfigManager, LogConfig from shared_libs.common_tools import ConfigManager
ConfigManager.init(AppConfig.SETTINGS_FILE) ConfigManager.init(AppConfig.SETTINGS_FILE)
LogConfig.logger(ConfigManager.get("logfile"))
if ConfigManager.get("autostart") != "off": if ConfigManager.get("autostart") != "off":
process: CompletedProcess[str] = run( process: CompletedProcess[str] = run(
["nmcli", "connection", "up", ConfigManager.get("autostart")], ["nmcli", "connection", "up", ConfigManager.get("autostart")],
@@ -18,7 +18,7 @@ if ConfigManager.get("autostart") != "off":
) )
# Output from start_wg error # Output from start_wg error
if process.stderr: if process.stderr:
logging.error(process.stderr)
else: else:
pass pass

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
import logging from logger import app_logger
import getpass import getpass
import zipfile import zipfile
from datetime import datetime from datetime import datetime
@@ -25,6 +25,7 @@ class Tunnel:
if filepath is not None: if filepath is not None:
filepath = Path(filepath) filepath = Path(filepath)
truncated_stem = None
try: try:
content = filepath.read_text() content = filepath.read_text()
@@ -70,17 +71,17 @@ class Tunnel:
content = secrets.token_bytes(len(content)) content = secrets.token_bytes(len(content))
except StopIteration: except StopIteration:
pass return None, None
elif directory is not None: elif directory is not None:
if not directory.exists() or not directory.is_dir(): if not directory.exists() or not directory.is_dir():
logging.error( app_logger.log(
"Temp directory does not exist or is not a directory.") "Temp directory does not exist or is not a directory.")
return None return None
# Get a list of all files in the directory # Get a list of all files in the directory
files = [file for file in AppConfig.TEMP_DIR.iterdir() files = [file for file in directory.iterdir()
if file.is_file()] if file.is_file()]
# Search for the string in the files # Search for the string in the files
@@ -146,12 +147,12 @@ class Tunnel:
) )
if process.stderr and "error" in process.stderr.lower(): if process.stderr and "error" in process.stderr.lower():
logging.error(f"Error output on nmcli: {process.stderr}") app_logger.log(f"Error output on nmcli: {process.stderr}")
except StopIteration: except StopIteration:
active = None active = None
except Exception as e: except Exception as e:
logging.error(f"Error on nmcli: {e}") app_logger.log(f"Error on nmcli: {e}")
active = None active = None
return active if active is not None else "" return active if active is not None else ""
@@ -189,7 +190,7 @@ class Tunnel:
) )
else: else:
logging.error( app_logger.log(
"There was a mistake at creating the Zip file. File is empty." "There was a mistake at creating the Zip file. File is empty."
) )
MessageDialog( MessageDialog(
@@ -199,18 +200,18 @@ class Tunnel:
return False return False
return True return True
except PermissionError: except PermissionError:
logging.error( app_logger.log(
f"Permission denied when creating archive in {wg_tar}" f"Permission denied when creating archive in {wg_tar}"
) )
return False return False
except zipfile.BadZipFile as e: except zipfile.BadZipFile as e:
logging.error(f"Invalid ZIP file: {e}") app_logger.log(f"Invalid ZIP file: {e}")
return False return False
except TypeError: except TypeError:
pass pass
except Exception as e: except Exception as e:
logging.error(f"Export failed: {str(e)}") app_logger.log(f"Export failed: {str(e)}")
MessageDialog( MessageDialog(
"error", Msg.STR["exp_try"], title=Msg.STR["exp_err"]) "error", Msg.STR["exp_try"], title=Msg.STR["exp_err"])
return False return False

0
ui/__init__.py Normal file
View File

87
ui/controls.py Normal file
View File

@@ -0,0 +1,87 @@
import tkinter as tk
from tkinter import ttk
from shared_libs.common_tools import Tooltip
from shared_libs.wp_app_config import Msg
from tunnel import Tunnel
class Controls(ttk.Frame):
def __init__(self, container, image_manager, tooltip_state, import_callback, delete_callback, start_stop_callback, **kwargs):
super().__init__(container, **kwargs)
self.image_manager = image_manager
self.tooltip_state = tooltip_state
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, weight=1)
self.rowconfigure(3, weight=1)
self.btn_stst = ttk.Button(
self,
image=self.image_manager.get_icon("vpn_start_large"),
command=start_stop_callback,
padding=0,
)
self.btn_stst.grid(column=0, row=0, sticky="ew")
Tooltip(self.btn_stst, Msg.TTIP["start_tl"],
state_var=self.tooltip_state)
self.btn_i = ttk.Button(
self,
image=self.image_manager.get_icon("import_large"),
command=import_callback,
padding=0,
)
self.btn_i.grid(column=0, row=1, sticky="ew")
Tooltip(self.btn_i, Msg.TTIP["import_tl"],
state_var=self.tooltip_state)
self.btn_tr = ttk.Button(
self,
image=self.image_manager.get_icon("trash_large"),
command=delete_callback,
padding=0,
)
self.btn_tr.grid(column=0, row=2, sticky="ew")
Tooltip(self.btn_tr, Msg.TTIP["trash_tl"],
state_var=self.tooltip_state)
self.btn_exp = ttk.Button(
self,
image=self.image_manager.get_icon("export_large"),
command=lambda: Tunnel.export(),
padding=0,
)
self.btn_exp.grid(column=0, row=3, sticky="ew")
Tooltip(self.btn_exp, Msg.TTIP["export_tl"],
state_var=self.tooltip_state)
def set_start_stop_button_state(self, is_active, list_box_size):
if is_active:
self.btn_stst.config(
image=self.image_manager.get_icon("vpn_stop_large"))
Tooltip(self.btn_stst,
Msg.TTIP["stop_tl"], state_var=self.tooltip_state)
else:
self.btn_stst.config(
image=self.image_manager.get_icon("vpn_start_large"))
if list_box_size == 0:
Tooltip(self.btn_stst,
Msg.TTIP["empty_list"], state_var=self.tooltip_state)
else:
Tooltip(self.btn_stst,
Msg.TTIP["start_tl"], state_var=self.tooltip_state)
def update_tooltips(self, list_box_size):
if list_box_size == 0:
Tooltip(self.btn_tr,
Msg.TTIP["trash_tl_info"], state_var=self.tooltip_state)
Tooltip(self.btn_exp,
Msg.TTIP["export_tl_info"], state_var=self.tooltip_state)
else:
Tooltip(self.btn_tr, Msg.TTIP["trash_tl"],
state_var=self.tooltip_state)
Tooltip(self.btn_exp,
Msg.TTIP["export_tl"], state_var=self.tooltip_state)

58
ui/header.py Normal file
View File

@@ -0,0 +1,58 @@
import tkinter as tk
from shared_libs.wp_app_config import AppConfig, Msg
class Header(tk.Frame):
def __init__(self, container, image_manager, **kwargs):
super().__init__(container, bg="#2c3e50", **kwargs)
self.image_manager = image_manager
wg_icon_header_frame = tk.Frame(self, bg="#2c3e50")
wg_icon_header_frame.grid(column=0, row=0, rowspan=2, sticky="w")
wg_icon_header_label = tk.Label(
wg_icon_header_frame,
image=self.image_manager.get_icon("vpn_small"),
bg="#2c3e50",
)
wg_icon_header_label.grid(column=0, row=0, sticky="e", ipadx=10)
self.header_label = tk.Label(
self,
text=Msg.STR["lx_tools"],
font=("Helvetica", 12, "bold"),
fg="#ffffff",
bg="#2c3e50",
)
self.header_label.grid(
column=1,
row=0,
sticky="w",
padx=(5, 20),
pady=(15, 5),
ipady=4,
)
self.version_label = tk.Label(
self,
text=f"{AppConfig.VERSION}{Msg.STR['header_left_bottom']}",
font=("Helvetica", 9),
fg="#bdc3c7",
bg="#2c3e50",
)
self.version_label.grid(
column=1, row=1, sticky="w", padx=(5, 20), pady=(0, 10))
info_label = tk.Label(
self,
text=Msg.STR["header_right_top"],
font=("Helvetica", 10),
fg="#ecf0f1",
bg="#2c3e50",
)
info_label.grid(column=2, row=0, sticky="ne",
padx=(10, 10), pady=(10, 0))
self.columnconfigure(1, weight=1, pad=2)
self.rowconfigure(0, weight=1)

47
ui/log_window.py Normal file
View File

@@ -0,0 +1,47 @@
import tkinter as tk
from tkinter import ttk
from datetime import datetime
class LogWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Log")
self.geometry("600x400")
self.protocol("WM_DELETE_WINDOW", self.withdraw) # Hide on close
log_container = tk.Frame(self)
log_container.pack(fill="both", expand=True, padx=10, pady=10)
self.log_text = tk.Text(
log_container,
wrap=tk.WORD,
font=("Consolas", 9),
bg="#1e1e1e",
fg="#d4d4d4",
insertbackground="white",
selectbackground="#264f78",
)
log_scrollbar = ttk.Scrollbar(
log_container, orient="vertical", command=self.log_text.yview
)
self.log_text.configure(yscrollcommand=log_scrollbar.set)
self.log_text.pack(side="left", fill="both", expand=True)
log_scrollbar.pack(side="right", fill="y")
self.withdraw() # Start hidden
def log_message(self, message):
"""Add message to log"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] {message}\n"
self.log_text.insert(tk.END, log_entry)
self.log_text.see(tk.END)
self.log_text.update()
def toggle(self):
if self.winfo_viewable():
self.withdraw()
else:
self.deiconify()

272
ui/main_frame.py Normal file
View File

@@ -0,0 +1,272 @@
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=1)
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_label()
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"])

179
ui/menu_bar.py Normal file
View File

@@ -0,0 +1,179 @@
import os
import subprocess
import tkinter as tk
import webbrowser
from functools import partial
from pathlib import Path
from tkinter import ttk
from logger import app_logger
from shared_libs.common_tools import ConfigManager, Tooltip
from shared_libs.gitea import GiteaUpdate
from shared_libs.message import MessageDialog
from shared_libs.wp_app_config import AppConfig, Msg
class MenuBar(ttk.Frame):
def __init__(self, container, image_manager, tooltip_state, on_theme_toggle, toggle_log_window, **kwargs):
super().__init__(container, **kwargs)
self.image_manager = image_manager
self.tooltip_state = tooltip_state
self.on_theme_toggle = on_theme_toggle
self.options_btn = ttk.Menubutton(self, text=Msg.STR["options"])
self.options_btn.grid(column=0, row=0)
Tooltip(self.options_btn, Msg.TTIP["settings"], state_var=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=Msg.STR["disable_updates"],
command=lambda: self.update_setting(self.set_update.get()),
variable=self.set_update,
)
self.update_label = tk.StringVar()
self.updates_lb = ttk.Label(self, textvariable=self.update_label)
self.updates_lb.grid(column=2, row=0)
self.updates_lb.grid_remove()
self.update_tooltip = tk.StringVar()
self.update_foreground = tk.StringVar(value="red")
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.tooltip_label = tk.StringVar()
self.tooltip_update_label()
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, text=Msg.STR["about"], style="Toolbutton", command=self.about
)
self.about_btn.grid(column=1, row=0)
self.columnconfigure(10, weight=1) # Add a column with weight to push button to the right
self.log_btn = ttk.Button(
self,
image=self.image_manager.get_icon("log_small"),
style="Toolbutton",
command=toggle_log_window,
)
self.log_btn.grid(column=11, row=0, sticky='e')
Tooltip(self.log_btn, "Show Log", state_var=self.tooltip_state)
def update_label_display(self, *args):
self.updates_lb.configure(foreground=self.update_foreground.get())
if self.update_label.get():
self.updates_lb.grid(column=4, columnspan=3, row=0, padx=10)
else:
self.updates_lb.grid_remove()
def updater(self):
tmp_dir = Path("/tmp/lxtools")
Path.mkdir(tmp_dir, exist_ok=True)
os.chdir(tmp_dir)
result = subprocess.run(["/usr/local/bin/lxtools_installer"], check=False)
if result.returncode != 0:
MessageDialog("error", result.stderr)
def update_ui_for_update(self, res):
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(Msg.STR["update_search_off"])
self.update_tooltip.set(Msg.TTIP["updates_disabled"])
self.update_foreground.set("")
Tooltip(self.updates_lb, self.update_tooltip.get(), state_var=self.tooltip_state)
elif res == "No Internet Connection!":
self.update_label.set(Msg.STR["no_server_connection"])
self.update_foreground.set("red")
Tooltip(self.updates_lb, Msg.TTIP["no_server_conn_tt"], state_var=self.tooltip_state)
elif res == "No Updates":
self.update_label.set(Msg.STR["no_updates"])
self.update_tooltip.set(Msg.TTIP["up_to_date"])
self.update_foreground.set("")
Tooltip(self.updates_lb, self.update_tooltip.get(), state_var=self.tooltip_state)
else:
self.set_update.set(value=0)
self.update_label.set("")
self.update_btn = ttk.Button(
self,
image=self.image_manager.get_icon("settings"),
style="Toolbutton",
command=self.updater,
)
self.update_btn.grid(column=5, row=0, padx=0)
Tooltip(self.update_btn, Msg.TTIP["install_new_version"], state_var=self.tooltip_state)
@staticmethod
def about() -> None:
MessageDialog(
"info",
Msg.STR["about_msg"],
buttons=["OK", Msg.STR["goto_git"]],
title=Msg.STR["info"],
commands=[
None,
partial(webbrowser.open, "https://git.ilunix.de/punix/Wire-Py"),
],
icon="/usr/share/icons/lx-icons/64/wg_vpn.png",
wraplength=420,
)
def update_setting(self, update_res) -> None:
if update_res == 1:
ConfigManager.set("updates", "off")
self.update_ui_for_update("False")
else:
ConfigManager.set("updates", "on")
try:
res = GiteaUpdate.api_down(AppConfig.UPDATE_URL, AppConfig.VERSION, "on")
if hasattr(self, "update_btn"):
self.update_btn.grid_forget()
if hasattr(self, "updates_lb"):
self.updates_lb.grid_forget()
self.update_ui_for_update(res)
except Exception as e:
app_logger.log(f"Error checking for updates: {e}")
self.update_ui_for_update("No Internet Connection!")
def tooltip_update_label(self) -> None:
if self.tooltip_state.get():
self.tooltip_label.set(Msg.STR["disable_tooltips"])
else:
self.tooltip_label.set(Msg.STR["enable_tooltips"])
def tooltips_toggle(self):
new_bool_state = not self.tooltip_state.get()
ConfigManager.set("tooltips", str(new_bool_state))
self.tooltip_state.set(new_bool_state)
self.tooltip_update_label()
self.settings.entryconfigure(1, label=self.tooltip_label.get())
def update_theme_label(self) -> None:
current_theme = ConfigManager.get("theme")
if current_theme == "light":
self.theme_label.set(Msg.STR["dark"])
else:
self.theme_label.set(Msg.STR["light"])

100
ui/status_panel.py Normal file
View File

@@ -0,0 +1,100 @@
import tkinter as tk
from tkinter import ttk
from shared_libs.common_tools import Tooltip, ConfigManager
from shared_libs.wp_app_config import Msg
class StatusPanel(ttk.Frame):
def __init__(self, container, tooltip_state, rename_callback, autoconnect_callback, **kwargs):
super().__init__(container, **kwargs)
self.columnconfigure(1, weight=1)
self.tooltip_state = tooltip_state
# Autoconnect Frame
autoconnect_frame = ttk.Frame(self)
autoconnect_frame.grid(column=0, row=0, sticky="w")
self.selected_option = tk.IntVar()
self.wg_autostart = ttk.Checkbutton(
autoconnect_frame,
text=Msg.STR["autoconnect_on"],
variable=self.selected_option,
command=autoconnect_callback,
)
self.wg_autostart.grid(column=0, row=0, pady=5,
padx=(10, 0), sticky="ew")
Tooltip(self.wg_autostart,
Msg.TTIP["autostart_info"], state_var=self.tooltip_state)
self.autoconnect_var = tk.StringVar()
self.autoconnect_label = ttk.Label(
autoconnect_frame,
textvariable=self.autoconnect_var,
foreground="#0071ff",
width=18,
)
self.autoconnect_label.config(font=("Ubuntu", 11))
self.autoconnect_label.grid(column=1, row=0, sticky="ew", pady=5)
# Rename Frame
rename_frame = ttk.Frame(self)
rename_frame.grid(column=1, row=0, sticky="e", padx=10)
self.lb_rename = ttk.Entry(rename_frame)
self.lb_rename.grid(column=0, row=0, padx=8, pady=10, sticky="ne")
self.lb_rename.config(width=15)
self.lb_rename.insert(0, Msg.STR["max_chars"])
self.lb_rename.config(state="disable")
Tooltip(self.lb_rename,
Msg.TTIP["rename_tl_info"], state_var=self.tooltip_state)
self.btn_rename = ttk.Button(
rename_frame,
text=Msg.STR["rename"],
state="disable",
command=rename_callback,
width=15,
)
self.btn_rename.grid(column=1, row=0, pady=10, sticky="nsew")
self.update_autoconnect_display()
def update_autoconnect_display(self):
autostart_tunnel = ConfigManager.get("autostart")
if autostart_tunnel and autostart_tunnel != "off":
self.selected_option.set(1)
self.autoconnect_var.set(autostart_tunnel)
else:
self.selected_option.set(0)
self.autoconnect_var.set(Msg.STR["no_autoconnect"])
def get_rename_value(self):
return self.lb_rename.get()
def clear_rename_entry(self):
self.lb_rename.delete(0, tk.END)
def enable_controls(self, list_box_size, is_selection):
if list_box_size > 0:
self.wg_autostart.config(state="normal")
Tooltip(self.wg_autostart,
Msg.TTIP["autostart"], state_var=self.tooltip_state)
if is_selection:
self.lb_rename.config(state="normal")
self.btn_rename.config(state="normal")
self.lb_rename.delete(0, tk.END)
Tooltip(self.lb_rename,
Msg.TTIP["rename_tl"], state_var=self.tooltip_state)
else:
self.wg_autostart.config(state="disabled")
self.lb_rename.config(state="disabled")
self.btn_rename.config(state="disabled")
self.lb_rename.delete(0, tk.END)
self.lb_rename.insert(0, Msg.STR["max_chars"])
Tooltip(self.wg_autostart,
Msg.TTIP["autostart_info"], state_var=self.tooltip_state)
Tooltip(self.lb_rename,
Msg.TTIP["rename_tl_info"], state_var=self.tooltip_state)

73
ui/tunnel_details.py Normal file
View File

@@ -0,0 +1,73 @@
import tkinter as tk
from tkinter import ttk
from shared_libs.common_tools import ConfigManager
from shared_libs.wp_app_config import Msg
class TunnelDetails(ttk.Frame):
def __init__(self, container, **kwargs):
super().__init__(container, **kwargs)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, weight=1)
self.active_frame = ttk.LabelFrame(self, text=Msg.STR["active_tunnel"])
self.active_frame.grid(column=0, row=0, sticky="nsew", padx=10, pady=5)
self.active_frame.columnconfigure(0, weight=1)
self.active_frame.rowconfigure(0, weight=1)
self.interface_frame = ttk.LabelFrame(self, text=Msg.STR["interface"])
self.interface_frame.grid(column=0, row=1, sticky="nsew", padx=10, pady=5)
self.interface_frame.columnconfigure(0, weight=1)
self.interface_frame.rowconfigure(0, weight=1)
self.interface_frame.rowconfigure(1, weight=1)
self.peer_frame = ttk.LabelFrame(self, text=Msg.STR["peer"])
self.peer_frame.grid(column=0, row=2, sticky="nsew", padx=10, pady=5)
self.peer_frame.columnconfigure(0, weight=1)
self.peer_frame.rowconfigure(0, weight=1)
self.str_var = tk.StringVar()
self.add_var = tk.StringVar()
self.dns_var = tk.StringVar()
self.enp_var = tk.StringVar()
self.lb_tunnel = None
self.color_label()
self.address_label = ttk.Label(self.interface_frame, textvariable=self.add_var, foreground="#0071ff")
self.address_label.grid(column=0, row=0, sticky="nsew", padx=10, pady=(0, 20))
self.address_label.config(font=("Ubuntu", 9))
self.dns_label = ttk.Label(self.interface_frame, textvariable=self.dns_var, foreground="#0071ff")
self.dns_label.grid(column=0, row=1, sticky="nsew", padx=10, pady=(0, 20))
self.dns_label.config(font=("Ubuntu", 9))
self.endpoint_label = ttk.Label(self.peer_frame, textvariable=self.enp_var, foreground="#0071ff")
self.endpoint_label.grid(column=0, row=0, sticky="nsew", padx=10, pady=(0, 30))
self.endpoint_label.config(font=("Ubuntu", 9))
def color_label(self):
if self.lb_tunnel:
self.lb_tunnel.destroy()
foreground = "yellow" if ConfigManager.get("theme") != "light" else "green"
self.lb_tunnel = ttk.Label(self.active_frame, textvariable=self.str_var, foreground=foreground)
self.lb_tunnel.config(font=("Ubuntu", 11, "bold"))
self.lb_tunnel.grid(column=0, row=0, padx=10, pady=(0, 10), sticky="nsew")
def update_details(self, active_tunnel_name, tunnel_data):
self.str_var.set(active_tunnel_name)
if active_tunnel_name and tunnel_data and active_tunnel_name in tunnel_data:
values = tunnel_data[active_tunnel_name]
self.add_var.set(f"Address: {values.get('Address', '')}")
self.dns_var.set(f" DNS: {values.get('DNS', '')}")
self.enp_var.set(f"Endpoint: {values.get('Endpoint', '')}")
else:
self.add_var.set("")
self.dns_var.set("")
self.enp_var.set("")
self.color_label()

57
ui/tunnel_list.py Normal file
View File

@@ -0,0 +1,57 @@
import tkinter as tk
from tkinter import ttk
from shared_libs.wp_app_config import Msg
class TunnelList(ttk.Frame):
def __init__(self, container, selection_callback, **kwargs):
super().__init__(container, **kwargs)
self.list_frame = ttk.LabelFrame(self, text=Msg.STR["tunnels"])
self.list_frame.grid(
column=0, row=0, sticky="nsew", padx=(10, 0), pady=(0, 8))
self.list_box = tk.Listbox(self.list_frame, selectmode="single")
self.list_box.config(
relief="flat",
font=("Ubuntu", 12, "bold"),
)
self.list_box.grid(column=0, row=0, sticky="nsew")
self.list_box.event_add("<<ClickEvent>>", "<Button-1>")
self.list_box.bind("<<ClickEvent>>", selection_callback)
self.scrollbar = ttk.Scrollbar(
self.list_frame, orient="vertical", command=self.list_box.yview
)
self.scrollbar.grid(column=1, row=0, sticky="ns")
self.list_box.configure(yscrollcommand=self.scrollbar.set)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.list_frame.columnconfigure(0, weight=1)
self.list_frame.rowconfigure(0, weight=1)
def populate(self, tunnels):
self.list_box.delete(0, tk.END)
for tunnel in tunnels:
self.list_box.insert("end", tunnel)
self.list_box.update()
def get_selected(self):
selection_indices = self.list_box.curselection()
if not selection_indices:
return None
return self.list_box.get(selection_indices[0])
def get_size(self):
return self.list_box.size()
def set_selection(self, index):
self.list_box.selection_clear(0, tk.END)
self.list_box.selection_set(index)
def delete_selected(self):
selection_indices = self.list_box.curselection()
if selection_indices:
self.list_box.delete(selection_indices[0])

1156
wirepy.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""App configuration for Wire-Py""" """App configuration for Wire-Py"""
import logging from logger import app_logger
from pathlib import Path from pathlib import Path
from subprocess import CompletedProcess, run from subprocess import CompletedProcess, run
from typing import Dict, Any from typing import Dict, Any
@@ -27,11 +27,6 @@ class AppConfig:
consistently and perform system-level setup tasks. consistently and perform system-level setup tasks.
""" """
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "wirepy.log"
# Base paths # Base paths
BASE_DIR: Path = Path.home() BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/wire_py" CONFIG_DIR: Path = BASE_DIR / ".config/wire_py"
@@ -47,7 +42,7 @@ class AppConfig:
"# Theme": "dark", "# Theme": "dark",
"# Tooltips": True, "# Tooltips": True,
"# Autostart": "off", "# Autostart": "off",
"# Logfile": LOG_FILE_PATH,
} }
# Updates # Updates
@@ -59,7 +54,7 @@ class AppConfig:
# UI configuration # UI configuration
UI_CONFIG: Dict[str, Any] = { UI_CONFIG: Dict[str, Any] = {
"window_title": "WirePy", "window_title": "WirePy",
"window_title2": "LogViewer",
"window_size": (590, 460), "window_size": (590, 460),
"font_family": "Ubuntu", "font_family": "Ubuntu",
"font_size": 11, "font_size": 11,
@@ -121,18 +116,12 @@ class AppConfig:
check=False, check=False,
) )
if process.returncode == 0: if process.returncode == 0:
logging.info(process.stdout) app_logger.log(process.stdout)
if process.stderr: if process.stderr:
logging.error( app_logger.log(
f"{process.stderr}") f"{process.stderr}")
@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 # here is initializing the class for translation strings
_ = Translate.setup_translations("wirepy") _ = Translate.setup_translations("wirepy")