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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true

Expand Down
34 changes: 19 additions & 15 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,36 @@ There are also two fingerprinting CLIs, ``telnetlib3-fingerprint`` and
Encoding
~~~~~~~~

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

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

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

**However**, from a February 2026 `census of MUDs <https://muds.modem.xyz>`_ and `BBSs servers
<https://bbs.modem.xyz>`_:

- 2.8% of MUDs support bi-directional CHARSET
- 0.5% of BBSs support bi-directional CHARSET.
- 18.4% of BBSs support BINARY.
- 3.2% of MUDs support BINARY.
- 2.8% of MUDs and 0.5% of BBSs support bi-directional CHARSET
- 18.4% of BBSs and 3.2% of MUDs support BINARY.

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

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

telnetlib3-client --encoding=big5bbs bbs.ccns.ncku.edu.tw 3456

Raw Mode
~~~~~~~~

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

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

Expand Down
5 changes: 5 additions & 0 deletions docs/api/client_shell_win32.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
client_shell_win32
------------------

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

4.0.0
* removed: ``telnetlib3.color_filter``. ``ColorFilter``, ``ColorConfig``, ``PALETTES``,
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"wcwidth>=0.6.0",
"blessed>=1.33; platform_system == 'Windows'",
]

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

def __init__(self) -> None:
def __init__(
self,
raw_mode: Optional[bool] = None,
ascii_eol: bool = False,
input_filter: Optional[Any] = None,
autoreply_engine: Optional[Any] = None,
autoreply_wait_fn: Optional[Callable[..., Awaitable[None]]] = None,
typescript_file: Optional[IO[str]] = None,
gmcp_data: Optional[dict[str, Any]] = None,
) -> None:
"""Initialize session context with default attribute values."""
self.raw_mode: Optional[bool] = None
self.ascii_eol: bool = False
self.input_filter: Optional[Any] = None
self.autoreply_engine: Optional[Any] = None
self.autoreply_wait_fn: Optional[Callable[..., Awaitable[None]]] = None
self.typescript_file: Optional[IO[str]] = None
self.gmcp_data: dict[str, Any] = {}
self.raw_mode = raw_mode
self.ascii_eol = ascii_eol
self.input_filter = input_filter
self.autoreply_engine = autoreply_engine
self.autoreply_wait_fn = autoreply_wait_fn
self.typescript_file = typescript_file
self.gmcp_data: dict[str, Any] = gmcp_data if gmcp_data is not None else {}
14 changes: 10 additions & 4 deletions telnetlib3/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,11 @@ def _winsize() -> Tuple[int, int]:
rows, cols, _, _ = struct.unpack(fmt, val)
return rows, cols
except (ImportError, IOError):
return (int(os.environ.get("LINES", 25)), int(os.environ.get("COLUMNS", 80)))
try:
sz = os.get_terminal_size()
return sz.lines, sz.columns
except OSError:
return (int(os.environ.get("LINES", 25)), int(os.environ.get("COLUMNS", 80)))


async def open_connection(
Expand Down Expand Up @@ -586,7 +590,7 @@ async def open_connection(
"""
if client_factory is None:
client_factory = TelnetClient
if sys.platform != "win32" and sys.stdin.isatty():
if sys.stdin.isatty():
client_factory = TelnetTerminalClient

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

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

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

client.writer.passive_do = {_GMCP}
client.writer.environ_encoding = environ_encoding
client.writer._encoding_explicit = encoding_explicit

client.connection_made = _patched_connection_made # type: ignore[method-assign]
Expand Down
Loading
Loading