telnetlib3 3.0.2__tar.gz → 3.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/PKG-INFO +1 -1
  2. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/history.rst +10 -0
  3. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/pyproject.toml +1 -1
  4. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/accessories.py +7 -2
  5. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client.py +26 -2
  6. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client_shell.py +156 -6
  7. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server.py +44 -1
  8. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_shell.py +50 -2
  9. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/slc.py +13 -0
  10. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/stream_writer.py +87 -12
  11. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_client_shell.py +88 -6
  12. telnetlib3-3.0.3/telnetlib3/tests/test_linemode.py +459 -0
  13. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_shell_unit.py +1 -0
  14. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_shell.py +7 -1
  15. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_stream_writer_full.py +8 -0
  16. telnetlib3-3.0.2/telnetlib3/tests/test_linemode.py +0 -106
  17. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/.gitignore +0 -0
  18. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/CONTRIBUTING.rst +0 -0
  19. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/LICENSE.txt +0 -0
  20. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/README.rst +0 -0
  21. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/Makefile +0 -0
  22. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/accessories.rst +0 -0
  23. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client.rst +0 -0
  24. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client_base.rst +0 -0
  25. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client_shell.rst +0 -0
  26. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/color_filter.rst +0 -0
  27. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/fingerprinting.rst +0 -0
  28. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/guard_shells.rst +0 -0
  29. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/mud.rst +0 -0
  30. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server.rst +0 -0
  31. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_base.rst +0 -0
  32. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_pty_shell.rst +0 -0
  33. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_shell.rst +0 -0
  34. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/session_context.rst +0 -0
  35. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/slc.rst +0 -0
  36. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/stream_reader.rst +0 -0
  37. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/stream_writer.rst +0 -0
  38. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/sync.rst +0 -0
  39. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/telnetlib.rst +0 -0
  40. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/telopt.rst +0 -0
  41. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api.rst +0 -0
  42. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/conf.py +0 -0
  43. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/contributing.rst +0 -0
  44. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/guidebook.rst +0 -0
  45. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/index.rst +0 -0
  46. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/intro.rst +0 -0
  47. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/make.bat +0 -0
  48. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/rfcs.rst +0 -0
  49. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/sphinxext/github.py +0 -0
  50. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-analysis.txt +0 -0
  51. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-docs.txt +0 -0
  52. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-tests.txt +0 -0
  53. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements.txt +0 -0
  54. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/.gitignore +0 -0
  55. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/__init__.py +0 -0
  56. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_base.py +0 -0
  57. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_paths.py +0 -0
  58. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_session_context.py +0 -0
  59. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_types.py +0 -0
  60. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client_base.py +0 -0
  61. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/color_filter.py +0 -0
  62. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/__init__.py +0 -0
  63. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/atarist.py +0 -0
  64. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/atascii.py +0 -0
  65. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/petscii.py +0 -0
  66. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/fingerprinting.py +0 -0
  67. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/fingerprinting_display.py +0 -0
  68. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/guard_shells.py +0 -0
  69. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/mud.py +0 -0
  70. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/py.typed +0 -0
  71. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/relay_server.py +0 -0
  72. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_base.py +0 -0
  73. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_fingerprinting.py +0 -0
  74. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_pty_shell.py +0 -0
  75. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/stream_reader.py +0 -0
  76. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/sync.py +0 -0
  77. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/telnetlib.py +0 -0
  78. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/telopt.py +0 -0
  79. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/accessories.py +0 -0
  80. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/conftest.py +0 -0
  81. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/pty_helper.py +0 -0
  82. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_accessories.py +0 -0
  83. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_accessories_extra.py +0 -0
  84. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_atascii_codec.py +0 -0
  85. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_benchmarks.py +0 -0
  86. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_charset.py +0 -0
  87. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_client_unit.py +0 -0
  88. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_color_filter.py +0 -0
  89. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_core.py +0 -0
  90. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_encoding.py +0 -0
  91. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_environ.py +0 -0
  92. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_fingerprinting.py +0 -0
  93. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_guard_integration.py +0 -0
  94. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mccp.py +0 -0
  95. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mud.py +0 -0
  96. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mud_negotiation.py +0 -0
  97. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_naws.py +0 -0
  98. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_petscii_codec.py +0 -0
  99. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_platform.py +0 -0
  100. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_pty_shell.py +0 -0
  101. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_reader.py +0 -0
  102. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_relay_server.py +0 -0
  103. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server.py +0 -0
  104. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_api.py +0 -0
  105. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_cli.py +0 -0
  106. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_fingerprinting.py +0 -0
  107. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_mud.py +0 -0
  108. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_slc.py +0 -0
  109. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_status_logger.py +0 -0
  110. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_stream_reader_extra.py +0 -0
  111. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_sync.py +0 -0
  112. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_telnetlib.py +0 -0
  113. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_timeout.py +0 -0
  114. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_tls.py +0 -0
  115. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_tspeed.py +0 -0
  116. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_ttype.py +0 -0
  117. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_uvloop_integration.py +0 -0
  118. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_writer.py +0 -0
  119. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_xdisploc.py +0 -0
  120. {telnetlib3-3.0.2 → telnetlib3-3.0.3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telnetlib3
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Python Telnet server and client CLI and Protocol library
5
5
  Project-URL: Homepage, https://github.com/jquast/telnetlib3
6
6
  Project-URL: Documentation, https://telnetlib3.readthedocs.io
@@ -1,5 +1,15 @@
1
1
  History
2
2
  =======
3
+ 3.0.3
4
+ * bugfix: server and client now correctly complete LINEMODE negotiation when prompted to.
5
+ * new: ``--logfile-mode {append,rewrite}`` and ``--typescript-mode`` CLI flags
6
+ and :func:`~telnetlib3.accessories.make_logger` ``filemode`` argument control whether the log
7
+ file is appended to (default) or overwritten on each connection.
8
+ * new: :class:`~telnetlib3.client_shell.LinemodeBuffer` used by ``telnetlib3-client``, a
9
+ client-side line buffer for LINEMODE EDIT mode with local erase-char, erase-line, erase-word
10
+ editing, forwardmask flushing, and TRAPSIG IAC command generation. The default 'telsh' server
11
+ was also updated to support linemode.
12
+
3
13
  3.0.2
4
14
  * bugfix: :meth:`~telnetlib3.stream_writer.TelnetWriter.request_charset` raised :exc:`TypeError`,
5
15
  :ghissue:`128`. Offer callbacks (no-arg, returning a list of items to propose) are now
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "telnetlib3"
7
- version = "3.0.2" # Keep in sync with telnetlib3/accessories.py::get_version !
7
+ version = "3.0.3" # Keep in sync with telnetlib3/accessories.py::get_version !
8
8
  description = " Python Telnet server and client CLI and Protocol library"
9
9
  readme = "README.rst"
10
10
  license = "ISC"
@@ -42,7 +42,7 @@ PATIENCE_MESSAGES = [
42
42
 
43
43
  def get_version() -> str:
44
44
  """Return the current version of telnetlib3."""
45
- return "3.0.2" # keep in sync with pyproject.toml !
45
+ return "3.0.3" # keep in sync with pyproject.toml !
46
46
 
47
47
 
48
48
  def encoding_from_lang(lang: str) -> Optional[str]:
@@ -130,7 +130,11 @@ _DEFAULT_LOGFMT = " ".join(
130
130
 
131
131
 
132
132
  def make_logger(
133
- name: str, loglevel: str = "info", logfile: Optional[str] = None, logfmt: str = _DEFAULT_LOGFMT
133
+ name: str,
134
+ loglevel: str = "info",
135
+ logfile: Optional[str] = None,
136
+ logfmt: str = _DEFAULT_LOGFMT,
137
+ filemode: str = "a",
134
138
  ) -> logging.Logger:
135
139
  """Create and return simple logger for given arguments."""
136
140
  lvl = getattr(logging, loglevel.upper(), None)
@@ -140,6 +144,7 @@ def make_logger(
140
144
  _cfg: Dict[str, Any] = {"format": logfmt}
141
145
  if logfile:
142
146
  _cfg["filename"] = logfile
147
+ _cfg["filemode"] = filemode
143
148
  logging.basicConfig(**_cfg)
144
149
  for handler in logging.getLogger().handlers:
145
150
  if isinstance(handler, logging.StreamHandler) and not isinstance(
@@ -645,7 +645,11 @@ async def run_client() -> None:
645
645
  config_msg = f"Client configuration: {accessories.repr_mapping(args)}"
646
646
 
647
647
  log = accessories.make_logger(
648
- name=__name__, loglevel=args["loglevel"], logfile=args["logfile"], logfmt=args["logfmt"]
648
+ name=__name__,
649
+ loglevel=args["loglevel"],
650
+ logfile=args["logfile"],
651
+ logfmt=args["logfmt"],
652
+ filemode="w" if args.get("logfile_mode") == "rewrite" else "a",
649
653
  )
650
654
  log.debug(config_msg)
651
655
 
@@ -774,7 +778,11 @@ async def run_client() -> None:
774
778
  ) -> None:
775
779
  ctx = writer_arg.ctx
776
780
  assert typescript_path is not None
777
- ts_file = open(typescript_path, "a", encoding="utf-8") # noqa: SIM115
781
+ ts_file = open( # noqa: SIM115
782
+ typescript_path,
783
+ "w" if args.get("typescript_mode") == "rewrite" else "a",
784
+ encoding="utf-8",
785
+ )
778
786
  ctx.typescript_file = ts_file
779
787
  try:
780
788
  await _ts_inner(reader, writer_arg)
@@ -819,6 +827,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
819
827
  parser.add_argument("--loglevel", default="warn", help="log level")
820
828
  parser.add_argument("--logfmt", default=accessories._DEFAULT_LOGFMT, help="log format")
821
829
  parser.add_argument("--logfile", help="filepath")
830
+ parser.add_argument(
831
+ "--logfile-mode",
832
+ default="append",
833
+ choices=["append", "rewrite"],
834
+ dest="logfile_mode",
835
+ help="Log file write mode: append (default) or rewrite.",
836
+ )
822
837
  parser.add_argument(
823
838
  "--shell", default="telnetlib3.telnet_client_shell", help="module.function_name"
824
839
  )
@@ -972,6 +987,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
972
987
  metavar="FILE",
973
988
  help="record session to FILE (like Unix script(1))",
974
989
  )
990
+ parser.add_argument(
991
+ "--typescript-mode",
992
+ default="append",
993
+ choices=["append", "rewrite"],
994
+ dest="typescript_mode",
995
+ help="Typescript write mode: append (default) or rewrite.",
996
+ )
975
997
  return parser
976
998
 
977
999
 
@@ -1040,6 +1062,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
1040
1062
  "port": args.port,
1041
1063
  "loglevel": args.loglevel,
1042
1064
  "logfile": args.logfile,
1065
+ "logfile_mode": args.logfile_mode,
1043
1066
  "logfmt": args.logfmt,
1044
1067
  "encoding": args.encoding,
1045
1068
  "tspeed": (args.speed, args.speed),
@@ -1068,6 +1091,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
1068
1091
  ),
1069
1092
  "compression": args.compression,
1070
1093
  "typescript": args.typescript,
1094
+ "typescript_mode": args.typescript_mode,
1071
1095
  }
1072
1096
 
1073
1097
 
@@ -10,12 +10,14 @@ from typing import Any, Dict, Tuple, Union, Callable, Optional
10
10
  from dataclasses import dataclass
11
11
 
12
12
  # local
13
+ from . import slc as slc_module
13
14
  from . import accessories
14
15
  from ._session_context import TelnetSessionContext
15
16
 
16
17
  log = logging.getLogger(__name__)
17
18
 
18
19
  # local
20
+ from .telopt import LINEMODE # noqa: E402
19
21
  from .accessories import TRACE # noqa: E402
20
22
  from .stream_reader import TelnetReader, TelnetReaderUnicode # noqa: E402
21
23
  from .stream_writer import TelnetWriter, TelnetWriterUnicode # noqa: E402
@@ -180,6 +182,93 @@ class _RawLoopState:
180
182
  reactivate_repl: bool = False
181
183
 
182
184
 
185
+ class LinemodeBuffer:
186
+ """
187
+ Client-side line buffer for LINEMODE EDIT mode (RFC 1184 §3.1).
188
+
189
+ Accumulates characters typed by the user, applying local SLC editing functions (erase-char,
190
+ erase-line, erase-word) and transmitting complete lines to the server. When TRAPSIG is enabled,
191
+ signal characters (^C etc.) are sent as IAC commands instead of buffered.
192
+
193
+ :param slctab: The writer's current SLC character table.
194
+ :param forwardmask: FORWARDMASK received from server, or None.
195
+ :param trapsig: When True, signal characters are sent as IAC commands.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ slctab: Dict[bytes, slc_module.SLC],
201
+ forwardmask: Optional[slc_module.Forwardmask] = None,
202
+ trapsig: bool = False,
203
+ ) -> None:
204
+ """Initialize LinemodeBuffer."""
205
+ from .telopt import IP, AYT, BRK, EOF, IAC, SUSP, ABORT
206
+
207
+ self._buf: list[str] = []
208
+ self.slctab = slctab
209
+ self.forwardmask = forwardmask
210
+ self.trapsig = trapsig
211
+ self._trapsig_map: Dict[bytes, bytes] = {
212
+ slc_module.SLC_IP: IAC + IP,
213
+ slc_module.SLC_ABORT: IAC + ABORT,
214
+ slc_module.SLC_SUSP: IAC + SUSP,
215
+ slc_module.SLC_EOF: IAC + EOF,
216
+ slc_module.SLC_BRK: IAC + BRK,
217
+ slc_module.SLC_AYT: IAC + AYT,
218
+ }
219
+
220
+ def _slc_val(self, func: bytes) -> Optional[int]:
221
+ """Return the active byte value for SLC function, or None if unsupported."""
222
+ defn = self.slctab.get(func)
223
+ if defn is None or defn.nosupport:
224
+ return None
225
+ v = defn.val
226
+ return ord(v) if v and v != slc_module.theNULL else None
227
+
228
+ def feed(self, char: str) -> Tuple[str, Optional[bytes]]:
229
+ """
230
+ Feed one character into the buffer.
231
+
232
+ :returns: ``(echo, data)`` where ``echo`` is text to display locally
233
+ (may be empty) and ``data`` is bytes to send to server, or None
234
+ if buffering.
235
+ """
236
+ b = ord(char)
237
+ if self.trapsig:
238
+ for func, cmd in self._trapsig_map.items():
239
+ if b == self._slc_val(func):
240
+ return ("", cmd)
241
+ if b == self._slc_val(slc_module.SLC_EC):
242
+ if self._buf:
243
+ self._buf.pop()
244
+ return ("\b \b", None)
245
+ return ("", None)
246
+ if b == self._slc_val(slc_module.SLC_EL):
247
+ n = len(self._buf)
248
+ self._buf.clear()
249
+ return ("\b \b" * n, None)
250
+ if b == self._slc_val(slc_module.SLC_EW):
251
+ popped = 0
252
+ # skip trailing spaces (POSIX VWERASE behaviour)
253
+ while self._buf and self._buf[-1] == " ":
254
+ self._buf.pop()
255
+ popped += 1
256
+ while self._buf and self._buf[-1] != " ":
257
+ self._buf.pop()
258
+ popped += 1
259
+ return ("\b \b" * popped, None)
260
+ if char in ("\r", "\n"):
261
+ line = "".join(self._buf) + char
262
+ self._buf.clear()
263
+ return (char, line.encode())
264
+ if self.forwardmask is not None and b in self.forwardmask:
265
+ data = ("".join(self._buf) + char).encode()
266
+ self._buf.clear()
267
+ return (char, data)
268
+ self._buf.append(char)
269
+ return (char, None)
270
+
271
+
183
272
  if sys.platform == "win32":
184
273
 
185
274
  async def telnet_client_shell(
@@ -310,10 +399,11 @@ else:
310
399
  )
311
400
 
312
401
  def _server_will_sga(self) -> bool:
313
- """Whether server has negotiated WILL SGA."""
402
+ """Whether SGA has been negotiated (either direction)."""
314
403
  from .telopt import SGA
315
404
 
316
- return bool(self.telnet_writer.client and self.telnet_writer.remote_option.enabled(SGA))
405
+ w = self.telnet_writer
406
+ return bool(w.client and (w.remote_option.enabled(SGA) or w.local_option.enabled(SGA)))
317
407
 
318
408
  def check_auto_mode(
319
409
  self, switched_to_raw: bool, last_will_echo: bool
@@ -330,6 +420,20 @@ else:
330
420
  return None
331
421
  wecho = self.telnet_writer.will_echo
332
422
  wsga = self._server_will_sga()
423
+ # LINEMODE EDIT: kernel must handle line editing; keep/restore cooked mode.
424
+ # This takes priority over the SGA/ECHO raw-mode heuristics below.
425
+ if (
426
+ self.telnet_writer.local_option.enabled(LINEMODE)
427
+ and self.telnet_writer.linemode.edit
428
+ ):
429
+ if switched_to_raw:
430
+ assert self._save_mode is not None
431
+ self.set_mode(self._save_mode)
432
+ self.telnet_writer.log.debug(
433
+ "auto: LINEMODE EDIT confirmed, restoring cooked mode"
434
+ )
435
+ return (False, wecho, False)
436
+ return None
333
437
  # WILL ECHO alone = line mode with server echo (suppress local echo)
334
438
  # WILL SGA (with or without ECHO) = raw/character-at-a-time
335
439
  should_go_raw = not switched_to_raw and wsga
@@ -362,20 +466,35 @@ else:
362
466
 
363
467
  Auto mode (``_raw_mode is None``): follows the server's negotiation.
364
468
 
365
- ================= ======== ========== ================================
469
+ ================= ======== ========== ========================================
366
470
  Server negotiates ICANON ECHO Behavior
367
- ================= ======== ========== ================================
471
+ ================= ======== ========== ========================================
368
472
  Nothing on on Line mode, local echo
473
+ LINEMODE EDIT **on** on Cooked mode, kernel handles EC/EL/echo
474
+ LINEMODE remote **off** **off** Raw, server echoes
369
475
  WILL SGA only **off** on Character-at-a-time, local echo
370
476
  WILL ECHO only on **off** Line mode, server echoes
371
477
  WILL SGA + ECHO **off** **off** Full kludge mode (most common)
372
- ================= ======== ========== ================================
478
+ ================= ======== ========== ========================================
373
479
  """
374
480
  raw_mode = _get_raw_mode(self.telnet_writer)
375
481
  will_echo = self.telnet_writer.will_echo
376
482
  will_sga = self._server_will_sga()
377
483
  # Auto mode (None): follow server negotiation
378
484
  if raw_mode is None:
485
+ if self.telnet_writer.local_option.enabled(LINEMODE):
486
+ linemode_mode = self.telnet_writer.linemode
487
+ if linemode_mode.edit:
488
+ # RFC 1184 / NetBSD reference: LINEMODE EDIT means ICANON on.
489
+ # The kernel line discipline handles EC (VERASE), EL (VKILL),
490
+ # EW (VWERASE), and echo. No software line editing needed.
491
+ self.telnet_writer.log.debug(
492
+ "auto: LINEMODE EDIT, cooked mode (kernel line editing)"
493
+ )
494
+ self.software_echo = False
495
+ return mode # keep ICANON on; kernel handles EC/EL/EW and echo
496
+ self.telnet_writer.log.debug("auto: LINEMODE remote, raw input server echo")
497
+ return self._make_raw(mode, suppress_echo=True)
379
498
  if will_echo and will_sga:
380
499
  self.telnet_writer.log.debug("auto: server echo + SGA, kludge mode")
381
500
  return self._make_raw(mode)
@@ -531,6 +650,18 @@ else:
531
650
  """Return the autoreply engine from the writer's context, if set."""
532
651
  return telnet_writer.ctx.autoreply_engine
533
652
 
653
+ def _get_linemode_buffer(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> "LinemodeBuffer":
654
+ """Return (or lazily create) the LinemodeBuffer attached to *writer*."""
655
+ buf: Optional[LinemodeBuffer] = getattr(writer, "_linemode_buf", None)
656
+ if buf is None:
657
+ buf = LinemodeBuffer(
658
+ slctab=writer.slctab,
659
+ forwardmask=writer.forwardmask,
660
+ trapsig=writer.linemode.trapsig,
661
+ )
662
+ writer._linemode_buf = buf
663
+ return buf
664
+
534
665
  async def _raw_event_loop(
535
666
  telnet_reader: Union[TelnetReader, TelnetReaderUnicode],
536
667
  telnet_writer: Union[TelnetWriter, TelnetWriterUnicode],
@@ -589,7 +720,26 @@ else:
589
720
  wait_for.remove(telnet_task)
590
721
  handle_close("Connection closed.")
591
722
  break
592
- new_timer, has_pending = _send_stdin(inp, telnet_writer, stdout, state.local_echo)
723
+ linemode_edit = (
724
+ telnet_writer.local_option.enabled(LINEMODE) and telnet_writer.linemode.edit
725
+ )
726
+ if linemode_edit and state.switched_to_raw:
727
+ # Raw PTY or non-TTY: kernel not doing line editing, use LinemodeBuffer
728
+ lmbuf = _get_linemode_buffer(telnet_writer)
729
+ for ch in inp.decode(errors="replace"):
730
+ echo, data = lmbuf.feed(ch)
731
+ if echo:
732
+ stdout.write(echo.encode())
733
+ if data:
734
+ telnet_writer._write(data)
735
+ new_timer, has_pending = None, False
736
+ elif linemode_edit:
737
+ # Cooked PTY: kernel already handled EC/EL/echo; forward line directly
738
+ new_timer, has_pending = _send_stdin(inp, telnet_writer, stdout, False)
739
+ else:
740
+ new_timer, has_pending = _send_stdin(
741
+ inp, telnet_writer, stdout, state.local_echo
742
+ )
593
743
  if has_pending and esc_timer_task not in wait_for:
594
744
  esc_timer_task = new_timer
595
745
  if esc_timer_task is not None:
@@ -42,7 +42,14 @@ try:
42
42
  except ImportError:
43
43
  PTY_SUPPORT = False
44
44
 
45
- __all__ = ("TelnetServer", "Server", "create_server", "run_server", "parse_server_args")
45
+ __all__ = (
46
+ "TelnetServer",
47
+ "LinemodeServer",
48
+ "Server",
49
+ "create_server",
50
+ "run_server",
51
+ "parse_server_args",
52
+ )
46
53
 
47
54
 
48
55
  class CONFIG(NamedTuple):
@@ -806,6 +813,42 @@ class _TLSAutoDetectProtocol(asyncio.Protocol):
806
813
  _ = exc
807
814
 
808
815
 
816
+ class LinemodeServer(TelnetServer):
817
+ """
818
+ :class:`TelnetServer` subclass that negotiates LINEMODE EDIT.
819
+
820
+ In addition to the standard options negotiated by :class:`TelnetServer`,
821
+ this server sends ``DO LINEMODE`` during advanced negotiation, proposes
822
+ LINEMODE EDIT (local line editing by the client), and suppresses
823
+ ``WILL ECHO`` so the client performs local echoing via its LINEMODE buffer.
824
+
825
+ Use with :func:`create_server` to enable RFC 1184 LINEMODE EDIT on a
826
+ :func:`~.telnet_server_shell` session or any custom shell.
827
+ """
828
+
829
+ from . import slc as _slc_module
830
+
831
+ #: Propose LINEMODE EDIT (local line editing) instead of remote mode.
832
+ default_linemode = _slc_module.Linemode(_slc_module.LMODE_MODE_LOCAL)
833
+
834
+ def begin_advanced_negotiation(self) -> None:
835
+ """Negotiate standard options plus ``DO LINEMODE``."""
836
+ from .telopt import DO, LINEMODE
837
+
838
+ super().begin_advanced_negotiation()
839
+ # Propagate the protocol-level default_linemode to the writer so that
840
+ # TelnetWriter.handle_will(LINEMODE) proposes the correct mode (LOCAL/EDIT)
841
+ # rather than the TelnetWriter class default (REMOTE).
842
+ self.writer.default_linemode = self.default_linemode
843
+ self.writer.iac(DO, LINEMODE)
844
+
845
+ def _negotiate_echo(self) -> None:
846
+ """Skip ``WILL ECHO`` — LINEMODE EDIT client handles local echo."""
847
+ if self._echo_negotiated:
848
+ return
849
+ self._echo_negotiated = True
850
+
851
+
809
852
  class Server:
810
853
  """
811
854
  Telnet server that tracks connected clients.
@@ -137,7 +137,14 @@ class _LineEditor:
137
137
  return char, None
138
138
 
139
139
 
140
- __all__ = ("telnet_server_shell", "readline_async", "readline")
140
+ __all__ = (
141
+ "telnet_server_shell",
142
+ "readline_async",
143
+ "readline",
144
+ "get_linemode",
145
+ "get_slcdata",
146
+ "do_toggle",
147
+ )
141
148
 
142
149
 
143
150
  async def telnet_server_shell(
@@ -179,7 +186,7 @@ async def telnet_server_shell(
179
186
  writer.write("Goodbye." + CR + LF)
180
187
  break
181
188
  if command == "help":
182
- writer.write("quit, writer, slc, toggle [option|all], reader, proto, dump")
189
+ writer.write("quit, writer, slc, linemode, toggle [option|all], reader, proto, dump")
183
190
  elif command == "writer":
184
191
  # show 'writer' status
185
192
  writer.write(repr(writer))
@@ -194,6 +201,8 @@ async def telnet_server_shell(
194
201
  elif command == "slc":
195
202
  # show 'slc' support and data tables
196
203
  writer.write(get_slcdata(writer))
204
+ elif command == "linemode":
205
+ writer.write(get_linemode(writer))
197
206
  elif command.startswith("toggle"):
198
207
  # toggle specified options
199
208
  option = command[len("toggle ") :] or None
@@ -342,8 +351,25 @@ def get_slcdata(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> str:
342
351
  )
343
352
 
344
353
 
354
+ def get_linemode(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> str:
355
+ """Display current LINEMODE negotiation state."""
356
+ active = writer.remote_option.enabled(telopt.LINEMODE)
357
+ if not active:
358
+ return "LINEMODE not negotiated."
359
+ lm = writer.linemode
360
+ bits = (
361
+ f"EDIT={'on' if lm.edit else 'off'}"
362
+ f" TRAPSIG={'on' if lm.trapsig else 'off'}"
363
+ f" SOFT_TAB={'on' if lm.soft_tab else 'off'}"
364
+ f" LIT_ECHO={'on' if lm.lit_echo else 'off'}"
365
+ f" ACK={'on' if lm.ack else 'off'}"
366
+ )
367
+ return f"LINEMODE active. Mode: {writer.mode}\r\n{bits}"
368
+
369
+
345
370
  def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional[str]) -> str:
346
371
  """Display or toggle telnet session parameters."""
372
+ linemode_active = writer.remote_option.enabled(telopt.LINEMODE)
347
373
  tbl_opt = {
348
374
  "echo": writer.local_option.enabled(telopt.ECHO),
349
375
  "goahead": not writer.local_option.enabled(telopt.SGA),
@@ -352,6 +378,9 @@ def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional
352
378
  "binary": writer.outbinary and writer.inbinary,
353
379
  "xon-any": writer.xon_any,
354
380
  "lflow": writer.lflow,
381
+ "linemode": linemode_active,
382
+ "linemode-edit": writer.linemode.edit if linemode_active else False,
383
+ "linemode-trapsig": writer.linemode.trapsig if linemode_active else False,
355
384
  }
356
385
 
357
386
  if not option:
@@ -390,6 +419,25 @@ def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional
390
419
  writer.send_lineflow_mode()
391
420
  msgs.append(f"lineflow {'en' if writer.lflow else 'dis'}abled.")
392
421
 
422
+ if option in ("linemode",):
423
+ cmd = telopt.DONT if tbl_opt["linemode"] else telopt.DO
424
+ writer.iac(cmd, telopt.LINEMODE)
425
+ msgs.append(f"{telopt.name_command(cmd).lower()} linemode.")
426
+
427
+ if option in ("linemode-edit",):
428
+ if not tbl_opt["linemode"]:
429
+ msgs.append("linemode not active.")
430
+ else:
431
+ writer.request_linemode_change(edit=not tbl_opt["linemode-edit"])
432
+ msgs.append(f"linemode-edit {'dis' if tbl_opt['linemode-edit'] else 'en'}abled.")
433
+
434
+ if option in ("linemode-trapsig",):
435
+ if not tbl_opt["linemode"]:
436
+ msgs.append("linemode not active.")
437
+ else:
438
+ writer.request_linemode_change(trapsig=not tbl_opt["linemode-trapsig"])
439
+ msgs.append(f"linemode-trapsig {'dis' if tbl_opt['linemode-trapsig'] else 'en'}abled.")
440
+
393
441
  if option not in tbl_opt and option != "all":
394
442
  msgs.append("toggle: not an option.")
395
443
 
@@ -17,6 +17,7 @@ __all__ = (
17
17
  "Linemode",
18
18
  "LMODE_FORWARDMASK",
19
19
  "LMODE_MODE",
20
+ "LMODE_MODE_EDIT",
20
21
  "LMODE_MODE_REMOTE",
21
22
  "LMODE_SLC",
22
23
  "name_slc_command",
@@ -89,6 +90,9 @@ LMODE_MODE, LMODE_FORWARDMASK, LMODE_SLC = (bytes([const]) for const in range(1,
89
90
  LMODE_MODE_REMOTE, LMODE_MODE_LOCAL, LMODE_MODE_TRAPSIG = (bytes([const]) for const in range(3))
90
91
  LMODE_MODE_ACK, LMODE_MODE_SOFT_TAB, LMODE_MODE_LIT_ECHO = (bytes([4]), bytes([8]), bytes([16]))
91
92
 
93
+ #: RFC 1184's name for LMODE_MODE_LOCAL (EDIT bit)
94
+ LMODE_MODE_EDIT = LMODE_MODE_LOCAL
95
+
92
96
 
93
97
  class SLC:
94
98
  """Defines the willingness to support a Special Linemode Character."""
@@ -315,6 +319,15 @@ class Linemode:
315
319
  """True if linemode is local."""
316
320
  return bool(ord(self.mask) & ord(LMODE_MODE_LOCAL))
317
321
 
322
+ @property
323
+ def edit(self) -> bool:
324
+ """
325
+ True if EDIT bit set (client performs local line editing).
326
+
327
+ RFC 1184 name.
328
+ """
329
+ return self.local
330
+
318
331
  @property
319
332
  def remote(self) -> bool:
320
333
  """True if linemode is remote."""