Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions locale/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 20 additions & 0 deletions locale/pt.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
23 changes: 23 additions & 0 deletions src/zashterminal/sessions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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", ""),
Expand Down
17 changes: 16 additions & 1 deletion src/zashterminal/terminal/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
89 changes: 82 additions & 7 deletions src/zashterminal/ui/dialogs/session_edit_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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."""
Expand All @@ -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 = (
Expand All @@ -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
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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()
Expand Down