mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
fix(webcrt): restore arrow keys under application cursor mode
Normalize SS3 arrows to CSI and stop remapping to Ctrl-B so left/right actually move the device cursor after login. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6d4cd741ef
commit
b690914f18
2 changed files with 39 additions and 12 deletions
|
|
@ -37,8 +37,13 @@ _reaper_started = False
|
||||||
# Sentinel for take_stdout timeout (distinct from device EOF None).
|
# Sentinel for take_stdout timeout (distinct from device EOF None).
|
||||||
_STDOUT_MISSING = object()
|
_STDOUT_MISSING = object()
|
||||||
|
|
||||||
# Network device CLIs often reject xterm CSI arrows over Telnet/SSH.
|
# Network device CLI key rewrites (SecureCRT-like).
|
||||||
# Backspace: map DEL(0x7f) -> BS(0x08), matching common SecureCRT/VT default.
|
# - Backspace: DEL(0x7f) -> BS(0x08)
|
||||||
|
# - Home/End/Delete: emacs controls (widely accepted)
|
||||||
|
# - Arrows: after login many boxes enable DECCKM (application cursor), so xterm
|
||||||
|
# sends SS3 forms (\x1bOD) which VRP/IOS ignore; normalize SS3 -> CSI and
|
||||||
|
# pass CSI through. Do not rewrite arrows to Ctrl-B/F — that leaves the
|
||||||
|
# device cursor stuck at EOL when SS3 was what actually arrived.
|
||||||
_NETWORK_CLI_KEY_SEQS: tuple[tuple[str, str], ...] = (
|
_NETWORK_CLI_KEY_SEQS: tuple[tuple[str, str], ...] = (
|
||||||
("\x1b[1~", "\x01"), # Home -> Ctrl-A
|
("\x1b[1~", "\x01"), # Home -> Ctrl-A
|
||||||
("\x1b[3~", "\x04"), # Delete key -> Ctrl-D
|
("\x1b[3~", "\x04"), # Delete key -> Ctrl-D
|
||||||
|
|
@ -47,10 +52,10 @@ _NETWORK_CLI_KEY_SEQS: tuple[tuple[str, str], ...] = (
|
||||||
("\x1b[F", "\x05"),
|
("\x1b[F", "\x05"),
|
||||||
("\x1bOH", "\x01"),
|
("\x1bOH", "\x01"),
|
||||||
("\x1bOF", "\x05"),
|
("\x1bOF", "\x05"),
|
||||||
("\x1b[A", "\x10"), # Up -> Ctrl-P (history)
|
("\x1bOA", "\x1b[A"), # App Up -> CSI Up
|
||||||
("\x1b[B", "\x0e"), # Down -> Ctrl-N
|
("\x1bOB", "\x1b[B"),
|
||||||
("\x1b[C", "\x06"), # Right -> Ctrl-F
|
("\x1bOC", "\x1b[C"),
|
||||||
("\x1b[D", "\x02"), # Left -> Ctrl-B
|
("\x1bOD", "\x1b[D"), # App Left -> CSI Left
|
||||||
("\x7f", "\x08"), # DEL -> BS
|
("\x7f", "\x08"), # DEL -> BS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -65,9 +70,15 @@ def uses_network_cli_keymap(device_type: str = "", vendor: str = "") -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def map_network_cli_keys(data: str, *, device_type: str = "", vendor: str = "") -> str:
|
def map_network_cli_keys(
|
||||||
|
data: str,
|
||||||
|
*,
|
||||||
|
device_type: str = "",
|
||||||
|
vendor: str = "",
|
||||||
|
protocol: str = "",
|
||||||
|
) -> str:
|
||||||
"""Rewrite xterm key sequences for network-device CLIs."""
|
"""Rewrite xterm key sequences for network-device CLIs."""
|
||||||
del device_type, vendor # kept for call-site compatibility; keymap is vendor-agnostic
|
del device_type, vendor, protocol # protocol kept for call-site compatibility
|
||||||
text = str(data or "")
|
text = str(data or "")
|
||||||
if not text:
|
if not text:
|
||||||
return text
|
return text
|
||||||
|
|
@ -294,7 +305,10 @@ class WebcrtSession:
|
||||||
return
|
return
|
||||||
if self.cli_keymap:
|
if self.cli_keymap:
|
||||||
text = map_network_cli_keys(
|
text = map_network_cli_keys(
|
||||||
text, device_type=self.device_type, vendor=self.vendor
|
text,
|
||||||
|
device_type=self.device_type,
|
||||||
|
vendor=self.vendor,
|
||||||
|
protocol=self.protocol,
|
||||||
)
|
)
|
||||||
text = map_network_cli_enter(text, self.conn)
|
text = map_network_cli_enter(text, self.conn)
|
||||||
if not text:
|
if not text:
|
||||||
|
|
@ -307,7 +321,14 @@ class WebcrtSession:
|
||||||
try:
|
try:
|
||||||
if channel is not None and hasattr(channel, "send") and callable(channel.send):
|
if channel is not None and hasattr(channel, "send") and callable(channel.send):
|
||||||
payload = text.encode(getattr(self.conn, "encoding", None) or "utf-8", errors="replace")
|
payload = text.encode(getattr(self.conn, "encoding", None) or "utf-8", errors="replace")
|
||||||
channel.send(payload)
|
# Paramiko may write partially when the window is full.
|
||||||
|
view = memoryview(payload)
|
||||||
|
while len(view):
|
||||||
|
n = int(channel.send(view) or 0)
|
||||||
|
if n <= 0:
|
||||||
|
time.sleep(0.01)
|
||||||
|
continue
|
||||||
|
view = view[n:]
|
||||||
elif channel is not None and hasattr(channel, "write") and callable(channel.write):
|
elif channel is not None and hasattr(channel, "write") and callable(channel.write):
|
||||||
encoding = getattr(self.conn, "encoding", None) or "utf-8"
|
encoding = getattr(self.conn, "encoding", None) or "utf-8"
|
||||||
channel.write(text.encode(encoding, errors="replace") if isinstance(text, str) else text)
|
channel.write(text.encode(encoding, errors="replace") if isinstance(text, str) else text)
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,11 @@ class WebcrtServiceTests(unittest.TestCase):
|
||||||
# Backspace DEL -> BS
|
# Backspace DEL -> BS
|
||||||
sess.write_stdin("\x7f")
|
sess.write_stdin("\x7f")
|
||||||
self.assertEqual(conn.written[-1], "\x08")
|
self.assertEqual(conn.written[-1], "\x08")
|
||||||
|
# CSI arrows pass through; application-cursor SS3 -> CSI.
|
||||||
sess.write_stdin("\x1b[D\x1b[C\x1b[A\x1b[B")
|
sess.write_stdin("\x1b[D\x1b[C\x1b[A\x1b[B")
|
||||||
self.assertEqual(conn.written[-1], "\x02\x06\x10\x0e")
|
self.assertEqual(conn.written[-1], "\x1b[D\x1b[C\x1b[A\x1b[B")
|
||||||
|
sess.write_stdin("\x1bOD")
|
||||||
|
self.assertEqual(conn.written[-1], "\x1b[D")
|
||||||
sess.resize(120, 40)
|
sess.resize(120, 40)
|
||||||
conn.remote_conn.resize_pty.assert_called_with(width=120, height=40)
|
conn.remote_conn.resize_pty.assert_called_with(width=120, height=40)
|
||||||
sess.close("test")
|
sess.close("test")
|
||||||
|
|
@ -73,7 +76,10 @@ class WebcrtServiceTests(unittest.TestCase):
|
||||||
self.assertEqual(svc.map_network_cli_keys("\x7fab"), "\x08ab")
|
self.assertEqual(svc.map_network_cli_keys("\x7fab"), "\x08ab")
|
||||||
self.assertEqual(svc.map_network_cli_keys("\x7fab", device_type="cisco_ios", vendor="Cisco"), "\x08ab")
|
self.assertEqual(svc.map_network_cli_keys("\x7fab", device_type="cisco_ios", vendor="Cisco"), "\x08ab")
|
||||||
self.assertEqual(svc.map_network_cli_keys("\x7fab", device_type="huawei", vendor="Huawei"), "\x08ab")
|
self.assertEqual(svc.map_network_cli_keys("\x7fab", device_type="huawei", vendor="Huawei"), "\x08ab")
|
||||||
self.assertEqual(svc.map_network_cli_keys("\x1b[D"), "\x02")
|
# Keep CSI left; normalize SS3 application-cursor left.
|
||||||
|
self.assertEqual(svc.map_network_cli_keys("\x1b[D"), "\x1b[D")
|
||||||
|
self.assertEqual(svc.map_network_cli_keys("\x1bOD"), "\x1b[D")
|
||||||
|
self.assertEqual(svc.map_network_cli_keys("\x1bOA\x1bOB\x1bOC\x1bOD"), "\x1b[A\x1b[B\x1b[C\x1b[D")
|
||||||
self.assertTrue(svc.uses_network_cli_keymap("huawei", "Huawei"))
|
self.assertTrue(svc.uses_network_cli_keymap("huawei", "Huawei"))
|
||||||
self.assertFalse(svc.uses_network_cli_keymap("linux", "bastion"))
|
self.assertFalse(svc.uses_network_cli_keymap("linux", "bastion"))
|
||||||
self.assertEqual(svc.map_network_cli_enter("\r", _FakeConn()), "\n") # type: ignore[arg-type]
|
self.assertEqual(svc.map_network_cli_enter("\r", _FakeConn()), "\n") # type: ignore[arg-type]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue