diff --git a/locale/en.po b/locale/en.po index f465446..cdf1580 100755 --- a/locale/en.po +++ b/locale/en.po @@ -5191,6 +5191,26 @@ msgstr "System keyring not available - password will be stored in plain text" msgid "SSH Options" msgstr "SSH Options" +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 730 +msgid "SSH Session Type" +msgstr "SSH Session Type" + +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 731 +msgid "Use Network Device CLI for switches, routers and appliances" +msgstr "Use Network Device CLI for switches, routers and appliances" + +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 734 +msgid "Unix/Linux Shell" +msgstr "Unix/Linux Shell" + +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 734 +msgid "Network Device CLI" +msgstr "Network Device CLI" + # # File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 680 msgid "Run Command After Login" diff --git a/locale/pt.po b/locale/pt.po index a2bc392..d611129 100644 --- a/locale/pt.po +++ b/locale/pt.po @@ -4737,6 +4737,26 @@ msgstr "O chaveiro do sistema não está disponível - a senha será armazenada msgid "SSH Options" msgstr "Opções SSH" # +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 730 +#: NEEDS WORK +msgid "SSH Session Type" +msgstr "Tipo de Sessão SSH" +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 731 +#: NEEDS WORK +msgid "Use Network Device CLI for switches, routers and appliances" +msgstr "Use CLI de Equipamento de Rede para switches, roteadores e appliances" +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 734 +#: NEEDS WORK +msgid "Unix/Linux Shell" +msgstr "Shell Unix/Linux" +# +# File: src/zashterminal/ui/dialogs/session_edit_dialog.py, line: 734 +#: NEEDS WORK +msgid "Network Device CLI" +msgstr "CLI de Equipamento de Rede" +# # File: usr/share/zashterminal/ui/dialogs/session_edit_dialog.py, line: 309 #: NEEDS WORK msgid "Run Command After Login" diff --git a/src/zashterminal/sessions/models.py b/src/zashterminal/sessions/models.py index 2d8948f..43b9100 100644 --- a/src/zashterminal/sessions/models.py +++ b/src/zashterminal/sessions/models.py @@ -107,6 +107,7 @@ def __init__( sftp_remote_directory: str = "", port_forwardings: Optional[List[Dict[str, Any]]] = None, x11_forwarding: bool = False, + ssh_connection_mode: str = "shell", source: str = "user", local_working_directory: str = "", local_startup_command: str = "", @@ -150,6 +151,11 @@ def __init__( if port_forwardings: self.port_forwardings = port_forwardings self._x11_forwarding = bool(x11_forwarding) + self._ssh_connection_mode = ( + ssh_connection_mode + if ssh_connection_mode in {"shell", "network_device"} + else "shell" + ) self._source = source or "user" # Local terminal specific properties self._local_working_directory = ( @@ -420,6 +426,21 @@ def x11_forwarding(self, value: bool): self._x11_forwarding = new_value self._mark_modified() + @property + def ssh_connection_mode(self) -> str: + return self._ssh_connection_mode + + @ssh_connection_mode.setter + def ssh_connection_mode(self, value: str): + new_value = value if value in {"shell", "network_device"} else "shell" + if self._ssh_connection_mode != new_value: + self._ssh_connection_mode = new_value + self._mark_modified() + + @property + def uses_network_device_mode(self) -> bool: + return self.is_ssh() and self._ssh_connection_mode == "network_device" + @property def source(self) -> str: return self._source @@ -576,6 +597,7 @@ def to_dict(self) -> Dict[str, Any]: "sftp_remote_directory": self.sftp_remote_directory, "port_forwardings": self.port_forwardings, "x11_forwarding": self.x11_forwarding, + "ssh_connection_mode": self.ssh_connection_mode, "local_working_directory": self.local_working_directory, "local_startup_command": self.local_startup_command, @@ -608,6 +630,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "SessionItem": sftp_remote_directory=data.get("sftp_remote_directory", ""), port_forwardings=data.get("port_forwardings", []), x11_forwarding=data.get("x11_forwarding", False), + ssh_connection_mode=data.get("ssh_connection_mode", "shell"), source=data.get("source", "user"), local_working_directory=data.get("local_working_directory", ""), local_startup_command=data.get("local_startup_command", ""), diff --git a/src/zashterminal/terminal/spawner.py b/src/zashterminal/terminal/spawner.py index 56ea682..3e6824f 100644 --- a/src/zashterminal/terminal/spawner.py +++ b/src/zashterminal/terminal/spawner.py @@ -1029,6 +1029,12 @@ def _build_remote_command_secure( } if persist_duration > 0: ssh_options["ControlPersist"] = str(persist_duration) + if command_type == "ssh" and getattr( + session, "uses_network_device_mode", False + ): + ssh_options.pop("ControlPersist", None) + ssh_options.pop("ControlMaster", None) + ssh_options.pop("ControlPath", None) if command_type == "ssh" and getattr(session, "x11_forwarding", False): ssh_options.pop("ControlPersist", None) ssh_options.pop("ControlMaster", None) @@ -1079,7 +1085,16 @@ def _build_remote_command_secure( insertion_index = max(len(cmd) - 1, 1) cmd[insertion_index:insertion_index] = ["-L", forward_spec] - if command_type == "ssh": + if command_type == "ssh" and getattr( + session, "uses_network_device_mode", False + ): + if "-tt" not in cmd and "-t" not in cmd: + cmd.insert(1, "-tt") + if initial_command: + self.logger.info( + "Ignoring initial SSH command for Network Device CLI session." + ) + elif command_type == "ssh": # Restore OSC7 emission for remote CWD tracking (used by the built-in # file manager via VTE's current-directory-uri). We skip fish because # the PROMPT_COMMAND pattern is bash-specific and can break fish login. diff --git a/src/zashterminal/ui/dialogs/session_edit_dialog.py b/src/zashterminal/ui/dialogs/session_edit_dialog.py index 442880e..e0439dd 100644 --- a/src/zashterminal/ui/dialogs/session_edit_dialog.py +++ b/src/zashterminal/ui/dialogs/session_edit_dialog.py @@ -69,6 +69,7 @@ def __init__( dict(item) for item in self.editing_session.port_forwardings ] self.x11_switch: Optional[Adw.SwitchRow] = None + self.ssh_mode_combo: Optional[Adw.ComboRow] = None # Local terminal options self.local_terminal_group: Optional[Adw.PreferencesGroup] = None self.startup_commands_group: Optional[Adw.PreferencesGroup] = None @@ -725,6 +726,21 @@ def _create_ssh_options_group(self, parent: Adw.PreferencesPage) -> None: # Other SSH options in a separate group options_group = Adw.PreferencesGroup() + self.ssh_mode_combo = Adw.ComboRow( + title=_("SSH Session Type"), + subtitle=_("Use Network Device CLI for switches, routers and appliances"), + ) + self.ssh_mode_combo.set_model( + Gtk.StringList.new([_("Unix/Linux Shell"), _("Network Device CLI")]) + ) + self.ssh_mode_combo.set_selected( + 1 + if self.editing_session.ssh_connection_mode == "network_device" + else 0 + ) + self.ssh_mode_combo.connect("notify::selected", self._on_ssh_mode_changed) + options_group.add(self.ssh_mode_combo) + # X11 Forwarding toggle self.x11_switch = Adw.SwitchRow( title=_("Enable X11 Forwarding"), @@ -770,6 +786,7 @@ def _create_ssh_options_group(self, parent: Adw.PreferencesPage) -> None: # Update initial visibility self._update_post_login_command_state() self._update_sftp_state() + self._update_network_device_state() def _create_port_forward_widgets(self, parent_group: Adw.PreferencesGroup) -> None: """Create port forwarding widgets inside the SSH Options group.""" @@ -1103,12 +1120,14 @@ def _on_host_changed(self, entry: Gtk.Entry) -> None: def _on_user_changed(self, entry: Gtk.Entry) -> None: self._mark_changed() - def _on_port_changed(self, spin_row, _param) -> None: + def _on_port_changed(self, value: float) -> None: self._mark_changed() - port = int(spin_row.get_value()) - spin_row.remove_css_class( + if not self.port_row: + return + port = int(value) + self.port_row.remove_css_class( "error" - ) if 1 <= port <= 65535 else spin_row.add_css_class("error") + ) if 1 <= port <= 65535 else self.port_row.add_css_class("error") def _on_auth_changed(self, combo_row, param) -> None: self._update_auth_visibility() @@ -1159,6 +1178,10 @@ def _on_sftp_toggle(self, switch_row: Adw.SwitchRow, _param) -> None: if self.sftp_local_entry and not switch_row.get_active(): self.sftp_local_entry.remove_css_class("error") + def _on_ssh_mode_changed(self, combo_row: Adw.ComboRow, _param) -> None: + self._mark_changed() + self._update_network_device_state() + def _on_sftp_local_changed(self, entry: Gtk.Entry) -> None: entry.remove_css_class("error") self._mark_changed() @@ -1187,9 +1210,12 @@ def _update_ssh_visibility(self) -> None: self.sftp_switch.set_sensitive(is_ssh) if not is_ssh: self.sftp_switch.set_active(False) + if self.ssh_mode_combo: + self.ssh_mode_combo.set_sensitive(is_ssh) self._update_port_forward_state() self._update_post_login_command_state() self._update_sftp_state() + self._update_network_device_state() def _update_local_visibility(self) -> None: """Update visibility of local terminal options based on session type.""" @@ -1210,6 +1236,7 @@ def _update_auth_visibility(self) -> None: self._update_port_forward_state() self._update_post_login_command_state() self._update_sftp_state() + self._update_network_device_state() def _update_port_forward_state(self) -> None: is_ssh_session = ( @@ -1231,18 +1258,48 @@ def _update_post_login_command_state(self) -> None: is_ssh_session = ( self.type_combo.get_selected() == 1 if self.type_combo else False ) + is_network_device = ( + self.ssh_mode_combo is not None + and self.ssh_mode_combo.get_selected() == 1 + and is_ssh_session + ) # Control visibility of switch and container based on session type - self.post_login_switch.set_sensitive(is_ssh_session) + self.post_login_switch.set_sensitive(is_ssh_session and not is_network_device) if ( hasattr(self, "post_login_command_container") and self.post_login_command_container ): # Show container only if SSH session AND switch is active - is_enabled = self.post_login_switch.get_active() and is_ssh_session + is_enabled = ( + self.post_login_switch.get_active() + and is_ssh_session + and not is_network_device + ) self.post_login_command_container.set_visible(is_enabled) - if not is_ssh_session: + if not is_ssh_session or is_network_device: self.post_login_entry.remove_css_class("error") + def _update_network_device_state(self) -> None: + is_ssh_session = ( + self.type_combo.get_selected() == 1 if self.type_combo else False + ) + is_network_device = ( + self.ssh_mode_combo is not None + and self.ssh_mode_combo.get_selected() == 1 + and is_ssh_session + ) + if is_network_device: + if self.post_login_switch: + self.post_login_switch.set_active(False) + if self.x11_switch: + self.x11_switch.set_active(False) + if self.sftp_switch: + self.sftp_switch.set_active(False) + if self.x11_switch: + self.x11_switch.set_sensitive(is_ssh_session and not is_network_device) + if self.sftp_switch: + self.sftp_switch.set_sensitive(is_ssh_session and not is_network_device) + def _update_sftp_state(self) -> None: if ( not self.sftp_switch @@ -1579,6 +1636,23 @@ def _build_updated_session(self) -> Optional[SessionItem]: if self.x11_switch and session_data["session_type"] == "ssh" else False ) + session_data["ssh_connection_mode"] = ( + "network_device" + if ( + self.ssh_mode_combo + and self.ssh_mode_combo.get_selected() == 1 + and session_data["session_type"] == "ssh" + ) + else "shell" + ) + is_network_device = session_data["ssh_connection_mode"] == "network_device" + if is_network_device: + session_data["post_login_command_enabled"] = False + session_data["post_login_command"] = "" + session_data["sftp_session_enabled"] = False + session_data["sftp_local_directory"] = "" + session_data["sftp_remote_directory"] = "" + session_data["x11_forwarding"] = False raw_password = "" if session_data["session_type"] == "ssh": @@ -1608,6 +1682,7 @@ def _build_updated_session(self) -> Optional[SessionItem]: session_data["sftp_session_enabled"] = False session_data["port_forwardings"] = [] session_data["x11_forwarding"] = False + session_data["ssh_connection_mode"] = "shell" # Set local terminal fields session_data["local_working_directory"] = ( self.local_working_dir_entry.get_text().strip()