From 052ed0a828bc4f46442e2964763abdbea13252ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Sun, 10 Aug 2025 20:51:12 +0200 Subject: [PATCH] ui fixes expand widget on x and y works --- logger.py | 12 + lx-icons/128/log.png | Bin 4566 -> 65969 bytes lx-icons/32/log.png | Bin 906 -> 4327 bytes lx-icons/48/log.png | Bin 1655 -> 9475 bytes lx-icons/64/log.png | Bin 2015 -> 16671 bytes ssl_encrypt.py | 18 +- start_wg.py | 8 +- tunnel.py | 21 +- ui/__init__.py | 0 ui/controls.py | 87 ++++ ui/header.py | 58 +++ ui/log_window.py | 47 ++ ui/main_frame.py | 272 ++++++++++ ui/menu_bar.py | 179 +++++++ ui/status_panel.py | 100 ++++ ui/tunnel_details.py | 73 +++ ui/tunnel_list.py | 57 +++ wirepy.py | 1156 +----------------------------------------- wp_app_config.py | 23 +- 19 files changed, 930 insertions(+), 1181 deletions(-) create mode 100644 logger.py create mode 100644 ui/__init__.py create mode 100644 ui/controls.py create mode 100644 ui/header.py create mode 100644 ui/log_window.py create mode 100644 ui/main_frame.py create mode 100644 ui/menu_bar.py create mode 100644 ui/status_panel.py create mode 100644 ui/tunnel_details.py create mode 100644 ui/tunnel_list.py diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..9ace1fc --- /dev/null +++ b/logger.py @@ -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() diff --git a/lx-icons/128/log.png b/lx-icons/128/log.png index 06de63c271cbc80db70f077159acb32ade5a07fd..726607332b287ba834e6f25e31b7ce36655ab999 100644 GIT binary patch literal 65969 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 Uxr=t6red|@?x|TacIu!158+ULk^lez delta 4453 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 Q7X*HqYWG!(l^+NHFY{>F*8l(j diff --git a/lx-icons/32/log.png b/lx-icons/32/log.png index ebc7be1ffe7c349336c654d68dddb68052f85400..02be837d5804bc7eea678054ca3234494ec765d5 100644 GIT binary patch literal 4327 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 delta 1518 zcmV^q)ZE<69VYVXO-^QA>=FC`QAh#-bH@NGp(P zRAMVFO-sw%Z$qnm3be&(_)apJ_TF>voZp?9d*_^c;HTsOEq{OuPynIE@%Zs$5eNjt zz`%ge>-DL$Rc38#nY9%FY4%(Kv;&o`t*w~NW>&3Q#ov~qq9QhI*nrJu!)mn>2m}}( zA7^@cdeOF_Pza~fNpo{Ezz|RmjK@cQ&}y{`uh%Qo7DZ9S{{8!f+wD%Pz1Qm%R;yJ2 zgYi4j0%XK^A%BDz8ygdVu-olv_u=(=LZo|5 zy=-TZR-G(&dyGeDLH%gtmy9U76kgwuv`+xTBlKYeava_=@0`T(XOLp(x z4S>;Tq^+&(?*PPNG1ZL^hr?XDbcw2}Dga7LOX=(D0#f#eRIlcYHDygoy#9UQ50_9zRi{`TjrEkR#sxO*-#W^X#iTS zR#kw}(SK3atXad_wQJ{;7Z(?^di83&UhmQXR;*Z|DnMUf9|sN`_+wpTVGyqAGR5N~JVgi@TMR|GoAL}bBD!6dr0+C20eSmN{tQvsJ z<>J_}WAiSSd3kvpJa~|gA3vrK5DW%Y19{w7|@7}$*T&|S*&C1GBe;S>d zn&QfpD;SN&1@%lO6YcHoDfJzHZ&iJXhlYkYb?Ov3IXMgJm6erYx7(SVoJ{p=R9%3s zt}g28>K4`0Xf&KWd6MDb;glbtY#>1%3&Iuw_>r5Ni%zFY%v#z1UGlwNFN&g|C<-$( zGl{d`@5k+SbM@-gqzv+SJeW)-3_{1OsNw0$2w{Pe5>(`6wefso?iHQly%F0v&$j#0D z2{1J^l~g06(a8P#_Yp!Y>iZr(d>C0)|7$%ZmvY^@bws04K7anapx%=wPpGM>$$yBW zl+qAMl7!W2UC<4joSfv=ty^r`v}ySP0GQ2YdV724t?%#e=iIq-=ybZ}2~b&C$+c_O zh(@FHuD-RkwHb>wr3{dtpU<8>dl(%Z{jDw%iE#1aMNB5s@&y2BXlUU5`}e<C>mbmG}1c(tps%jqaN_Z;FnNj*O1J-|tUsbi)!;4V+G=`10jT z#*Fv*e4@FzIgx6xXtGC6PG=56h=dy{%Q6~`21$}AEG#5DI~%Q5%a0#F@OV7T%*>$E z>E^hT8jS|6R!bldNUWggti1Cjdn6ovQcSne>-D0ir$@M4F0py@W>wO2SR~!%U*Tiu UwfzuqX8-^I07*qoM6N<$g1L14`~Uy| diff --git a/lx-icons/64/log.png b/lx-icons/64/log.png index e1eb8dc09fc8a77a04ee7c1ddde6287d223dd816..f1f00e3f737e06d76587a60a5f684ecb60417f98 100644 GIT binary patch literal 16671 zcmeHPT}%{L6h4bA3sejek+cFK7SJFdDpkZv7hGi*khG*WK@%hJ(DX?gt1-NkG>u80 z`cR{Ns0lAk6Jwe*#;Pa?0a1)-m4M<8(bONni2p`bV0ylZV*qD%?+!D&lXQ={%+8&A z@0|19zjMw#<4>Pgmd3??6iY;L9=EHCuTG@sDE@osn{NmB68UxU#$uwse*R#r#?JS) zomHhJw*Ic{@A)RJ&Rw;Us6LaZ;YT7bXEh8G?aCt>-bPgTD^b#p-urwokY1GBiZ)+{@HHMbbRO8xZW`o9UK2iaC;>yOko7V2!s&`BM?SF5CI#P!{%jLDlad$d%a%!jT<-Y(b3U*-UA*DCv4<3oZ2}( z=Y$>F@Dvdd;XQERfcNFgm)=K@9(k*)s{?%pcmt2%6+HVT5wsGxl|!><&n~H}tFx7q zl+fI{bNx1~r-0pVr{&9+Q*UoCB_<}4!{MO*{(h1qNvOHUj~`P@OA8%5cu;!r;zbKr z`ij$tP-TrmK=BkaC3SUmNn>MU%!~Q7fB$~z=Kz(@e^kb#rtRFCihO(Z|d!T%e%u@1%Ckes%!YO;o)Jr zfB!zErKM3$P7a+pb4GXv(94Xa0O;xIA&iB#7QZmH_}gjUk_znJLR+v)O3Pnl*$`VJ2Ay0BC$`*RG{G zbLPC0ck$xI#0n8%aWso81Hid+=j0)eo>wS2Ihh_ld`Lq>LrRw6S4#lE+8-S|c8s#J zvZe@##gMf^x_R^Flzby^ECIm4zyPJBq|p5N^WT$g#flYl;lhRY@{P){1OU+f(9>!f zXGTT_9Y21YFiOmzkOROvyDZ?+rAt-iOG-+TN5;K-_f((HG&AG?K=XU@DF7oQBXsucS%QGR2+9#G#F?1KTJ47v0L<&MvNB@NjTV1$K08aPu&|J> zT)E=2oSugv1pvJdeSfm{rzJ#ocDC&B3`8v!X;;%zGM$k(Z{ElZVBVtP=?Sa#%9SfA zHWr8D{)t`RbmYhp+PZbC|H51%H8quP-@Z-I8RFyP-@PBBL*YgFJ#XGTc~J$g{zw=A zH1zD*Ga4Nol~-dl&Q44DH?BvI9;Jqc2EvLhLNIgl^707GiV)P|&6_tc`BezAfya?T06bxCpFe+I)r^H@2TKrB#VTYt z0GQhdSm0CM_hjS{7`gYMG+BiV2OuFKffg-Vq-w^(5OO-5K^y*xX6OrS!vTP1h9hc( zyi6v=4IZfWH@OWnej5$|n1mS%cNX@1h2YpwoSjOZ6|SZL05cZG4ji8f;l8xARNf|) zJS$vH0RY|}OatYV7p5!D`w$IgH8KSNsKsz+!8`^H4h|9mA#f68H8KSN2#K8ly^h|8 z8@!^TLJorX-vF>+!2;Qv1hp929}Fccl4&CV5uZ5wgYXD~@aF~(6*&F?7)2V8y}ke#>@3Y!-+i=y*nf(+Gg(-UyoX039*IyyyxAPK&rtb%oBu zJG*4b64~(qCkb={4rkXj3`HxM5jwBD8zZ5HQ$Qb2R8>`ZIoQd2`0!ybA9e=%A%vdt z0e?K92q=2Rh!Oszg*8Guo51!hTeb+-38a0EvLd5eRNip8Falu&!U%*BuqFb#THLkF fSvme+0E0LjiTL-!hNi#q*&TV-SGxWz+4jSK3eg(y delta 1882 zcmV-g2c`I*f&t$Tkeh!69u+SQ^Z9Ch000LgNklSMxr)puQY5s( z=b4$MXeyBeB}>CtrakBnJ>Ej*57H0)3lS7Z24-PDC}l}VmfgmZ1Rp9((&rTkWr}cFwuZxL*4_oL$#0n5}4_1o(dk5G&Dk?ARd&2M2{# zs}+5HeIh6*XsK)d^WCTDpPiM=I9>}B0LM)oN~IDnFE9Q}$z(DvT)2R*uP+r96&yHl zfbQ;Y^78V~>-7s>Z)j+U2M-?1>?#5N0{+KO&esA$p->2wN+rg|#>7f$YHCWv#l?xy z(a|NZH8wUTR4RX!P$(3pGotpp&t??^A}1$j#T_@Pv$InGqOq}Y+14Bz8xuJ>IVK3j zGp~7}P$;a%c_D--EiDy*C@d^o)^*3m#%7)g(WYxcs8lMeao%V&ietx)2|y$#Ckvy| zxUB1|R4NmI5=?h{Yx?&4`}+kT^7HcrAo~0Jm;Ed-Jsp1>fQ?F}5?5DOtVOM@t@!!* zapcGmyu7@4_wL=YuJ7vVic+Zr*ysTK=a0I7)3ayKID7Ui9v&W?Idg_ruU;+tXPA6; zz{)f}KF-yvSBa002OvH^p5o%-MePCRBdY_@-rkO0uV>4aEda>na#~wk`Sj`2p8=qu zp@E#79Nd51-2w3N@gX)g7L7(@{{ZxQJ-2V)CNVM5+$WVvNlQzkrl!XJ0d#kF)7;$5 z)~#EA=#Pqu;@Y)qOiWDJH-M(5CQ?#T@bU5ap+7h{7`ol2{{4ScR#w_C0JU08cz8Je{{9xn!^6WV zEiGkuc-TGxR8>`R;=~DLGMUBkfPesGG8r8m9rg!cbaa%wygXuKW9OXX?CeZ-b~bO` zysjwS551&CSHc#j$^R^X7TmNF)+cQc|d@s)KX_^YAQWFJ*--_3YkoX zlamupPEJUrQuC`+D&_d`<0K^|E$V~Y+S*vZemx-}Aq%#3b8{mrD~qC{A~YHej*fqh z<|dIyaBy(2*mmvOHEXkF5kPi!Hd?I~Ap}AQj7FpR*z~@(ww8*DibVscsj1=IxpS;t zyLM6bhYlU$@4x>>tyW_&7%&(N7>z~@1_M)5Q|6}A>Co%-=yW`Sts)S! zBmiMyVLW>Dh|ixt&)eqX$B)$4*RyTgHrof_=jXTJ>}^w16N!n5`1<=~e9PXJl$6XXdpj^Nz}>rdiHeHCTC`*Ua=9F}T0O7qty--n zFfh>k2(=3UrtIx}!c;1iiZf@g5XXg+7Mx&8?_wJ!oD(4;gwn+f%)~&Ocz3qSP?WMA^azUYQ zn*;!mkdVOr`}Z*z4CcP(=4N7IV(|0xvwr}gp`p~)*8Y&at*ophGc$9+p>LZ6uxZmK z>UnnRjVCT-ASgV#5fJ`PMGc(hiy=`l2!`tWiIg%+#8;tIMT`hzJ0y34L4IntxX<{D!i*k&zJr zh@PGvtNZq~TJ5Z|Irt5QlNT>u6w}kwR^|NUT1f$%2xDlKcC|1nObuBd`e1+7#|-um%my;o3niW|Myj= UFF;{Qp#T5?07*qoM6N<$f?eLNOaK4? diff --git a/ssl_encrypt.py b/ssl_encrypt.py index fd581bf..23393b0 100755 --- a/ssl_encrypt.py +++ b/ssl_encrypt.py @@ -2,27 +2,27 @@ """ This Script encrypt Wireguardfiles for Wirepy users for more Security """ import argparse -import logging + from pathlib import Path import pwd import shutil from subprocess import CompletedProcess, run from shared_libs.wp_app_config import AppConfig -from shared_libs.common_tools import LogConfig + parser = argparse.ArgumentParser() parser.add_argument("--user", required=True, help="Username of the target file system") args = parser.parse_args() -LogConfig.logger(f"/home/{args.user}/.local/share/lxlogs/wirepy.log") + try: # Retrieve UID and GID user_info = pwd.getpwnam(args.user) uid = user_info.pw_uid # User ID (e.g., 1000) gid = user_info.pw_gid # Group ID (e.g., 1000) except KeyError: - logging.error(f"User '{args.user}' not found.") + exit(1) 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 if process.stderr: - if "writing RSA key" in process.stderr: - logging.info(f"{process.stderr}") - else: - logging.error(f"{process.stderr}") - - if process.returncode == 0: - logging.info("Public key generated successfully.") + pass shutil.chown(keyfile, uid, gid) @@ -86,4 +80,4 @@ if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()): # Output from Openssl Error if process.stderr: - logging.error(process.stderr) + pass \ No newline at end of file diff --git a/start_wg.py b/start_wg.py index 6491666..56e73df 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 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) -LogConfig.logger(ConfigManager.get("logfile")) + if ConfigManager.get("autostart") != "off": process: CompletedProcess[str] = run( ["nmcli", "connection", "up", ConfigManager.get("autostart")], @@ -18,7 +18,7 @@ if ConfigManager.get("autostart") != "off": ) # Output from start_wg error if process.stderr: - logging.error(process.stderr) + else: pass diff --git a/tunnel.py b/tunnel.py index 6cd66f2..4bd2fca 100644 --- a/tunnel.py +++ b/tunnel.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -import logging +from logger import app_logger import getpass import zipfile from datetime import datetime @@ -25,6 +25,7 @@ class Tunnel: if filepath is not None: filepath = Path(filepath) + truncated_stem = None try: content = filepath.read_text() @@ -70,17 +71,17 @@ class Tunnel: content = secrets.token_bytes(len(content)) except StopIteration: - pass + return None, None elif directory is not None: if not directory.exists() or not directory.is_dir(): - logging.error( + app_logger.log( "Temp directory does not exist or is not a directory.") return None # 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()] # Search for the string in the files @@ -146,12 +147,12 @@ class Tunnel: ) 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: active = None except Exception as e: - logging.error(f"Error on nmcli: {e}") + app_logger.log(f"Error on nmcli: {e}") active = None return active if active is not None else "" @@ -189,7 +190,7 @@ class Tunnel: ) else: - logging.error( + app_logger.log( "There was a mistake at creating the Zip file. File is empty." ) MessageDialog( @@ -199,18 +200,18 @@ class Tunnel: return False return True except PermissionError: - logging.error( + app_logger.log( f"Permission denied when creating archive in {wg_tar}" ) return False except zipfile.BadZipFile as e: - logging.error(f"Invalid ZIP file: {e}") + app_logger.log(f"Invalid ZIP file: {e}") return False except TypeError: pass except Exception as e: - logging.error(f"Export failed: {str(e)}") + app_logger.log(f"Export failed: {str(e)}") MessageDialog( "error", Msg.STR["exp_try"], title=Msg.STR["exp_err"]) return False diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/controls.py b/ui/controls.py new file mode 100644 index 0000000..44cd69a --- /dev/null +++ b/ui/controls.py @@ -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) diff --git a/ui/header.py b/ui/header.py new file mode 100644 index 0000000..4cf4d3f --- /dev/null +++ b/ui/header.py @@ -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) diff --git a/ui/log_window.py b/ui/log_window.py new file mode 100644 index 0000000..e6533f4 --- /dev/null +++ b/ui/log_window.py @@ -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() diff --git a/ui/main_frame.py b/ui/main_frame.py new file mode 100644 index 0000000..fd23846 --- /dev/null +++ b/ui/main_frame.py @@ -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"]) diff --git a/ui/menu_bar.py b/ui/menu_bar.py new file mode 100644 index 0000000..94bfd48 --- /dev/null +++ b/ui/menu_bar.py @@ -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"]) + diff --git a/ui/status_panel.py b/ui/status_panel.py new file mode 100644 index 0000000..d8692a8 --- /dev/null +++ b/ui/status_panel.py @@ -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) diff --git a/ui/tunnel_details.py b/ui/tunnel_details.py new file mode 100644 index 0000000..e4380dc --- /dev/null +++ b/ui/tunnel_details.py @@ -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() diff --git a/ui/tunnel_list.py b/ui/tunnel_list.py new file mode 100644 index 0000000..94838e1 --- /dev/null +++ b/ui/tunnel_list.py @@ -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("<>", "") + self.list_box.bind("<>", 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]) diff --git a/wirepy.py b/wirepy.py index 49a1f25..588bfb0 100755 --- a/wirepy.py +++ b/wirepy.py @@ -2,32 +2,22 @@ """ this script is a simple GUI for managing Wireguard Tunnels """ -import logging import getpass -import shutil import sys -import os -import subprocess import tkinter as tk -import webbrowser -from functools import partial -from pathlib import Path -from subprocess import CompletedProcess, run -from tkinter import TclError, filedialog, ttk -from tunnel import Tunnel -from shared_libs.message import MessageDialog -from shared_libs.gitea import GiteaUpdate +from tkinter import TclError + from shared_libs.common_tools import ( LxTools, CryptoUtil, - LogConfig, ConfigManager, ThemeManager, - Tooltip, IconManager, ) - -from shared_libs.wp_app_config import AppConfig, Msg +from shared_libs.wp_app_config import AppConfig +from ui.main_frame import MainFrame +from ui.log_window import LogWindow +from logger import app_logger class Wirepy(tk.Tk): @@ -70,11 +60,16 @@ class Wirepy(tk.Tk): icon = self.image_manager.get_icon("vpn_small") if icon: self.iconphoto(True, icon) - except: + except Exception: pass + self.log_window = LogWindow(self) + app_logger.init_logger(self.log_window.log_message) + # Add the widgets - FrameWidgets(self).grid() + main_frame = MainFrame(self, image_manager=self.image_manager, + toggle_log_window=self.toggle_log_window) + main_frame.grid(sticky="nsew") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) @@ -84,1123 +79,8 @@ class Wirepy(tk.Tk): # 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.image_manager = IconManager() - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) - self.columnconfigure(1, weight=1) - self.rowconfigure(1, weight=1) - self.columnconfigure(2, weight=18) - self.rowconfigure(2, weight=1) - self.rowconfigure(3, weight=1) - - # 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.grid(column=0, columnspan=3, row=1, sticky="we") - - self.options_btn = ttk.Menubutton( - self.menu_frame, 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.updates_lb = ttk.Label( - self.menu_frame, textvariable=self.update_label) - self.updates_lb.grid(column=2, row=0) - 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 - ) - # 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=Msg.STR["about"], style="Toolbutton", command=self.about - ) - self.about_btn.grid(column=1, row=0) - - self.a = Tunnel.get_active() - - # Header Frame - # Festlegen der Farbe - self.header_frame = tk.Frame(self, bg="#2c3e50") - self.wg_icon_header_frame = tk.Frame(self.header_frame, bg="#2c3e50") - self.header_label = tk.Label( - self.header_frame, - text=Msg.STR["lx_tools"], - font=("Helvetica", 12, "bold"), - fg="#ffffff", - bg="#2c3e50", - ) - - self.version_label = tk.Label( - self.header_frame, - text=f"{AppConfig.VERSION} • {Msg.STR['header_left_bottom']}", - font=("Helvetica", 9), - fg="#bdc3c7", - bg="#2c3e50", - ) - self.info_label = tk.Label( - self.header_frame, - text=Msg.STR["header_right_top"], - font=("Helvetica", 10), - fg="#ecf0f1", - bg="#2c3e50", - ) - self.header_frame.grid(column=0, columnspan=3, row=0, sticky="nsew") - self.wg_icon_header_frame.grid(column=0, row=0, rowspan=2, sticky="w") - - self.wg_icon_header_label = tk.Label( - self.wg_icon_header_frame, - image=self.image_manager.get_icon("vpn_small"), - bg="#2c3e50", - ) - self.wg_icon_header_label.grid(column=0, row=0, sticky="e", ipadx=10) - - self.header_label.grid( - column=1, - row=0, - sticky="w", - padx=(5, 20), - pady=(15, 5), - ipady=4, - ) - self.version_label.grid( - column=1, row=1, sticky="w", padx=(5, 20), pady=(0, 10)) - self.info_label.grid(column=2, row=0, sticky="ne", - padx=(10, 10), pady=(10, 0)) - self.header_frame.columnconfigure(1, weight=1, pad=2) - self.header_frame.rowconfigure(0, weight=1) - - # Frame for Control Buttons (Start, Stop, Import, Trash, Export) - self.control_buttons_frame = ttk.Frame(self) - self.control_buttons_frame.grid( - column=0, row=2, sticky="w", padx=(15, 0)) - self.control_buttons_frame.columnconfigure(0, weight=1) - self.control_buttons_frame.rowconfigure(2, weight=1) - - # Frame for Listbox and Scrollbar - self.list_container_frame = ttk.Frame(self) - self.list_container_frame.grid(column=1, row=2, sticky="nsew") - self.list_container_frame.columnconfigure(1, weight=1) - self.list_container_frame.rowconfigure(2, weight=1) - self.list_frame = ttk.LabelFrame( - self.list_container_frame, text=Msg.STR["tunnels"]) - self.list_frame.grid(column=0, row=0, sticky="nsew", padx=10, ipady=20) - # Listbox with Scrollbar - 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("<>", "") - self.list_box.bind("<>", self.enable_check_box) - 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.scrollbar.columnconfigure(1, weight=1) - self.scrollbar.rowconfigure(0, weight=1) - - # Frame for Active Tunnel, Interface and Peer - # Right Side Frame - self.right_side_frame = ttk.Frame(self) - self.right_side_frame.grid(column=2, row=2, sticky="nsew") - self.right_side_frame.columnconfigure(2, weight=1) - self.right_side_frame.rowconfigure(2, weight=1) - - # Show active Label - self.select_tunnel = None - self.active_frame = ttk.LabelFrame( - self.right_side_frame, text=Msg.STR["active_tunnel"] - ) - - self.active_frame.grid( - column=0, row=0, sticky="nsew", padx=10, pady=5, columnspan=3 - ) - self.active_frame.columnconfigure(0, weight=1) - self.active_frame.rowconfigure(0, weight=1) - # Interface Label Frame - self.interface_frame = ttk.LabelFrame( - self.right_side_frame, text=Msg.STR["interface"] - ) - self.interface_frame.grid( - column=0, row=1, sticky="nsew", padx=10, pady=5, columnspan=3 - ) - self.interface_frame.columnconfigure(0, weight=1) - self.interface_frame.rowconfigure(1, weight=1) - - # Peer Label Frame - self.peer_frame = ttk.LabelFrame( - self.right_side_frame, text=Msg.STR["peer"]) - self.peer_frame.grid( - column=0, row=2, sticky="nsew", padx=10, pady=5, columnspan=3 - ) - self.peer_frame.columnconfigure(0, weight=1) - self.peer_frame.rowconfigure(2, weight=1) - - # Auto Start Label Frame - self.autoconnect_frame = ttk.Frame(self) - self.autoconnect_frame.grid(column=0, row=3, columnspan=2, sticky="w") - - # Rename Frame - self.rename_frame = ttk.Frame(self) - self.rename_frame.grid(column=2, padx=10, row=3, sticky="nsew") - self.rename_frame.columnconfigure(0, weight=2) - self.rename_frame.columnconfigure(1, weight=1) - - # Label to Show active Tunnel - self.str_var = tk.StringVar(value=self.a) - self.color_label() - - # Interface Label - self.interface = ttk.Label(self.interface_frame) - self.interface.grid(column=0, row=4, sticky="we") - self.interface.config(font=("Ubuntu", 9)) - - # Peer Label - self.peer = ttk.Label(self.peer_frame) - self.peer.config(font=("Ubuntu", 9)) - self.peer.grid(column=0, row=5, sticky="we") - - # 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.list_box.insert("end", tunnels) - self.list_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.control_buttons_frame, - image=self.image_manager.get_icon("import_large"), - command=self.import_sl, - padding=0, - ) - self.btn_i.grid(column=0, row=1, pady=8) - - Tooltip(self.btn_i, Msg.TTIP["import_tl"], - state_var=self.tooltip_state) - - # Button Trash - self.btn_tr = ttk.Button( - self.control_buttons_frame, - image=self.image_manager.get_icon("trash_large"), - command=self.delete, - padding=0, - ) - self.btn_tr.grid(column=0, row=2, pady=8) - - if self.list_box.size() == 0: - Tooltip(self.btn_tr, - Msg.TTIP["trash_tl_info"], state_var=self.tooltip_state) - else: - Tooltip(self.btn_tr, Msg.TTIP["trash_tl"], - state_var=self.tooltip_state) - - # Button Export - self.btn_exp = ttk.Button( - self.control_buttons_frame, - image=self.image_manager.get_icon("export_large"), - command=lambda: Tunnel.export(), - padding=0, - ) - - self.btn_exp.grid(column=0, row=3, pady=8) - self.btn_exp.columnconfigure(0, weight=1) - self.btn_exp.rowconfigure(3, weight=1) - - if self.list_box.size() == 0: - Tooltip(self.btn_exp, - Msg.TTIP["export_tl_info"], state_var=self.tooltip_state) - else: - Tooltip(self.btn_exp, - Msg.TTIP["export_tl"], state_var=self.tooltip_state) - - # Label Entry - self.lb_rename = ttk.Entry(self.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") - - if self.list_box.size() != 0: - Tooltip( - self.lb_rename, - Msg.TTIP["rename_tl"], - state_var=self.tooltip_state) - else: - Tooltip( - self.lb_rename, - Msg.TTIP["rename_tl_info"], - state_var=self.tooltip_state) - - # Button Rename - self.btn_rename = ttk.Button( - self.rename_frame, - text=Msg.STR["rename"], - state="disable", - command=self.tl_rename, - width=15, - ) - self.btn_rename.grid(column=1, row=0, pady=10, sticky="nsew") - - # 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.autoconnect_frame, - textvariable=self.autoconnect_var - ) - self.autoconnect.config(font=("Ubuntu", 11)) - self.autoconnect.grid(column=1, row=0, pady=10, sticky="nsew") - self.autoconnect.columnconfigure(1, weight=1) - self.autoconnect.rowconfigure(0, weight=1) - self.wg_autostart = ttk.Checkbutton( - self.autoconnect_frame, - text=Msg.STR["autoconnect_on"], - variable=self.selected_option, - command=self.box_set, - ) - self.wg_autostart.grid(column=0, row=0, pady=10, - padx=(10, 0), sticky="ew") - - if self.list_box.size() >= 1 and len(self.list_box.curselection()) >= 1: - Tooltip( - self.wg_autostart, - Msg.TTIP["autostart"], - state_var=self.tooltip_state) - - if self.list_box.size() == 0: - Tooltip( - self.wg_autostart, - Msg.TTIP["autostart_info"], - state_var=self.tooltip_state) - - else: - - Tooltip( - self.wg_autostart, - Msg.TTIP["autostart"], - state_var=self.tooltip_state) - - 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() - - def updater(self): - """Start the lxtools_installer""" - 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) - - # 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(Msg.STR["update_search_off"]) - self.update_tooltip.set(Msg.TTIP["updates_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(), - state_var=self.tooltip_state) - - elif res == "No Internet Connection!": - self.update_label.set(Msg.STR["no_server_connection"]) - self.update_foreground.set("red") - # Set the tooltip for "No Server Connection" - 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("") - # Set the tooltip for the label - Tooltip(self.updates_lb, self.update_tooltip.get(), - state_var=self.tooltip_state) - - else: - self.set_update.set(value=0) - - # Clear the label text since we'll show the button instead - self.update_label.set("") - - # Create the update button - self.update_btn = ttk.Button( - self.menu_frame, - 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: - """ - a tk.Toplevel window - """ - 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: - """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}") - # 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(Msg.STR["disable_tooltips"]) - else: - # If tooltips are disabled, the menu option should be to enable them - self.tooltip_label.set(Msg.STR["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(Msg.STR["dark"]) - else: - self.theme_label.set(Msg.STR["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.header_label.config(fg="#ffffff") - 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.control_buttons_frame, - image=self.image_manager.get_icon("vpn_start_large"), - command=lambda: self.wg_switch("start"), - padding=0, - ) - self.btn_stst.grid(column=0, row=0, pady=8) - self.btn_stst.columnconfigure(0, weight=1) - self.btn_stst.rowconfigure(0, weight=1) - - if self.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 color_label(self) -> None: - """ - View activ Tunnel in the color green or yellow - """ - if ConfigManager.get("theme") == "light": - - self.lb_tunnel = ttk.Label( - self.active_frame, textvariable=self.str_var, foreground="green" - ) - - else: - self.lb_tunnel = ttk.Label( - self.active_frame, textvariable=self.str_var, foreground="yellow" - ) - - self.lb_tunnel.config(font=("Ubuntu", 11, "bold")) - self.lb_tunnel.grid(column=0, row=0, padx=10, pady=(0, 10), sticky="n") - self.lb_tunnel.columnconfigure(0, weight=1) - self.lb_tunnel.rowconfigure(0, weight=1) - - def stop(self) -> None: - """ - Stop Button - """ - self.btn_stst = ttk.Button( - self.control_buttons_frame, - image=self.image_manager.get_icon("vpn_stop_large"), - command=lambda: self.wg_switch("stop"), - padding=0, - ) - self.btn_stst.grid(column=0, row=0, pady=8) - self.btn_stst.columnconfigure(0, weight=1) - self.btn_stst.rowconfigure(0, weight=1) - - Tooltip(self.btn_stst, Msg.TTIP["stop_tl"], - state_var=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']}="): - MessageDialog( - "error", Msg.STR["tl_exist"], title=Msg.STR["imp_err"]) - - elif not CryptoUtil.is_valid_base64( - f"{data_import[key_name]['PrivateKey']}=" - ): # 2. Second check: Is it valid Base64? - MessageDialog( - "error", - Msg.STR["invalid_base64"], - title=Msg.STR["imp_err"], - ) - 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}") - - 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.list_box.insert(0, self.a) - self.wg_autostart.configure(state="normal") - self.list_box.selection_clear(0, tk.END) - self.list_box.update() - self.list_box.selection_set(0) - - Tooltip( - self.wg_autostart, - Msg.TTIP["autostart"], - state_var=self.tooltip_state) - 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) - Tooltip(self.btn_rename, - Msg.TTIP["rename_tl"], state_var=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) - - if process.returncode == 0: - print( - f">> {import_file.stem} << autostart is disabled by default") - - except UnboundLocalError: - MessageDialog( - "error", Msg.STR["no_valid_file"], title=Msg.STR["imp_err"]) - 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.list_box.curselection() - select_tl = self.list_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}") - - self.list_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(Msg.STR["no_autoconnect"]) - - self.wg_autostart.configure(state="disabled") - - # for disabling checkbox when Listbox empty - if self.list_box.size() == 0: - self.wg_autostart.configure(state="disabled") - self.lb_rename.configure(state="disabled") - Tooltip( - self.wg_autostart, - Msg.TTIP["autostart_info"], - state_var=self.tooltip_state) - - Tooltip(self.btn_exp, - Msg.TTIP["export_tl_info"], state_var=self.tooltip_state) - Tooltip(self.btn_stst, - Msg.TTIP["empty_list"], state_var=self.tooltip_state) - Tooltip(self.lb_rename, - Msg.TTIP["rename_tl_info"], state_var=self.tooltip_state) - self.lb_rename.insert(0, Msg.STR["max_chars"]) - - if self.a != "" and self.a == select_tl: - self.str_var.set(value="") - self.start() - self.list_box.update() - self.reset_fields() - - except IndexError: - - if self.list_box.size() != 0: - - MessageDialog( - "info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) - - else: - - MessageDialog( - "info", Msg.STR["tl_first"], title=Msg.STR["sel_tl"]) - - def enable_check_box(self, _) -> None: - """ - checkbox for enable autostart Tunnel - """ - AppConfig.get_autostart_content() - if self.list_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") - AppConfig.get_autostart_content() - - else: - self.selected_option.set(0) - self.auto_con = Msg.STR["no_autoconnect"] - self.autoconnect_var.set("") - self.autoconnect_var = tk.StringVar() - self.autoconnect_var.set(self.auto_con) - - self.autoconnect = ttk.Label( - self.autoconnect_frame, - textvariable=self.autoconnect_var, - foreground="#0071ff", - width=18, - ) - self.autoconnect.config(font=("Ubuntu", 11)) - self.autoconnect.grid(column=1, row=0, sticky="ew", pady=19) - self.autoconnect.rowconfigure(0, weight=1) - - 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.list_box.curselection() - select_tl = self.list_box.get(select_tunnel[0]) - - if self.selected_option.get() == 0: - ConfigManager.set("autostart", "off") - - if self.list_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: - - MessageDialog("info", Msg.STR["sign_len"], - title=Msg.STR["ren_err"]) - - elif len(self.lb_rename.get()) == 0: - - MessageDialog( - "info", Msg.STR["zero_signs"], title=Msg.STR["ren_err"]) - - elif any(ch in special_characters for ch in self.lb_rename.get()): - - MessageDialog( - "info", Msg.STR["false_signs"], title=Msg.STR["ren_err"]) - - elif self.lb_rename.get() in [ - file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat") - ]: - - MessageDialog( - "info", Msg.STR["is_in_use"], title=Msg.STR["ren_err"]) - - else: - - try: - self.select_tunnel = self.list_box.curselection() - select_tl = self.list_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}") - - 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.list_box.delete(self.select_tunnel[0]) - self.list_box.insert("end", self.lb_rename.get()) - self.list_box.update() - self.lb_rename.delete(0, tk.END) - self.update_connection_display() - - except IndexError: - - MessageDialog( - "info", Msg.STR["sel_list"], title=Msg.STR["ren_err"]) - - except EOFError as e: - logging.error(e) - - 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 `peer_frame`). - 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.interface_frame, textvariable=self.add, foreground="#0071ff" - ) - self.address.grid(column=0, row=5, sticky="w", padx=10, pady=(0, 20)) - self.address.config(font=("Ubuntu", 9)) - - # DNS Label - self.dns = ttk.Label( - self.interface_frame, textvariable=self.DNS, foreground="#0071ff" - ) - self.dns.grid(column=0, row=7, sticky="w", padx=10, pady=(0, 20)) - self.dns.config(font=("Ubuntu", 9)) - - # Endpoint Label - self.endpoint = ttk.Label( - self.peer_frame, textvariable=self.enp, foreground="#0071ff" - ) - self.endpoint.grid(column=0, row=8, sticky="w", padx=10, pady=(0, 30)) - 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 (`list_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.list_box.curselection() - select_tl = self.list_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.list_box.size() != 0: - - MessageDialog( - "info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) - - else: - - MessageDialog( - "info", Msg.STR["tl_first"], title=Msg.STR["sel_tl"]) - - 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}") - - 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}") - - 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() + def toggle_log_window(self): + self.log_window.toggle() if __name__ == "__main__": @@ -1209,7 +89,7 @@ if __name__ == "__main__": LxTools.sigi(AppConfig.TEMP_DIR) CryptoUtil.decrypt(getpass.getuser()) window = Wirepy() - LogConfig.logger(ConfigManager.get("logfile")) + """ the hidden files are hidden in Filedialog """ @@ -1221,5 +101,5 @@ if __name__ == "__main__": window.tk.call("set", "::tk::dialog::file::showHiddenVar", "0") window.mainloop() -LxTools.clean_files(AppConfig.TEMP_DIR) -sys.exit(0) + LxTools.clean_files(AppConfig.TEMP_DIR) + sys.exit(0) \ No newline at end of file diff --git a/wp_app_config.py b/wp_app_config.py index 2dc4a48..6f0c4c6 100755 --- a/wp_app_config.py +++ b/wp_app_config.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 """App configuration for Wire-Py""" -import logging +from logger import app_logger from pathlib import Path from subprocess import CompletedProcess, run from typing import Dict, Any @@ -27,11 +27,6 @@ class AppConfig: 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_DIR: Path = Path.home() CONFIG_DIR: Path = BASE_DIR / ".config/wire_py" @@ -47,7 +42,7 @@ class AppConfig: "# Theme": "dark", "# Tooltips": True, "# Autostart": "off", - "# Logfile": LOG_FILE_PATH, + } # Updates @@ -59,7 +54,7 @@ class AppConfig: # UI configuration UI_CONFIG: Dict[str, Any] = { "window_title": "WirePy", - "window_title2": "LogViewer", + "window_size": (590, 460), "font_family": "Ubuntu", "font_size": 11, @@ -121,18 +116,12 @@ class AppConfig: check=False, ) if process.returncode == 0: - logging.info(process.stdout) + app_logger.log(process.stdout) if process.stderr: - logging.error( + app_logger.log( 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 _ = Translate.setup_translations("wirepy") @@ -247,4 +236,4 @@ class Msg: "no_server_conn_tt": _("Could not connect to update server"), "up_to_date": _("Congratulations! Wire-Py is up to date"), "install_new_version": _("Click to install new version"), - } \ No newline at end of file + }