Skip to content

Commit c4fde87

Browse files
committed
Update the telnet server to provide the client terminal type to the prompt-toolkit output object
1 parent a9799ff commit c4fde87

File tree

2 files changed

+57
-22
lines changed

2 files changed

+57
-22
lines changed

prompt_toolkit/contrib/telnet/protocol.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def int2byte(number: int) -> bytes:
3636
LINEMODE = int2byte(34)
3737
SUPPRESS_GO_AHEAD = int2byte(3)
3838

39+
TTYPE = int2byte(24)
40+
SEND = int2byte(1)
41+
IS = int2byte(0)
42+
3943
DM = int2byte(242)
4044
BRK = int2byte(243)
4145
IP = int2byte(244)
@@ -65,10 +69,12 @@ def __init__(
6569
self,
6670
data_received_callback: Callable[[bytes], None],
6771
size_received_callback: Callable[[int, int], None],
72+
ttype_received_callback: Callable[[str], None],
6873
) -> None:
6974

7075
self.data_received_callback = data_received_callback
7176
self.size_received_callback = size_received_callback
77+
self.ttype_received_callback = ttype_received_callback
7278

7379
self._parser = self._parse_coroutine()
7480
self._parser.send(None) # type: ignore
@@ -121,6 +127,17 @@ def naws(self, data: bytes) -> None:
121127
else:
122128
logger.warning("Wrong number of NAWS bytes")
123129

130+
def ttype(self, data: bytes) -> None:
131+
"""
132+
Received terminal type.
133+
"""
134+
subcmd, ttype = data[0:1], data[1:]
135+
if subcmd == IS:
136+
ttype = ttype.decode("ascii")
137+
self.ttype_received_callback(ttype)
138+
else:
139+
logger.warning("Received a non-IS terminal type Subnegotiation")
140+
124141
def negotiate(self, data: bytes) -> None:
125142
"""
126143
Got negotiate data.
@@ -129,6 +146,8 @@ def negotiate(self, data: bytes) -> None:
129146

130147
if command == NAWS:
131148
self.naws(payload)
149+
elif command == TTYPE:
150+
self.ttype(payload)
132151
else:
133152
logger.info("Negotiate (%r got bytes)", len(data))
134153

prompt_toolkit/contrib/telnet/server.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from prompt_toolkit.application.run_in_terminal import run_in_terminal
1212
from prompt_toolkit.data_structures import Size
1313
from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text
14-
from prompt_toolkit.input.posix_pipe import PosixPipeInput
14+
from prompt_toolkit.input import create_pipe_input
1515
from prompt_toolkit.output.vt100 import Vt100_Output
1616
from prompt_toolkit.renderer import print_formatted_text as print_formatted_text
1717
from prompt_toolkit.styles import BaseStyle, DummyStyle
@@ -28,6 +28,8 @@
2828
SE,
2929
SUPPRESS_GO_AHEAD,
3030
WILL,
31+
TTYPE,
32+
SEND,
3133
TelnetProtocolParser,
3234
)
3335

@@ -59,6 +61,12 @@ def _initialize_telnet(connection: socket.socket) -> None:
5961
# Negotiate window size
6062
connection.send(IAC + DO + NAWS)
6163

64+
# Negotiate terminal type
65+
connection.send(IAC + DO + TTYPE)
66+
67+
# Negotiate terminal type
68+
connection.send(IAC + SB + TTYPE + SEND + IAC + SE)
69+
6270

6371
class _ConnectionStdout:
6472
"""
@@ -115,6 +123,7 @@ def __init__(
115123
self.encoding = encoding
116124
self.style = style
117125
self._closed = False
126+
self._ready = asyncio.Event()
118127

119128
# Create "Output" object.
120129
self.size = Size(rows=40, columns=79)
@@ -123,14 +132,14 @@ def __init__(
123132
_initialize_telnet(conn)
124133

125134
# Create input.
126-
self.vt100_input = PosixPipeInput()
135+
self.vt100_output = None
136+
self.vt100_input = create_pipe_input()
127137

128138
# Create output.
129139
def get_size() -> Size:
130140
return self.size
131141

132142
self.stdout = cast(TextIO, _ConnectionStdout(conn, encoding=encoding))
133-
self.vt100_output = Vt100_Output(self.stdout, get_size, write_binary=False)
134143

135144
def data_received(data: bytes) -> None:
136145
""" TelnetProtocolParser 'data_received' callback """
@@ -139,9 +148,17 @@ def data_received(data: bytes) -> None:
139148
def size_received(rows: int, columns: int) -> None:
140149
""" TelnetProtocolParser 'size_received' callback """
141150
self.size = Size(rows=rows, columns=columns)
142-
get_app()._on_resize()
151+
if self.vt100_output is not None:
152+
get_app()._on_resize()
153+
154+
def ttype_received(ttype: str) -> None:
155+
""" TelnetProtocolParser 'ttype_received' callback """
156+
self.vt100_output = Vt100_Output(
157+
self.stdout, get_size, term=ttype, write_binary=False
158+
)
159+
self._ready.set()
143160

144-
self.parser = TelnetProtocolParser(data_received, size_received)
161+
self.parser = TelnetProtocolParser(data_received, size_received, ttype_received)
145162
self.context: Optional[contextvars.Context] = None
146163

147164
async def run_application(self) -> None:
@@ -158,25 +175,24 @@ def handle_incoming_data() -> None:
158175
logger.info("Connection closed by client. %r %r" % self.addr)
159176
self.close()
160177

161-
async def run() -> None:
162-
# Add reader.
163-
loop = get_event_loop()
164-
loop.add_reader(self.conn, handle_incoming_data)
178+
# Add reader.
179+
loop = get_event_loop()
180+
loop.add_reader(self.conn, handle_incoming_data)
165181

166-
try:
182+
try:
183+
# Wait for v100_output to be properly instantiated
184+
await self._ready.wait()
185+
with create_app_session(input=self.vt100_input, output=self.vt100_output):
186+
self.context = contextvars.copy_context()
167187
await self.interact(self)
168-
except Exception as e:
169-
print("Got %s" % type(e).__name__, e)
170-
import traceback
171-
172-
traceback.print_exc()
173-
raise
174-
finally:
175-
self.close()
176-
177-
with create_app_session(input=self.vt100_input, output=self.vt100_output):
178-
self.context = contextvars.copy_context()
179-
await run()
188+
except Exception as e:
189+
print("Got %s" % type(e).__name__, e)
190+
import traceback
191+
192+
traceback.print_exc()
193+
raise
194+
finally:
195+
self.close()
180196

181197
def feed(self, data: bytes) -> None:
182198
"""

0 commit comments

Comments
 (0)