Skip to content

Commit c49e5c5

Browse files
committed
clean up pyte
1 parent 59d9d08 commit c49e5c5

5 files changed

Lines changed: 36 additions & 298 deletions

File tree

centml/cli/shell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _connect_args(deployment_id, pod, shell_type):
2929

3030
@click.command(help="Open an interactive shell to a deployment pod")
3131
@click.argument("deployment_id", type=int)
32-
@click.option("--pod", default=None, help="Specific pod name (auto-selects first running pod)")
32+
@click.option("--pod", default=None, help="Specify a pod name (Default: auto-selects first running pod)")
3333
@click.option("--shell", "shell_type", default=None, type=click.Choice(["bash", "sh", "zsh"]), help="Shell type")
3434
@handle_exception
3535
def shell(deployment_id, pod, shell_type):

centml/sdk/shell/renderer.py

Lines changed: 0 additions & 130 deletions
This file was deleted.

centml/sdk/shell/session.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from centml.sdk import PodStatus
1717
from centml.sdk.shell.exceptions import NoPodAvailableError, PodNotFoundError
18-
from centml.sdk.shell.renderer import pyte_extract_text, render_dirty
1918

2019
BEGIN_MARKER = "__CENTML_BEGIN_5f3a__"
2120
END_MARKER = "__CENTML_END_5f3a__"
@@ -74,20 +73,19 @@ def resolve_pod(cclient, deployment_id, pod_name=None) -> Tuple[str, Optional[st
7473
return running_pods[0], warning
7574

7675

77-
async def forward_io(ws, screen, stream, shutdown):
76+
async def forward_io(ws, term_size, shutdown):
7877
"""Bidirectional forwarding between local stdin/stdout and WebSocket.
7978
80-
Output flows through a pyte terminal emulator so that cursor
81-
addressing, line wrapping, and colors are rendered correctly
82-
regardless of the remote PTY dimensions.
79+
Output is written directly to stdout (the local terminal is in raw
80+
mode, so ANSI sequences from the remote PTY are rendered natively).
8381
8482
The platform API proxy sends a close frame (code=1000) when the
8583
remote shell exits, so _read_ws terminates via ConnectionClosed.
8684
8785
Args:
8886
ws: WebSocket connection.
89-
screen: pyte.Screen instance sized to the local terminal.
90-
stream: pyte.Stream attached to *screen*.
87+
term_size: Mutable list ``[cols, rows]`` kept up-to-date by the
88+
SIGWINCH handler in ``interactive_session``.
9189
shutdown: asyncio.Event set by signal handlers to request exit.
9290
9391
Returns:
@@ -104,11 +102,11 @@ async def _read_ws():
104102
msg = json.loads(raw_msg)
105103
data = msg.get("data", "")
106104
if data:
107-
stream.feed(data.replace("\n", "\r\n"))
108-
render_dirty(screen, sys.stdout.buffer)
105+
sys.stdout.buffer.write(data.replace("\n", "\r\n").encode("utf-8", errors="replace"))
106+
sys.stdout.buffer.flush()
109107
elif msg.get("error"):
110-
stream.feed(f"Error: {msg['error']}\r\n")
111-
render_dirty(screen, sys.stdout.buffer)
108+
sys.stderr.buffer.write(f"Error: {msg['error']}\r\n".encode())
109+
sys.stderr.buffer.flush()
112110
except websockets.ConnectionClosed:
113111
return
114112

@@ -135,8 +133,8 @@ def _on_stdin_ready():
135133
{
136134
"operation": "stdin",
137135
"data": data.decode("utf-8", errors="replace"),
138-
"rows": screen.lines,
139-
"cols": screen.columns,
136+
"rows": term_size[1],
137+
"cols": term_size[0],
140138
}
141139
)
142140
)
@@ -183,13 +181,7 @@ async def interactive_session(ws_url, token):
183181
try:
184182
tty.setraw(fd)
185183
cols, rows = shutil.get_terminal_size()
186-
187-
screen = pyte.Screen(cols, rows)
188-
stream = pyte.Stream(screen)
189-
190-
# Switch to alternate screen buffer (disables scrollback) and clear.
191-
sys.stdout.buffer.write(b"\033[?1049h\033[2J\033[H")
192-
sys.stdout.buffer.flush()
184+
term_size = [cols, rows]
193185

194186
shutdown = asyncio.Event()
195187
loop.add_signal_handler(signal.SIGTERM, shutdown.set)
@@ -201,15 +193,14 @@ async def interactive_session(ws_url, token):
201193

202194
def _send_resize():
203195
c, r = shutil.get_terminal_size()
204-
screen.resize(r, c)
205-
screen.dirty.update(range(r))
196+
term_size[0], term_size[1] = c, r
206197
asyncio.ensure_future(ws.send(json.dumps({"operation": "resize", "rows": r, "cols": c})))
207198

208199
loop.add_signal_handler(signal.SIGWINCH, _send_resize)
209200

210201
await ws.send(json.dumps({"operation": "resize", "rows": rows, "cols": cols}))
211202
try:
212-
exit_code = await forward_io(ws, screen, stream, shutdown)
203+
exit_code = await forward_io(ws, term_size, shutdown)
213204
finally:
214205
loop.remove_signal_handler(signal.SIGWINCH)
215206

@@ -219,9 +210,17 @@ def _send_resize():
219210
loop.remove_signal_handler(signal.SIGTERM)
220211
loop.remove_signal_handler(signal.SIGHUP)
221212
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
222-
# Leave alternate screen buffer, restore cursor and attributes.
223-
sys.stdout.buffer.write(b"\033[?1049l\033[?25h\033[0m")
224-
sys.stdout.buffer.flush()
213+
214+
215+
def _pyte_extract_text(line_stream, line_screen, text):
216+
"""Feed text through a single-row pyte screen and return visible characters.
217+
218+
More robust than regex ANSI stripping: pyte interprets all VT100/VT220
219+
sequences including OSC, cursor repositioning, and truecolor escapes.
220+
"""
221+
line_screen.reset()
222+
line_stream.feed(text)
223+
return "".join(line_screen.buffer[0][col].data for col in range(line_screen.columns)).rstrip()
225224

226225

227226
async def exec_session(ws_url, token, command):
@@ -265,7 +264,7 @@ async def exec_session(ws_url, token, command):
265264
buffer += msg["data"]
266265
while "\n" in buffer:
267266
line, buffer = buffer.split("\n", 1)
268-
clean = pyte_extract_text(line_stream, line_screen, line.rstrip("\r"))
267+
clean = _pyte_extract_text(line_stream, line_screen, line.rstrip("\r"))
269268
if BEGIN_MARKER in clean:
270269
is_capturing = True
271270
continue

0 commit comments

Comments
 (0)