Skip to content

Commit f508a5c

Browse files
authored
Support Windows for telnetlib3-client (jquast#137)
Support the windows platform in telnetlib3-client by using 'blessed' (and its dependency, jinxed). Closes #21
1 parent eb0b98d commit f508a5c

13 files changed

Lines changed: 1208 additions & 457 deletions

README.rst

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,36 @@ There are also two fingerprinting CLIs, ``telnetlib3-fingerprint`` and
8383
Encoding
8484
~~~~~~~~
8585

86-
The default encoding is the system locale, usually UTF-8, and, without negotiation of BINARY
87-
transmission, all Telnet protocol text *should* be limited to ASCII text, by strict compliance of
88-
Telnet. Further, the encoding used *should* be negotiated by CHARSET.
86+
The default encoding of telnetlib3-client and server is set by the `locale
87+
<https://man7.org/linux/man-pages/man1/locale.1.html>`_.
8988

90-
When these conditions are true, telnetlib3-server and telnetlib3-client allow connections of any
91-
encoding supporting by the python language, and additionally specially ``ATASCII`` and ``PETSCII``
92-
encodings. Any server capable of negotiating CHARSET or LANG through NEW_ENVIRON is also presumed
93-
to support BINARY.
89+
Without negotiation of BINARY transmission, all Telnet protocol text *should* be limited to ASCII
90+
text, by strict compliance of Telnet. Further, the encoding used *should* be negotiated by CHARSET
91+
:rfc:`2066` or by ``LANG`` using ``NEW_ENVIRON`` :rfc:`1572`. Otherwise, a compliant telnet client
92+
should be limited to ASCII.
9493

95-
From a February 2026 `census of MUDs <https://muds.modem.xyz>`_ and `BBSs servers
94+
When these conditions are true, telnetlib3-server and telnetlib3-client allow *automatic
95+
negotiation* of any encoding in either direction supported by the python language, or any
96+
custom ``ATASCII``, ``PETSCII``, and ``big5bbs`` provided with telnetlib3.
97+
98+
**However**, from a February 2026 `census of MUDs <https://muds.modem.xyz>`_ and `BBSs servers
9699
<https://bbs.modem.xyz>`_:
97100

98-
- 2.8% of MUDs support bi-directional CHARSET
99-
- 0.5% of BBSs support bi-directional CHARSET.
100-
- 18.4% of BBSs support BINARY.
101-
- 3.2% of MUDs support BINARY.
101+
- 2.8% of MUDs and 0.5% of BBSs support bi-directional CHARSET
102+
- 18.4% of BBSs and 3.2% of MUDs support BINARY.
102103

103-
For this reason, it is often required to specify the encoding, eg.!
104+
This means that connecting to *large majority* of BBSs or MUDs that transmit non-ascii, it will
105+
require *manually specifying an encoding*, eg.::
104106

105107
telnetlib3-client --encoding=cp437 20forbeers.com 1337
106108

109+
telnetlib3-client --encoding=big5bbs bbs.ccns.ncku.edu.tw 3456
110+
107111
Raw Mode
108112
~~~~~~~~
109113

110-
Some telnet servers, especially BBS systems or those designed for serial transmission but are
111-
connected to a TCP socket without any telnet negotiation may require "raw" mode argument::
114+
Some telnet servers, especially "retro" BBS systems or those designed for serial transmission but
115+
are connected to a TCP socket without any telnet negotiation may require the "raw" mode argument::
112116

113117
telnetlib3-client --raw-mode area52.tk 5200 --encoding=atascii
114118

docs/api/client_shell_win32.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
client_shell_win32
2+
------------------
3+
4+
.. automodule:: telnetlib3.client_shell_win32
5+
:members:

docs/history.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
History
22
=======
33
4.0.1
4-
* bugfix: ``telnetlib3-client`` could begin a shell in wrong ECHO mode, depending on order of
5-
options in a "connection burst".
64
* new: ``--encoding=big5bbs``, BBS 半形字 (half-width characters) encoding, matching PCMan/PttBBS
75
terminal clients, popular with Taiwanese BBS culture.
6+
* enhancement: ``telnetlib3-client`` now works on Windows by using the optional
7+
``blessed>=1.20`` dependency, installed automatically for Windows platforms.
8+
* bugfix: ``telnetlib3-client`` could begin a shell in wrong ECHO mode, depending on order of
9+
options in a "connection burst".
10+
* bugfix: :class:`~telnetlib3._session_context.TelnetSessionContext` ``gmcp_data``
11+
mutable default argument caused all instances to share a single dict, so GMCP data
12+
from one connection contaminated subsequent connections.
13+
* bugfix: keyboard escape detection raised :exc:`UnicodeDecodeError` on non-UTF-8
14+
terminal input bytes; now uses ``errors="replace"``.
815

916
4.0.0
1017
* removed: ``telnetlib3.color_filter``. ``ColorFilter``, ``ColorConfig``, ``PALETTES``,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ classifiers = [
4545
requires-python = ">=3.9"
4646
dependencies = [
4747
"wcwidth>=0.6.0",
48+
"blessed>=1.33; platform_system == 'Windows'",
4849
]
4950

5051
[project.optional-dependencies]

telnetlib3/_session_context.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,32 @@ class TelnetSessionContext:
2424
line mode.
2525
:param ascii_eol: When ``True``, translate ATASCII CR/LF glyphs to
2626
ASCII ``\r`` / ``\n``.
27+
:param input_filter: Optional :class:`~telnetlib3.client_shell.InputFilter` for
28+
translating raw keyboard bytes (e.g. arrow keys for ATASCII/PETSCII).
29+
:param autoreply_engine: Optional autoreply engine (e.g. a MUD macro engine)
30+
that receives server output via ``engine.feed(text)`` and can send replies.
31+
:param autoreply_wait_fn: Async callable installed by the shell to gate autoreply
32+
sends on GA/EOR prompt signals; set automatically during shell startup.
33+
:param typescript_file: When set, all server output is appended to this file
34+
(like the POSIX ``typescript`` command).
35+
:param gmcp_data: Initial GMCP module data mapping; defaults to an empty dict.
2736
"""
2837

29-
def __init__(self) -> None:
38+
def __init__(
39+
self,
40+
raw_mode: Optional[bool] = None,
41+
ascii_eol: bool = False,
42+
input_filter: Optional[Any] = None,
43+
autoreply_engine: Optional[Any] = None,
44+
autoreply_wait_fn: Optional[Callable[..., Awaitable[None]]] = None,
45+
typescript_file: Optional[IO[str]] = None,
46+
gmcp_data: Optional[dict[str, Any]] = None,
47+
) -> None:
3048
"""Initialize session context with default attribute values."""
31-
self.raw_mode: Optional[bool] = None
32-
self.ascii_eol: bool = False
33-
self.input_filter: Optional[Any] = None
34-
self.autoreply_engine: Optional[Any] = None
35-
self.autoreply_wait_fn: Optional[Callable[..., Awaitable[None]]] = None
36-
self.typescript_file: Optional[IO[str]] = None
37-
self.gmcp_data: dict[str, Any] = {}
49+
self.raw_mode = raw_mode
50+
self.ascii_eol = ascii_eol
51+
self.input_filter = input_filter
52+
self.autoreply_engine = autoreply_engine
53+
self.autoreply_wait_fn = autoreply_wait_fn
54+
self.typescript_file = typescript_file
55+
self.gmcp_data: dict[str, Any] = gmcp_data if gmcp_data is not None else {}

telnetlib3/client.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,11 @@ def _winsize() -> Tuple[int, int]:
482482
rows, cols, _, _ = struct.unpack(fmt, val)
483483
return rows, cols
484484
except (ImportError, IOError):
485-
return (int(os.environ.get("LINES", 25)), int(os.environ.get("COLUMNS", 80)))
485+
try:
486+
sz = os.get_terminal_size()
487+
return sz.lines, sz.columns
488+
except OSError:
489+
return (int(os.environ.get("LINES", 25)), int(os.environ.get("COLUMNS", 80)))
486490

487491

488492
async def open_connection(
@@ -586,7 +590,7 @@ async def open_connection(
586590
"""
587591
if client_factory is None:
588592
client_factory = TelnetClient
589-
if sys.platform != "win32" and sys.stdin.isatty():
593+
if sys.stdin.isatty():
590594
client_factory = TelnetTerminalClient
591595

592596
def connection_factory() -> client_base.BaseClient:
@@ -660,13 +664,14 @@ async def run_client() -> None:
660664

661665
# Wrap client factory to inject always_will/always_do/always_wont/always_dont
662666
# and encoding flags before negotiation starts.
663-
encoding_explicit = args["encoding"] not in ("utf8", "utf-8", False)
667+
environ_encoding = args["encoding"] or "ascii"
668+
encoding_explicit = environ_encoding not in ("utf8", "utf-8", "ascii")
664669
gmcp_modules: Optional[List[str]] = args.get("gmcp_modules")
665670

666671
def _client_factory(**kwargs: Any) -> client_base.BaseClient:
667672
client: TelnetClient
668673
kwargs["gmcp_modules"] = gmcp_modules
669-
if sys.platform != "win32" and sys.stdin.isatty():
674+
if sys.stdin.isatty():
670675
client = TelnetTerminalClient(**kwargs)
671676
else:
672677
client = TelnetClient(**kwargs)
@@ -684,6 +689,7 @@ def _patched_connection_made(transport: asyncio.BaseTransport) -> None:
684689
from .telopt import GMCP as _GMCP
685690

686691
client.writer.passive_do = {_GMCP}
692+
client.writer.environ_encoding = environ_encoding
687693
client.writer._encoding_explicit = encoding_explicit
688694

689695
client.connection_made = _patched_connection_made # type: ignore[method-assign]

0 commit comments

Comments
 (0)