""" Server add/edit dialog — modal window with all server fields. """ import customtkinter as ctk from core.server_store import SERVER_TYPES, DEFAULT_PORTS from core.i18n import t def _get_network_interfaces() -> list[tuple[str, str]]: """Return list of (name, ipv4_address) for available network interfaces.""" try: import psutil result = [] for name, addrs in psutil.net_if_addrs().items(): for addr in addrs: if addr.family.name == "AF_INET" and addr.address != "127.0.0.1": result.append((name, addr.address)) return result except Exception: return [] class ServerDialog(ctk.CTkToplevel): def __init__(self, master, store, server: dict | None = None): super().__init__(master) self.store = store self.editing = server self.result = None self.title(t("edit_server") if server else t("add_server")) self.geometry("450x680") self.resizable(False, False) self.grab_set() # Center on parent self.transient(master) self._build_ui(server) def _build_ui(self, server: dict | None): pad = {"padx": 20, "pady": (5, 0)} entry_pad = {"padx": 20, "pady": (2, 5)} # Alias ctk.CTkLabel(self, text=t("alias"), anchor="w").pack(fill="x", **pad) self.alias_entry = ctk.CTkEntry(self, placeholder_text=t("placeholder_alias")) self.alias_entry.pack(fill="x", **entry_pad) # IP ctk.CTkLabel(self, text=t("ip"), anchor="w").pack(fill="x", **pad) self.ip_entry = ctk.CTkEntry(self, placeholder_text=t("placeholder_ip")) self.ip_entry.pack(fill="x", **entry_pad) # Type + Port row row = ctk.CTkFrame(self, fg_color="transparent") row.pack(fill="x", padx=20, pady=(5, 5)) type_frame = ctk.CTkFrame(row, fg_color="transparent") type_frame.pack(side="left", fill="x", expand=True, padx=(0, 5)) ctk.CTkLabel(type_frame, text=t("type"), anchor="w").pack(fill="x") self.type_var = ctk.StringVar(value="ssh") self.type_menu = ctk.CTkOptionMenu( type_frame, values=SERVER_TYPES, variable=self.type_var, command=self._on_type_change ) self.type_menu.pack(fill="x") port_frame = ctk.CTkFrame(row, fg_color="transparent") port_frame.pack(side="left", fill="x", expand=True, padx=(5, 0)) ctk.CTkLabel(port_frame, text=t("port"), anchor="w").pack(fill="x") self.port_entry = ctk.CTkEntry(port_frame, placeholder_text=t("placeholder_port")) self.port_entry.pack(fill="x") # Network interface ctk.CTkLabel(self, text=t("network_interface"), anchor="w").pack(fill="x", **pad) self._iface_map: dict[str, str] = {} # display_name -> ip ifaces = _get_network_interfaces() auto_label = t("auto_default") iface_values = [auto_label] for name, ip in ifaces: label = f"{name} ({ip})" iface_values.append(label) self._iface_map[label] = ip self._iface_var = ctk.StringVar(value=auto_label) self._iface_menu = ctk.CTkOptionMenu(self, values=iface_values, variable=self._iface_var) self._iface_menu.pack(fill="x", **entry_pad) # User ctk.CTkLabel(self, text=t("username"), anchor="w").pack(fill="x", **pad) self.user_entry = ctk.CTkEntry(self, placeholder_text=t("placeholder_user")) self.user_entry.pack(fill="x", **entry_pad) # Password ctk.CTkLabel(self, text=t("password"), anchor="w").pack(fill="x", **pad) pass_frame = ctk.CTkFrame(self, fg_color="transparent") pass_frame.pack(fill="x", padx=20, pady=(2, 5)) self.password_entry = ctk.CTkEntry(pass_frame, show="*", placeholder_text=t("placeholder_password")) self.password_entry.pack(side="left", fill="x", expand=True, padx=(0, 5)) self.show_pass = ctk.CTkButton(pass_frame, text=t("show"), width=60, command=self._toggle_password) self.show_pass.pack(side="right") self._pass_visible = False # TOTP Secret ctk.CTkLabel(self, text=t("totp_secret_dialog"), anchor="w").pack(fill="x", **pad) self.totp_entry = ctk.CTkEntry(self, placeholder_text=t("placeholder_totp_secret"), font=ctk.CTkFont(family="Consolas", size=12)) self.totp_entry.pack(fill="x", **entry_pad) # Skip status checks self.skip_check_var = ctk.BooleanVar(value=False) self.skip_check_cb = ctk.CTkCheckBox( self, text=t("skip_check"), variable=self.skip_check_var ) self.skip_check_cb.pack(fill="x", padx=20, pady=(8, 2)) # Notes ctk.CTkLabel(self, text=t("notes"), anchor="w").pack(fill="x", **pad) self.notes_entry = ctk.CTkEntry(self, placeholder_text=t("placeholder_notes")) self.notes_entry.pack(fill="x", **entry_pad) # Buttons btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=20, pady=(15, 20)) ctk.CTkButton(btn_frame, text=t("cancel"), fg_color="#6b7280", command=self.destroy).pack(side="left", expand=True, padx=(0, 5)) ctk.CTkButton(btn_frame, text=t("save"), command=self._save).pack(side="right", expand=True, padx=(5, 0)) # Fill values if editing if server: self.alias_entry.insert(0, server.get("alias", "")) self.alias_entry.configure(state="disabled") self.ip_entry.insert(0, server.get("ip", "")) self.type_var.set(server.get("type", "ssh")) self.port_entry.insert(0, str(server.get("port", 22))) self.user_entry.insert(0, server.get("user", "")) self.password_entry.insert(0, server.get("password", "")) self.totp_entry.insert(0, server.get("totp_secret", "")) self.skip_check_var.set(server.get("skip_check", False)) self.notes_entry.insert(0, server.get("notes", "")) # Restore network interface selection saved_ip = server.get("bind_interface") if saved_ip: found = False for label, ip in self._iface_map.items(): if ip == saved_ip: self._iface_var.set(label) found = True break if not found: unavail_label = f"? ({saved_ip})" self._iface_map[unavail_label] = saved_ip current_values = self._iface_menu.cget("values") current_values.append(unavail_label) self._iface_menu.configure(values=current_values) self._iface_var.set(unavail_label) def _on_type_change(self, value): default_port = DEFAULT_PORTS.get(value, 22) self.port_entry.delete(0, "end") self.port_entry.insert(0, str(default_port)) def _toggle_password(self): self._pass_visible = not self._pass_visible self.password_entry.configure(show="" if self._pass_visible else "*") self.show_pass.configure(text=t("hide") if self._pass_visible else t("show")) def _save(self): alias = self.alias_entry.get().strip() ip = self.ip_entry.get().strip() port_str = self.port_entry.get().strip() user = self.user_entry.get().strip() password = self.password_entry.get() server_type = self.type_var.get() totp_secret = self.totp_entry.get().strip() notes = self.notes_entry.get().strip() # Validation if not alias: self._show_error(t("alias_required")) return if not ip: self._show_error(t("ip_required")) return try: port = int(port_str) if port_str else DEFAULT_PORTS.get(server_type, 22) except ValueError: self._show_error(t("port_must_be_number")) return if port < 1 or port > 65535: self._show_error(t("port_out_of_range")) return server_data = { "alias": alias, "ip": ip, "port": port, "user": user or "root", "password": password, "type": server_type, "notes": notes, } if totp_secret: server_data["totp_secret"] = totp_secret if self.skip_check_var.get(): server_data["skip_check"] = True # Network interface binding iface_selection = self._iface_var.get() bind_ip = self._iface_map.get(iface_selection) if bind_ip: server_data["bind_interface"] = bind_ip try: if self.editing: self.store.update_server(alias, server_data) else: self.store.add_server(server_data) self.result = server_data self.destroy() except ValueError as e: self._show_error(str(e)) def _show_error(self, message: str): # Simple error via title flash self.title(t("error_prefix").format(msg=message)) self.after(2000, lambda: self.title(t("edit_server") if self.editing else t("add_server")))