@@ -98,7 +98,6 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None:
9898 and character set negotiation.
9999 """
100100 # pylint: disable=import-outside-toplevel
101- # local
102101 from telnetlib3 .telopt import NAWS , TTYPE , TSPEED , CHARSET , XDISPLOC , NEW_ENVIRON
103102
104103 super ().connection_made (transport )
@@ -198,17 +197,17 @@ def _normalize_charset_name(name: str) -> str:
198197 :param name: Raw charset name from the server.
199198 :returns: Normalized name suitable for :func:`codecs.lookup`.
200199 """
201- # std imports
202200 import re # pylint: disable=import-outside-toplevel
203- base = name .strip ().replace (' ' , '-' )
201+
202+ base = name .strip ().replace (" " , "-" )
204203 # Strip leading zeros from numeric segments: iso-8859-02 → iso-8859-2
205- no_leading_zeros = re .sub (r' -0+(\d)' , r' -\1' , base )
204+ no_leading_zeros = re .sub (r" -0+(\d)" , r" -\1" , base )
206205 # All hyphens removed: cp-1250 → cp1250
207- no_hyphens = base .replace ('-' , '' )
206+ no_hyphens = base .replace ("-" , "" )
208207 # Keep first hyphen-segment, collapse the rest: iso-8859-2 stays
209- parts = no_leading_zeros .split ('-' )
208+ parts = no_leading_zeros .split ("-" )
210209 if len (parts ) > 2 :
211- partial = parts [0 ] + '-' + '' .join (parts [1 :])
210+ partial = parts [0 ] + "-" + "" .join (parts [1 :])
212211 else :
213212 partial = no_leading_zeros
214213 for candidate in (base , no_leading_zeros , no_hyphens , partial ):
@@ -256,9 +255,7 @@ def send_charset(self, offered: List[str]) -> str:
256255
257256 for offer in offered :
258257 try :
259- canon = codecs .lookup (
260- self ._normalize_charset_name (offer )
261- ).name
258+ canon = codecs .lookup (self ._normalize_charset_name (offer )).name
262259
263260 # Record first viable encoding
264261 if first_viable is None :
@@ -382,7 +379,6 @@ def send_env(self, keys: Sequence[str]) -> Dict[str, Any]:
382379 @staticmethod
383380 def _winsize () -> Tuple [int , int ]:
384381 try :
385- # std imports
386382 import fcntl # pylint: disable=import-outside-toplevel
387383 import termios # pylint: disable=import-outside-toplevel
388384
@@ -575,7 +571,6 @@ def _patched_connection_made(transport: asyncio.BaseTransport) -> None:
575571 colormatch : str = args ["colormatch" ]
576572 shell_callback = args ["shell" ]
577573 if colormatch .lower () != "none" :
578- # local
579574 from .color_filter import ( # pylint: disable=import-outside-toplevel
580575 PALETTES ,
581576 ColorConfig ,
@@ -625,29 +620,36 @@ async def _color_shell(
625620 shell_callback = _color_shell
626621
627622 # Wrap shell to inject raw_mode flag and input translation for retro encodings
628- raw_mode : bool = args .get ("raw_mode" , False )
629- if raw_mode :
630- # local
623+ raw_mode_val : Optional [bool ] = args .get ("raw_mode" , False )
624+ if raw_mode_val is not False :
631625 from .client_shell import ( # pylint: disable=import-outside-toplevel
632626 _INPUT_XLAT ,
633627 _INPUT_SEQ_XLAT ,
634628 InputFilter ,
635629 )
636630
637631 enc_key = (args .get ("encoding" , "" ) or "" ).lower ()
638- byte_xlat = _INPUT_XLAT .get (enc_key , {})
639- seq_xlat = _INPUT_SEQ_XLAT .get (enc_key , {})
632+ byte_xlat = dict (_INPUT_XLAT .get (enc_key , {}))
633+ if args .get ("ascii_eol" ):
634+ # --ascii-eol: don't translate CR/LF to encoding-native EOL
635+ byte_xlat .pop (0x0D , None )
636+ byte_xlat .pop (0x0A , None )
637+ seq_xlat = {} if args .get ("ansi_keys" ) else _INPUT_SEQ_XLAT .get (enc_key , {})
640638 input_filter : Optional [InputFilter ] = (
641639 InputFilter (seq_xlat , byte_xlat ) if (seq_xlat or byte_xlat ) else None
642640 )
641+ ascii_eol : bool = args .get ("ascii_eol" , False )
643642 _inner_shell = shell_callback
644643
645644 async def _raw_shell (
646645 reader : Union [TelnetReader , TelnetReaderUnicode ],
647646 writer_arg : Union [TelnetWriter , TelnetWriterUnicode ],
648647 ) -> None :
649648 # pylint: disable-next=protected-access
650- writer_arg ._raw_mode = True # type: ignore[union-attr]
649+ writer_arg ._raw_mode = raw_mode_val # type: ignore[union-attr]
650+ if ascii_eol :
651+ # pylint: disable-next=protected-access
652+ writer_arg ._ascii_eol = True # type: ignore[union-attr]
651653 if input_filter is not None :
652654 # pylint: disable-next=protected-access
653655 writer_arg ._input_filter = input_filter # type: ignore[union-attr]
@@ -703,13 +705,21 @@ def _get_argument_parser() -> argparse.ArgumentParser:
703705 )
704706
705707 parser .add_argument ("--force-binary" , action = "store_true" , help = "force encoding" , default = True )
706- parser .add_argument (
708+ mode_group = parser .add_mutually_exclusive_group ()
709+ mode_group .add_argument (
710+ "--raw-mode" ,
711+ action = "store_true" ,
712+ default = False ,
713+ help = "force raw terminal mode (no line buffering, no local echo). "
714+ "Correct for BBS and retro systems. Default: auto-detect from "
715+ "server negotiation." ,
716+ )
717+ mode_group .add_argument (
707718 "--line-mode" ,
708719 action = "store_true" ,
709720 default = False ,
710- help = "use line-buffered input with local echo instead of raw terminal "
711- "mode. By default the client uses raw mode (no line buffering, no "
712- "local echo) which is correct for most BBS and MUD servers." ,
721+ help = "force line-buffered input with local echo. Appropriate for "
722+ "simple command-line services." ,
713723 )
714724 parser .add_argument (
715725 "--connect-minwait" , default = 0 , type = float , help = "shell delay for negotiation"
@@ -779,6 +789,22 @@ def _get_argument_parser() -> argparse.ArgumentParser:
779789 default = False ,
780790 help = "swap foreground/background for light-background terminals" ,
781791 )
792+ parser .add_argument (
793+ "--ascii-eol" ,
794+ action = "store_true" ,
795+ default = False ,
796+ help = "use ASCII CR/LF for line endings instead of encoding-native "
797+ "EOL (e.g. ATASCII 0x9B). Use for BBSes that display retro "
798+ "graphics but use standard CR/LF for line breaks." ,
799+ )
800+ parser .add_argument (
801+ "--ansi-keys" ,
802+ action = "store_true" ,
803+ default = False ,
804+ help = "transmit raw ANSI escape sequences for arrow and function "
805+ "keys instead of encoding-specific control codes. Use for "
806+ "BBSes that expect ANSI cursor sequences." ,
807+ )
782808 return parser
783809
784810
@@ -790,7 +816,6 @@ def _parse_option_arg(value: str) -> bytes:
790816 :returns: Single-byte option value.
791817 :raises ValueError: When *value* is not a known name or valid integer.
792818 """
793- # local
794819 from .telopt import option_from_name # pylint: disable=import-outside-toplevel
795820
796821 try :
@@ -815,12 +840,17 @@ def _parse_background_color(value: str) -> Tuple[int, int, int]:
815840
816841def _transform_args (args : argparse .Namespace ) -> Dict [str , Any ]:
817842 # Auto-enable force_binary for retro BBS encodings that use high-bit bytes.
818- # local
819843 from .encodings import FORCE_BINARY_ENCODINGS # pylint: disable=import-outside-toplevel
820844
821845 force_binary = args .force_binary
822- raw_mode = not args .line_mode
823- if args .encoding .lower ().replace ('-' , '_' ) in FORCE_BINARY_ENCODINGS :
846+ # Three-state: True (forced raw), False (forced line), None (auto-detect)
847+ if args .raw_mode :
848+ raw_mode : Optional [bool ] = True
849+ elif args .line_mode :
850+ raw_mode = False
851+ else :
852+ raw_mode = None
853+ if args .encoding .lower ().replace ("-" , "_" ) in FORCE_BINARY_ENCODINGS :
824854 force_binary = True
825855 raw_mode = True
826856
@@ -847,6 +877,8 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
847877 "background_color" : _parse_background_color (args .background_color ),
848878 "reverse_video" : args .reverse_video ,
849879 "raw_mode" : raw_mode ,
880+ "ascii_eol" : args .ascii_eol ,
881+ "ansi_keys" : args .ansi_keys ,
850882 }
851883
852884
@@ -957,7 +989,6 @@ async def run_fingerprint_client() -> None:
957989 :func:`~telnetlib3.server_fingerprinting.fingerprinting_client_shell`
958990 via :func:`functools.partial`, and runs the connection.
959991 """
960- # local
961992 from . import fingerprinting # pylint: disable=import-outside-toplevel
962993 from . import server_fingerprinting # pylint: disable=import-outside-toplevel
963994
@@ -1021,8 +1052,12 @@ def patched_connection_made(transport: asyncio.BaseTransport) -> None:
10211052 client .writer .environ_encoding = environ_encoding
10221053 # pylint: disable-next=protected-access
10231054 client .writer ._encoding_explicit = environ_encoding != "ascii"
1024- client .writer .always_will = fp_always_will
1025- client .writer .always_do = fp_always_do
1055+ # pylint: disable-next=import-outside-toplevel
1056+ from .fingerprinting import EXTENDED_OPTIONS
1057+
1058+ mud_opts = {opt for opt , _ , _ in EXTENDED_OPTIONS }
1059+ client .writer .always_will = fp_always_will | mud_opts
1060+ client .writer .always_do = fp_always_do | mud_opts
10261061
10271062 def patched_send_env (keys : Sequence [str ]) -> Dict [str , Any ]:
10281063 result = orig_send_env (keys )
0 commit comments