From 2463d63c124e2d6b8e621cef54f3e7753e03723d Mon Sep 17 00:00:00 2001 From: punix Date: Fri, 27 Jun 2025 19:28:08 +0200 Subject: [PATCH 1/6] part one moden UI and reorganized Frames and Labels --- lx-icons/16/wg_vpn.png | Bin 0 -> 846 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lx-icons/16/wg_vpn.png diff --git a/lx-icons/16/wg_vpn.png b/lx-icons/16/wg_vpn.png new file mode 100644 index 0000000000000000000000000000000000000000..0bef818b66903c8e56e9f71c0fb8a0b0ac486a6f GIT binary patch literal 846 zcmV-U1F`&xP)y{D4^000SaNLh0L01FcU01FcV0GgZ_00007 zbV*G`2k8bH3J5r$Hx=;!00NRpL_t(I%Z<}tNK;`L$MNqOMp_q%Nr#h_MHy};DEJ2z zgs`jhCJLf1t+X5KDqL6H2%V6M=q7^7$bo@{_$Q5+W*KU!nK2TZ|DD@(xYU|wbMEOP z7!phRT|e*VeP8%J3jc_%Y8lC;C#zAEbzf_nZuPpq$C^_%9f$|AfTFakRi`W^CyLUt zvk<^JJj}Pw&d#yB_u7CiVA$$)OB8rme&J@$`E%ApRYg5;02BpBPY*MLgT!r*=TKfQ z=7Iv&4THtGIhNdR-dwEEtEVQe#{r09O3E%N0bpTvmWJ|jWMhM3Yc+|QM(?wF25dIY z*4C10v5;!9U{8oo05Ab8zw1^2LWV(O)fLvhI@q==5pzKy5)s17%Pcmx(sJuo^zV_t znm-CeR#q1PM*I3$?du2N*qPI~Cnr%;Q`uKo2*C8?I$TaC0Ad*PQ2>|;8U{08MgYjL zUc+2mjCXt-lgUI{US9O(`LQv6t*ij7ZU#pE9uI4NKLFd}6Ic%hagB_iC<+swhoV3z z5MX(65ny;T@P1)xYE3n#1K=7O0!86?WhK7(c_JH;D3Fwrf^TllWA(a6qkz@x7H4m7 z^U?f#0QQ*8B<$S5>)UtuP;cW%ZtkC#?cc}2%uGh@_Ls4mSKx9w#s2iE=&B}fZ8ll* zc*O7ZioLE*bXAj)?rxcKI7C;qw3zj6tf_esG7S0H+9tPia^=b8%kt<_h1@SKlW+a~ z5)Or=v8Lt$Tl9Z#UrVb?L?mb!vM@U`b!KVr@O Y25ZtvcJhLMJOBUy07*qoM6N<$f|Z Date: Fri, 27 Jun 2025 19:28:18 +0200 Subject: [PATCH 2/6] part one of new modern design --- wirepy.py | 347 ++++++++++++++++++++++++++++++----------------- wp_app_config.py | 11 +- 2 files changed, 226 insertions(+), 132 deletions(-) diff --git a/wirepy.py b/wirepy.py index 48868ca..732df7b 100755 --- a/wirepy.py +++ b/wirepy.py @@ -24,7 +24,7 @@ from shared_libs.common_tools import ( Tooltip, ) -from shared_libs.wp_app_config import AppConfig, Msg +from wp_app_config import AppConfig, Msg class Wirepy(tk.Tk): @@ -48,10 +48,12 @@ class Wirepy(tk.Tk): AppConfig.UI_CONFIG["resizable_window"][0], AppConfig.UI_CONFIG["resizable_window"][1], ) + """self.minsize( + AppConfig.UI_CONFIG["window_size"][0], + AppConfig.UI_CONFIG["window_size"][1], + )""" self.title(AppConfig.UI_CONFIG["window_title"]) - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) self.tk.call("source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") ConfigManager.init(AppConfig.SETTINGS_FILE) theme = ConfigManager.get("theme") @@ -65,6 +67,8 @@ class Wirepy(tk.Tk): # Add the widgets FrameWidgets(self).grid() + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) # Center the window on the primary monitor LxTools.center_window_cross_platform(self, self.x_width, self.y_height) @@ -87,13 +91,20 @@ class FrameWidgets(ttk.Frame): self.dns = None self.address = None self.auto_con = None - self.style = ttk.Style() self.wg_vpn_start = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_start"]) self.wg_vpn_stop = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_stop"]) self.imp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_import"]) self.tr_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_trash"]) self.exp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_export"]) self.warning_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_error"]) + self.wg_icon_header = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_header"]) + 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() @@ -122,20 +133,10 @@ class FrameWidgets(ttk.Frame): # Frame for Menu self.menu_frame = ttk.Frame(self) - self.menu_frame.configure(relief="flat") - self.menu_frame.grid(column=0, row=0, columnspan=4, sticky="w") - - # App Menu - self.version_lb = ttk.Label(self.menu_frame, text=AppConfig.VERSION) - self.version_lb.config(font=("Ubuntu", 11), foreground="#00c4ff") - self.version_lb.grid(column=0, row=0, rowspan=4, padx=10) - - Tooltip( - self.version_lb, f"Version: {AppConfig.VERSION[2:]}", self.tooltip_state - ) + self.menu_frame.grid(column=0, columnspan=3, row=1, sticky="we") self.options_btn = ttk.Menubutton(self.menu_frame, text=_("Options")) - self.options_btn.grid(column=1, columnspan=1, row=0) + self.options_btn.grid(column=0, row=0) Tooltip(self.options_btn, Msg.TTIP["settings"], self.tooltip_state) @@ -147,9 +148,8 @@ class FrameWidgets(ttk.Frame): command=lambda: self.update_setting(self.set_update.get()), variable=self.set_update, ) - self.updates_lb = ttk.Label(self.menu_frame, textvariable=self.update_label) - self.updates_lb.grid(column=4, columnspan=3, row=0, padx=10) + self.updates_lb.grid(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) @@ -177,69 +177,146 @@ class FrameWidgets(ttk.Frame): self.about_btn = ttk.Button( self.menu_frame, text=_("About"), style="Toolbutton", command=self.about ) - self.about_btn.grid(column=2, columnspan=2, row=0) - self.readme = tk.Menu(self) + self.about_btn.grid(column=1, row=0) self.a = Tunnel.get_active() - # Label Frame 1 - self.lb_frame_btn_lbox = ttk.Frame(self) - self.lb_frame_btn_lbox.configure(relief="flat") - self.lb_frame_btn_lbox.grid(column=0, rowspan=3, row=1) - - # Label Frame 2 - self.lb_frame = ttk.Frame(self) - self.lb_frame.configure(relief="solid") - self.lb_frame.grid(column=2, row=2, sticky="snew", padx=20, pady=5) - - # Label Frame 3 - self.lb_frame2 = ttk.Frame(self) - self.lb_frame2.configure(relief="solid") - self.lb_frame2.grid(column=2, row=3, sticky="snew", padx=20, pady=5) - - # Bottom Frame 4 - self.lb_frame3 = ttk.Frame(self) - self.lb_frame3.configure(relief="flat") - self.lb_frame3.grid( - column=0, row=5, columnspan=4, sticky="snew", padx=2, pady=2 + # 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=_("Lx Tools Wire-Py"), + font=("Helvetica", 12, "bold"), + fg="#ffffff", + bg="#2c3e50", ) - # Bottom Frame 5 - self.lb_frame4 = ttk.Frame(self) - self.lb_frame4.configure(relief="flat") - self.lb_frame4.grid(column=2, row=5, columnspan=3, sticky="e", padx=15) + 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.wg_icon_header, 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=(10, 0), + 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", pady=3) + 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=_("Tunnels")) + self.list_frame.grid(column=0, row=0, sticky="nsew", padx=10, ipady=25) + # 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.lb = ttk.Label(self, text=_("Active: ")) - self.lb.config(font=("Ubuntu", 11, "bold")) - self.lb.grid(column=2, row=1, padx=15, pady=4, sticky="w") + self.active_frame = ttk.LabelFrame( + self.right_side_frame, text=_("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=_("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=_("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=1) + 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.lb_frame, text=_("Interface")) - self.interface.grid(column=0, row=3, sticky="we", padx=120) + 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.lb_frame2, text=_("Peer")) + self.peer = ttk.Label(self.peer_frame) self.peer.config(font=("Ubuntu", 9)) - self.peer.grid(column=0, row=4, sticky="we", padx=130) - - # Listbox with Scrollbar - self.l_box = tk.Listbox(self.lb_frame_btn_lbox, selectmode="single") - self.l_box.config(relief="ridge", font=("Ubuntu", 12, "bold")) - self.l_box.grid(column=1, rowspan=4, row=0, sticky="ns") - self.l_box.event_add("<>", "") - self.l_box.bind("<>", self.enable_check_box) - self.scrollbar = ttk.Scrollbar( - self.lb_frame_btn_lbox, orient="vertical", command=self.l_box.yview - ) - self.scrollbar.grid(column=1, rowspan=4, row=0, sticky="nse") - self.l_box.configure(yscrollcommand=self.scrollbar.set) + self.peer.grid(column=0, row=5, sticky="we") # Tunnel List self.tl = Tunnel.parse_files_to_dictionary(directory=AppConfig.TEMP_DIR) @@ -247,8 +324,8 @@ class FrameWidgets(ttk.Frame): AppConfig.ensure_directories() for tunnels, values in self.tl.items(): - self.l_box.insert("end", tunnels) - self.l_box.update() + self.list_box.insert("end", tunnels) + self.list_box.update() # Button Vpn if self.a != "": @@ -267,52 +344,55 @@ class FrameWidgets(ttk.Frame): # Button Import self.btn_i = ttk.Button( - self.lb_frame_btn_lbox, + self.control_buttons_frame, image=self.imp_pic, command=self.import_sl, padding=0, ) - self.btn_i.grid(column=0, row=1, padx=15, pady=8) + self.btn_i.grid(column=0, row=1, pady=8) Tooltip(self.btn_i, Msg.TTIP["import_tl"], self.tooltip_state) # Button Trash self.btn_tr = ttk.Button( - self.lb_frame_btn_lbox, + self.control_buttons_frame, image=self.tr_pic, command=self.delete, padding=0, - style="CButton.TButton", ) - self.btn_tr.grid(column=0, row=2, padx=15, pady=8) + self.btn_tr.grid(column=0, row=2, pady=8) - if self.l_box.size() == 0: + if self.list_box.size() == 0: Tooltip(self.btn_tr, Msg.TTIP["trash_tl_info"], self.tooltip_state) else: Tooltip(self.btn_tr, Msg.TTIP["trash_tl"], self.tooltip_state) # Button Export self.btn_exp = ttk.Button( - self.lb_frame_btn_lbox, + self.control_buttons_frame, image=self.exp_pic, command=lambda: Tunnel.export(), padding=0, ) - self.btn_exp.grid(column=0, row=3, padx=15, pady=8) + 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.l_box.size() == 0: + if self.list_box.size() == 0: Tooltip(self.btn_exp, Msg.TTIP["export_tl_info"], self.tooltip_state) else: Tooltip(self.btn_exp, Msg.TTIP["export_tl"], self.tooltip_state) # Label Entry - self.lb_rename = ttk.Entry(self.lb_frame4, width=20) - self.lb_rename.grid(column=2, row=0, padx=8, pady=10, sticky="ne") + self.lb_rename = 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, _("Max. 12 characters!")) self.lb_rename.config(state="disable") - if self.l_box.size() != 0: + if self.list_box.size() != 0: Tooltip( self.lb_rename, Msg.TTIP["rename_tl"], @@ -331,14 +411,12 @@ class FrameWidgets(ttk.Frame): # Button Rename self.btn_rename = ttk.Button( - self.lb_frame4, + self.rename_frame, text=_("Rename"), state="disable", command=self.tl_rename, - padding=4, - style="RnButton.TButton", ) - self.btn_rename.grid(column=3, row=0, padx=5, pady=10, sticky="ne") + self.btn_rename.grid(column=1, row=0, pady=10, sticky="nsew") # Check Buttons self.selected_option = tk.IntVar() @@ -347,19 +425,21 @@ class FrameWidgets(ttk.Frame): # Frame for Labels, Entry and Button self.autoconnect = ttk.Label( - self.lb_frame3, textvariable=self.autoconnect_var, width=15 + self.autoconnect_frame, textvariable=self.autoconnect_var ) self.autoconnect.config(font=("Ubuntu", 11)) - self.autoconnect.grid(column=1, row=0, sticky="e", pady=19) + 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.lb_frame3, + self.autoconnect_frame, text=_("Autoconnect on:"), variable=self.selected_option, command=self.box_set, ) - self.wg_autostart.grid(column=0, row=0, pady=15, padx=15, sticky="nw") + self.wg_autostart.grid(column=0, row=0, pady=10, padx=(10, 0), sticky="ew") - if self.l_box.size() >= 1 and len(self.l_box.curselection()) >= 1: + if self.list_box.size() >= 1 and len(self.list_box.curselection()) >= 1: Tooltip( self.wg_autostart, Msg.TTIP["autostart"], @@ -368,7 +448,7 @@ class FrameWidgets(ttk.Frame): y_offset=-40, ) - if self.l_box.size() == 0: + if self.list_box.size() == 0: Tooltip( self.wg_autostart, Msg.TTIP["autostart_info"], @@ -562,7 +642,9 @@ class FrameWidgets(ttk.Frame): 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()) @@ -571,14 +653,16 @@ class FrameWidgets(ttk.Frame): Start Button """ self.btn_stst = ttk.Button( - self.lb_frame_btn_lbox, + self.control_buttons_frame, image=self.wg_vpn_start, command=lambda: self.wg_switch("start"), padding=0, ) - self.btn_stst.grid(column=0, row=0, padx=5, pady=8) + 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.l_box.size() == 0: + if self.list_box.size() == 0: Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state) else: Tooltip(self.btn_stst, Msg.TTIP["start_tl"], self.tooltip_state) @@ -590,28 +674,32 @@ class FrameWidgets(ttk.Frame): if ConfigManager.get("theme") == "light": self.lb_tunnel = ttk.Label( - self, textvariable=self.str_var, foreground="green" + self.active_frame, textvariable=self.str_var, foreground="green" ) else: self.lb_tunnel = ttk.Label( - self, textvariable=self.str_var, foreground="yellow" + self.active_frame, textvariable=self.str_var, foreground="yellow" ) self.lb_tunnel.config(font=("Ubuntu", 11, "bold")) - self.lb_tunnel.grid(column=2, padx=10, row=1) + 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.lb_frame_btn_lbox, + self.control_buttons_frame, image=self.wg_vpn_stop, command=lambda: self.wg_switch("stop"), padding=0, ) - self.btn_stst.grid(column=0, row=0, padx=5, pady=8) + 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"], self.tooltip_state) @@ -699,11 +787,11 @@ class FrameWidgets(ttk.Frame): AppConfig.ensure_directories() self.str_var.set("") self.a = Tunnel.get_active() - self.l_box.insert(0, self.a) + self.list_box.insert(0, self.a) self.wg_autostart.configure(state="normal") - self.l_box.selection_clear(0, tk.END) - self.l_box.update() - self.l_box.selection_set(0) + self.list_box.selection_clear(0, tk.END) + self.list_box.update() + self.list_box.selection_set(0) Tooltip( self.wg_autostart, @@ -748,8 +836,8 @@ class FrameWidgets(ttk.Frame): delete Wireguard Tunnel """ try: - self.select_tunnel = self.l_box.curselection() - select_tl = self.l_box.get(self.select_tunnel[0]) + 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], @@ -763,7 +851,7 @@ class FrameWidgets(ttk.Frame): f"{process.stderr} Code: {process.returncode}", exc_info=True ) - self.l_box.delete(self.select_tunnel[0]) + self.list_box.delete(self.select_tunnel[0]) Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat") if select_tl == ConfigManager.get("autostart"): @@ -774,7 +862,7 @@ class FrameWidgets(ttk.Frame): self.wg_autostart.configure(state="disabled") # for disabling checkbox when Listbox empty - if self.l_box.size() == 0: + if self.list_box.size() == 0: self.wg_autostart.configure(state="disabled") self.lb_rename.configure(state="disabled") Tooltip( @@ -793,12 +881,12 @@ class FrameWidgets(ttk.Frame): if self.a != "" and self.a == select_tl: self.str_var.set(value="") self.start() - self.l_box.update() + self.list_box.update() self.reset_fields() except IndexError: - if self.l_box.size() != 0: + if self.list_box.size() != 0: MessageDialog("info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) @@ -811,7 +899,7 @@ class FrameWidgets(ttk.Frame): checkbox for enable autostart Tunnel """ AppConfig.get_autostart_content() - if self.l_box.size() != 0: + 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) @@ -838,13 +926,14 @@ class FrameWidgets(ttk.Frame): self.autoconnect_var.set(self.auto_con) self.autoconnect = ttk.Label( - self.lb_frame3, + self.autoconnect_frame, textvariable=self.autoconnect_var, foreground="#0071ff", - width=15, + width=18, ) self.autoconnect.config(font=("Ubuntu", 11)) - self.autoconnect.grid(column=1, row=0, sticky="e", pady=19) + self.autoconnect.grid(column=1, row=0, sticky="ew", pady=19) + self.autoconnect.rowconfigure(0, weight=1) def box_set(self) -> None: """ @@ -858,13 +947,13 @@ class FrameWidgets(ttk.Frame): to disable the autostart. """ try: - select_tunnel = self.l_box.curselection() - select_tl = self.l_box.get(select_tunnel[0]) + 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.l_box.size() == 0: + if self.list_box.size() == 0: self.wg_autostart.configure(state="disabled") if self.selected_option.get() >= 1: @@ -906,8 +995,8 @@ class FrameWidgets(ttk.Frame): else: try: - self.select_tunnel = self.l_box.curselection() - select_tl = self.l_box.get(self.select_tunnel[0]) + 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( @@ -935,9 +1024,9 @@ class FrameWidgets(ttk.Frame): if select_tl == ConfigManager.get("autostart"): ConfigManager.set("autostart", self.lb_rename.get()) self.autoconnect_var.set(value=self.lb_rename.get()) - self.l_box.delete(self.select_tunnel[0]) - self.l_box.insert("end", self.lb_rename.get()) - self.l_box.update() + self.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() @@ -968,34 +1057,36 @@ class FrameWidgets(ttk.Frame): in the UI using ttk.Label widgets. Creates three labels for address, DNS, and endpoint with specific styling (color, font), positioning them in a - grid layout (`lb_frame` and `lb_frame2`). + 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.lb_frame, textvariable=self.add, foreground="#0071ff" + self.interface_frame, textvariable=self.add, foreground="#0071ff" ) - self.address.grid(column=0, row=5, sticky="w", padx=10, pady=6) + 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.lb_frame, textvariable=self.DNS, foreground="#0071ff") - self.dns.grid(column=0, row=7, sticky="w", padx=10, pady=6) + 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.lb_frame2, textvariable=self.enp, foreground="#0071ff" + self.peer_frame, textvariable=self.enp, foreground="#0071ff" ) - self.endpoint.grid(column=0, row=8, sticky="w", padx=10, pady=20) + 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 (`l_box`). + 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 @@ -1003,8 +1094,8 @@ class FrameWidgets(ttk.Frame): """ try: if self.a == "": - self.select_tunnel = self.l_box.curselection() - select_tl = self.l_box.get(self.select_tunnel[0]) + 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: @@ -1014,7 +1105,7 @@ class FrameWidgets(ttk.Frame): except IndexError: - if self.l_box.size() != 0: + if self.list_box.size() != 0: MessageDialog("info", Msg.STR["sel_list"], title=Msg.STR["sel_tl"]) diff --git a/wp_app_config.py b/wp_app_config.py index db81efe..135d0a0 100755 --- a/wp_app_config.py +++ b/wp_app_config.py @@ -57,18 +57,18 @@ class AppConfig: # Updates # 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year - VERSION: str = "v. 2.06.2425" + VERSION: str = "v. 2.06.2625" UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" DOWNLOAD_URL: str = "https://git.ilunix.de/punix/Wire-Py/archive" # UI configuration UI_CONFIG: Dict[str, Any] = { - "window_title": "Wire-Py", + "window_title": "", "window_title2": "LogViewer", - "window_size": (600, 383), + "window_size": (558, 450), "font_family": "Ubuntu", "font_size": 11, - "resizable_window": (False, False), + "resizable_window": (True, True), } # System-dependent paths @@ -81,6 +81,7 @@ class AppConfig: # Images and icons paths IMAGE_PATHS: Dict[str, Path] = { + "icon_header": "/usr/share/icons/lx-icons/32/wg_vpn.png", "icon_vpn": "/usr/share/icons/lx-icons/48/wg_vpn.png", "icon_msg": "/usr/share/icons/lx-icons/48/wg_msg.png", "icon_import": "/usr/share/icons/lx-icons/48/wg_import.png", @@ -193,6 +194,8 @@ class Msg: STR: Dict[str, str] = { # Strings for messages + "header_left_bottom": _("Simple GUI for Wireguard"), + "header_right_top": _("Wireguard VPN Manager"), "sel_tl": _("Select tunnel"), "ren_err": _("Renaming not possible"), "exp_succ": _("Export successful"), -- 2.49.0 From ddae246d4609ff02bf44318130d07f74932ef3a7 Mon Sep 17 00:00:00 2001 From: punix Date: Fri, 27 Jun 2025 23:09:21 +0200 Subject: [PATCH 3/6] ui works now better with rename button --- Changelog | 12 +++++++++++- wirepy.py | 7 ++++--- wp_app_config.py | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Changelog b/Changelog index e623ab7..7d99d8f 100644 --- a/Changelog +++ b/Changelog @@ -6,11 +6,21 @@ My standard System: Linux Mint 22 Cinnamon - os import in common_tools.py replaced by other methods - If Wire-Py already runs, prevent further start - for loops with lists replaced by List Comprehensions - - Tunnel in tk.canvas for modern look - Replace Download Button with Lx Tools installer ### Added 23-06-2025 + + - Header added for more modern desing + + - Sizes adjust the frames and labels improve + + - More modern desing for listbox, Address, Dns and Endpoint + + - ui works now better with rename button + + ### Added +23-06-2025 - all msg_window with MassageDialog replaced diff --git a/wirepy.py b/wirepy.py index 732df7b..0ddfbce 100755 --- a/wirepy.py +++ b/wirepy.py @@ -48,10 +48,10 @@ class Wirepy(tk.Tk): AppConfig.UI_CONFIG["resizable_window"][0], AppConfig.UI_CONFIG["resizable_window"][1], ) - """self.minsize( + self.minsize( AppConfig.UI_CONFIG["window_size"][0], AppConfig.UI_CONFIG["window_size"][1], - )""" + ) self.title(AppConfig.UI_CONFIG["window_title"]) self.tk.call("source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") @@ -301,7 +301,7 @@ class FrameWidgets(ttk.Frame): # 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=1) + self.rename_frame.columnconfigure(0, weight=2) self.rename_frame.columnconfigure(1, weight=1) # Label to Show active Tunnel @@ -415,6 +415,7 @@ class FrameWidgets(ttk.Frame): text=_("Rename"), state="disable", command=self.tl_rename, + width=15, ) self.btn_rename.grid(column=1, row=0, pady=10, sticky="nsew") diff --git a/wp_app_config.py b/wp_app_config.py index 135d0a0..b4ff7ed 100755 --- a/wp_app_config.py +++ b/wp_app_config.py @@ -65,7 +65,7 @@ class AppConfig: UI_CONFIG: Dict[str, Any] = { "window_title": "", "window_title2": "LogViewer", - "window_size": (558, 450), + "window_size": (590, 450), "font_family": "Ubuntu", "font_size": 11, "resizable_window": (True, True), -- 2.49.0 From 47aa3ac7498202393dbf6cfec7c43b605e084f93 Mon Sep 17 00:00:00 2001 From: punix Date: Sat, 28 Jun 2025 00:32:49 +0200 Subject: [PATCH 4/6] add class Image for icon managment --- Changelog | 2 ++ wirepy.py | 41 +++++++++++++------------- wp_app_config.py | 76 ++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 83 insertions(+), 36 deletions(-) diff --git a/Changelog b/Changelog index 7d99d8f..5ea5338 100644 --- a/Changelog +++ b/Changelog @@ -19,6 +19,8 @@ My standard System: Linux Mint 22 Cinnamon - ui works now better with rename button + - add Image class for manage Images + ### Added 23-06-2025 diff --git a/wirepy.py b/wirepy.py index 0ddfbce..2a82723 100755 --- a/wirepy.py +++ b/wirepy.py @@ -24,7 +24,7 @@ from shared_libs.common_tools import ( Tooltip, ) -from wp_app_config import AppConfig, Msg +from shared_libs.wp_app_config import AppConfig, Image, Msg class Wirepy(tk.Tk): @@ -39,6 +39,7 @@ class Wirepy(tk.Tk): self.withdraw() self.my_tool_tip = None + self.x_width = AppConfig.UI_CONFIG["window_size"][0] self.y_height = AppConfig.UI_CONFIG["window_size"][1] @@ -53,17 +54,19 @@ class Wirepy(tk.Tk): AppConfig.UI_CONFIG["window_size"][1], ) self.title(AppConfig.UI_CONFIG["window_title"]) - + self.image_manager = Image() self.tk.call("source", f"{AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl") ConfigManager.init(AppConfig.SETTINGS_FILE) theme = ConfigManager.get("theme") ThemeManager.change_theme(self, theme) - # Load the image file from the disk - self.wg_icon = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_vpn"]) - - # Set it as the window icon - self.iconphoto(True, self.wg_icon) + # Try to set icon + try: + icon = self.image_manager.load_image("icon_vpn") + if icon: + self.iconphoto(True, icon) + except: + pass # Add the widgets FrameWidgets(self).grid() @@ -91,13 +94,7 @@ class FrameWidgets(ttk.Frame): self.dns = None self.address = None self.auto_con = None - self.wg_vpn_start = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_start"]) - self.wg_vpn_stop = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_stop"]) - self.imp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_import"]) - self.tr_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_trash"]) - self.exp_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_export"]) - self.warning_pic = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_error"]) - self.wg_icon_header = tk.PhotoImage(file=AppConfig.IMAGE_PATHS["icon_header"]) + self.image_manager = Image() self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.columnconfigure(1, weight=1) @@ -211,7 +208,9 @@ class FrameWidgets(ttk.Frame): 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.wg_icon_header, bg="#2c3e50" + self.wg_icon_header_frame, + image=self.image_manager.load_image("icon_header"), + bg="#2c3e50", ) self.wg_icon_header_label.grid(column=0, row=0, sticky="e", ipadx=10) @@ -345,7 +344,7 @@ class FrameWidgets(ttk.Frame): # Button Import self.btn_i = ttk.Button( self.control_buttons_frame, - image=self.imp_pic, + image=self.image_manager.load_image("icon_import"), command=self.import_sl, padding=0, ) @@ -356,7 +355,7 @@ class FrameWidgets(ttk.Frame): # Button Trash self.btn_tr = ttk.Button( self.control_buttons_frame, - image=self.tr_pic, + image=self.image_manager.load_image("icon_trash"), command=self.delete, padding=0, ) @@ -370,7 +369,7 @@ class FrameWidgets(ttk.Frame): # Button Export self.btn_exp = ttk.Button( self.control_buttons_frame, - image=self.exp_pic, + image=self.image_manager.load_image("icon_export"), command=lambda: Tunnel.export(), padding=0, ) @@ -560,7 +559,7 @@ class FrameWidgets(ttk.Frame): None, partial(webbrowser.open, "https://git.ilunix.de/punix/Wire-Py"), ], - icon=AppConfig.IMAGE_PATHS["icon_vpn"], + icon="/usr/share/icons/lx-icons/64/wg_vpn.png", wraplength=420, ) @@ -655,7 +654,7 @@ class FrameWidgets(ttk.Frame): """ self.btn_stst = ttk.Button( self.control_buttons_frame, - image=self.wg_vpn_start, + image=self.image_manager.load_image("icon_start"), command=lambda: self.wg_switch("start"), padding=0, ) @@ -694,7 +693,7 @@ class FrameWidgets(ttk.Frame): """ self.btn_stst = ttk.Button( self.control_buttons_frame, - image=self.wg_vpn_stop, + image=self.image_manager.load_image("icon_stop"), command=lambda: self.wg_switch("stop"), padding=0, ) diff --git a/wp_app_config.py b/wp_app_config.py index b4ff7ed..5cbd337 100755 --- a/wp_app_config.py +++ b/wp_app_config.py @@ -3,6 +3,8 @@ import logging import gettext import locale +import tkinter as tk +import os from pathlib import Path from subprocess import CompletedProcess, run from typing import Dict, Any @@ -79,21 +81,6 @@ class AppConfig: "pkey_path": "/usr/local/etc/ssl/pwgk.pem", } - # Images and icons paths - IMAGE_PATHS: Dict[str, Path] = { - "icon_header": "/usr/share/icons/lx-icons/32/wg_vpn.png", - "icon_vpn": "/usr/share/icons/lx-icons/48/wg_vpn.png", - "icon_msg": "/usr/share/icons/lx-icons/48/wg_msg.png", - "icon_import": "/usr/share/icons/lx-icons/48/wg_import.png", - "icon_export": "/usr/share/icons/lx-icons/48/wg_export.png", - "icon_trash": "/usr/share/icons/lx-icons/48/wg_trash.png", - "icon_start": "/usr/share/icons/lx-icons/48/wg_vpn-start.png", - "icon_stop": "/usr/share/icons/lx-icons/48/wg_vpn-stop.png", - "icon_info": "/usr/share/icons/lx-icons/64/info.png", - "icon_error": "/usr/share/icons/lx-icons/64/error.png", - "icon_log": "/usr/share/icons/lx-icons/48/log.png", - } - @staticmethod def setup_translations() -> gettext.gettext: """ @@ -171,6 +158,65 @@ class AppConfig: _ = AppConfig.setup_translations() +class Image: + def __init__(self): + self.images = {} + + def load_image(self, image_key, fallback_paths=None) -> None | tk.PhotoImage: + """Load PNG image using tk.PhotoImage with fallback options""" + if image_key in self.images: + return self.images[image_key] + + # Define image paths based on key + image_paths = { + "icon_header": [ + "/usr/share/icons/lx-icons/32/wg_vpn.png", + ], + "icon_vpn": [ + "/usr/share/icons/lx-icons/48/wg_vpn.png", + ], + "icon_start": [ + "/usr/share/icons/lx-icons/48/wg_vpn-start.png", + ], + "icon_stop": [ + "/usr/share/icons/lx-icons/48/wg_vpn-stop.png", + ], + "icon_import": [ + "/usr/share/icons/lx-icons/48/wg_import.png", + ], + "icon_export": [ + "/usr/share/icons/lx-icons/48/wg_export.png", + ], + "icon_log": [ + "/usr/share/icons/lx-icons/48/log.png", + ], + "icon_trash": [ + "/usr/share/icons/lx-icons/48/wg_trash.png", + ], + } + + # Get paths to try + paths_to_try = image_paths.get(image_key, []) + + # Add fallback paths if provided + if fallback_paths: + paths_to_try.extend(fallback_paths) + + # Try to load image from paths + for path in paths_to_try: + try: + if os.path.exists(path): + photo = tk.PhotoImage(file=path) + self.images[image_key] = photo + return photo + except tk.TclError as e: + # print(f"{LocaleStrings.MSGP["fail_load_image"]}{path}: {e}") + continue + + # Return None if no image found + return None + + class Msg: """ A utility class that provides centralized access to translated message strings. -- 2.49.0 From 7b8ec10e6cd719355eebd65102070da1f12c2171 Mon Sep 17 00:00:00 2001 From: punix Date: Wed, 2 Jul 2025 23:25:57 +0200 Subject: [PATCH 5/6] updater replace Downloadbutton with Lx Tools installer --- Changelog | 14 +++++++++++++- lx-icons/16/settings.png | Bin 0 -> 575 bytes wirepy.py | 38 ++++++++++++++++++++++---------------- wp_app_config.py | 10 +++++----- 4 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 lx-icons/16/settings.png diff --git a/Changelog b/Changelog index 5ea5338..70328fe 100644 --- a/Changelog +++ b/Changelog @@ -9,7 +9,19 @@ My standard System: Linux Mint 22 Cinnamon - Replace Download Button with Lx Tools installer ### Added -23-06-2025 +02-07-2025 + +- Complete code for faulty f" string configuration dur addicted and fixed + +- updater replace Downloadbutton with Lx Tools installer + +- remove logviewer icon + +- add settings.png icon for update button + + + ### Added +27-06-2025 - Header added for more modern desing diff --git a/lx-icons/16/settings.png b/lx-icons/16/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..5a09d743e5384996a1199ac0d9a8f46523a8d570 GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;x#X;^) zj5kl})B`yR$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?d}4kf#9d}Zjf%y0*}aI1_nK45N51cYF`EvWH0gbb!C6aCd;d%yJVJK1JK51PZ!4! zi_>c-@AYDK6lvXmcTLBpYym|!Clwan4l}b`O9kW|#p<~(-#EjZD=3}I-1LK?>qcj* zW{A)QHxc2;;G7F8yCS*MQ%nQ{_uQ3vx3l=&Ok=kDAtyh}D5&>`hIcH8pMSA^2E(ia zwhufk7$!BmYIO44m3$V=ViF`(Jk*i&cE(&H1MSt$#m^ z<(E__vpvBpDqGYr=VZRtwYQBv3$&glxUg|*`VnaQ*KmQRbZl?X#hnc1Bn}Y%A_4oyn0t!!|bk$3?f( zoxC Date: Wed, 9 Jul 2025 08:31:26 +0200 Subject: [PATCH 6/6] updater replace Downloadbutton with Lx Tools installer --- languages/de/wirepy.mo | Bin 5793 -> 4776 bytes lx-icons/16/settings.png | Bin 575 -> 757 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/languages/de/wirepy.mo b/languages/de/wirepy.mo index 6abb961e1d3d397191fd1ef0482568cb3106f01b..836e3a0c189c37d2a6de47b40fa4e5f4b918e6da 100644 GIT binary patch literal 4776 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 W_}|Fd)dRYPQx>03LW45eiT?re9nG}> literal 5793 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 YzFR|4SA3EvS1c&&@syIfQnOC|2ceXIw*UYD diff --git a/lx-icons/16/settings.png b/lx-icons/16/settings.png index 5a09d743e5384996a1199ac0d9a8f46523a8d570..8f49da2556ae3da5e2cc48f2dc2e4f355394eef1 100644 GIT binary patch delta 626 zcmV-&0*(E@1oZ`wa|!y{D6rd>?-Y0|gioDr30r0006xNklOD<7!RxMEz@y`-0jejy!OQ2nZxF}@lAVRUAi%X#toSej|#iffN2*oZ^x@2%D zLKjON45+mSt)NYbo{>N_xl@VBaUJAKtJW6!OyBqNJm34he;$cH!_^mwJPwCKjR1ce znwI{sy7~qvUL`uO+}mQ&=%d3-#vB_So^{GH0Qb7PIXyd58hk!)e0ljDkQ4$*9C|uC z9}dN0OU;3RYM3Uf*L&$@5liX1u^5jJtZi+r060WuAqL7~+qSW7o8M#2fq*&`i!A|d z05U)<8XbGk+dC+-Wpg>!cM@2Ys~mrqG&Z*k>Z)sTD+&O&RMmZce!l;0clVPMa2;kc zrubqum!qh@XX?c!m6b;TO-+BArr`Fk z@yRmbP^i&yjVW;>3We)*$A+e*OO|z^l_Y+AZ6%+tzeYUD`pKq(<$KAp*wD1J{C#il z%l^TE-qGGZ=vEZ!s%u$UyQtaYG0WnamtBNf?xVO|7^cbW!ov7UBJtV@6dQa#ub6YS z*Nb%0&(uUKO~Gvn1qazwknUdgJ5*Ivly{=R=ko$ZQE}VM?-X9Stlkq?$f}0004lNklW<<0Fr?13Q>RW zJD}uw1Hcxr0bBypUuG+82iSClcmc4q21Q1ZBO`jhB{I+ks-D7!0x7$uCN)w%XVb^{ z;T-@8E6rRvk5iB9ipE_b>PkUVNo=HijxzfU6(@<;#sPTFbU0YuWieXDc^FaaGyx3h z_N_APaA}WDaSXF&eKH`-wi#V{W#NBt3C-A)DWA$wJNw~X4;dhKJ;)6e9ddx2$!{vw z?SV|7D&=5A1_>)oH`D2945!*mgflu6098{aEGdm)uAHZ}dPSIRejhI5E@nFwc?JzZz`(MHRO4Ls>d+}1+!Q|-7y6qF|isy1az ll|=eP85QOK|4e`V&To0UiORaW7XAPL002ovPDHLkV1mcTzViS8 -- 2.49.0