Skip to content

Commit 0a0636c

Browse files
committed
fix: include rows/cols in stdin messages and send Ctrl+L after resize toggle
1 parent 54445b8 commit 0a0636c

1 file changed

Lines changed: 66 additions & 16 deletions

File tree

centml/cli/shell.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,22 @@ def _resolve_pod(cclient, deployment_id, pod_name=None):
5252
running_pods.append(pod.name)
5353

5454
if not running_pods:
55-
raise click.ClickException(f"No running pods found for deployment {deployment_id}")
55+
raise click.ClickException(
56+
f"No running pods found for deployment {deployment_id}"
57+
)
5658

5759
if pod_name is not None:
5860
if pod_name not in running_pods:
5961
pods_list = ", ".join(running_pods)
60-
raise click.ClickException(f"Pod '{pod_name}' not found. Available running pods: {pods_list}")
62+
raise click.ClickException(
63+
f"Pod '{pod_name}' not found. Available running pods: {pods_list}"
64+
)
6165
return pod_name
6266

6367
if len(running_pods) > 1:
6468
click.echo(
65-
f"Multiple running pods found, connecting to {running_pods[0]}. " f"Use --pod to specify a different pod.",
69+
f"Multiple running pods found, connecting to {running_pods[0]}. "
70+
f"Use --pod to specify a different pod.",
6671
err=True,
6772
)
6873
return running_pods[0]
@@ -110,8 +115,16 @@ def _on_stdin_ready():
110115
data = await asyncio.wait_for(read_queue.get(), timeout=0.5)
111116
except asyncio.TimeoutError:
112117
continue
118+
r, c = shutil.get_terminal_size()
113119
await ws.send(
114-
json.dumps({"operation": "stdin", "data": data.decode("utf-8", errors="replace")})
120+
json.dumps(
121+
{
122+
"operation": "stdin",
123+
"data": data.decode("utf-8", errors="replace"),
124+
"rows": r,
125+
"cols": c,
126+
}
127+
)
115128
)
116129
finally:
117130
loop.remove_reader(stdin_fd)
@@ -144,14 +157,20 @@ async def _interactive_session(ws_url, token):
144157
rows, cols = shutil.get_terminal_size()
145158

146159
headers = {"Authorization": f"Bearer {token}"}
147-
async with websockets.connect(ws_url, additional_headers=headers, close_timeout=2) as ws:
148-
await ws.send(json.dumps({"operation": "resize", "rows": rows, "cols": cols}))
160+
async with websockets.connect(
161+
ws_url, additional_headers=headers, close_timeout=2
162+
) as ws:
163+
await ws.send(
164+
json.dumps({"operation": "resize", "rows": rows, "cols": cols})
165+
)
149166

150167
loop = asyncio.get_running_loop()
151168

152169
def _send_resize():
153170
r, c = shutil.get_terminal_size()
154-
asyncio.ensure_future(ws.send(json.dumps({"operation": "resize", "rows": r, "cols": c})))
171+
asyncio.ensure_future(
172+
ws.send(json.dumps({"operation": "resize", "rows": r, "cols": c}))
173+
)
155174

156175
async def _force_initial_redraw():
157176
"""Force SIGWINCH on the remote by toggling the PTY width.
@@ -164,9 +183,17 @@ async def _force_initial_redraw():
164183
try:
165184
await asyncio.sleep(0.5)
166185
r, c = shutil.get_terminal_size()
167-
await ws.send(json.dumps({"operation": "resize", "rows": r, "cols": c + 1}))
186+
await ws.send(
187+
json.dumps({"operation": "resize", "rows": r, "cols": c + 1})
188+
)
168189
await asyncio.sleep(0.05)
169-
await ws.send(json.dumps({"operation": "resize", "rows": r, "cols": c}))
190+
await ws.send(
191+
json.dumps({"operation": "resize", "rows": r, "cols": c})
192+
)
193+
await asyncio.sleep(0.1)
194+
# Ctrl+L clears screen and forces the shell to redraw
195+
# its prompt at the now-correct terminal width.
196+
await ws.send(json.dumps({"operation": "stdin", "data": "\x0c"}))
170197
except Exception:
171198
pass
172199

@@ -208,7 +235,9 @@ async def _exec_session(ws_url, token, command):
208235
rows, cols = shutil.get_terminal_size(fallback=(80, 24))
209236
headers = {"Authorization": f"Bearer {token}"}
210237

211-
async with websockets.connect(ws_url, additional_headers=headers, close_timeout=2) as ws:
238+
async with websockets.connect(
239+
ws_url, additional_headers=headers, close_timeout=2
240+
) as ws:
212241
await ws.send(json.dumps({"operation": "resize", "rows": rows, "cols": cols}))
213242

214243
# Suppress echo/bracketed-paste, emit begin marker, run command,
@@ -264,8 +293,16 @@ async def _exec_session(ws_url, token, command):
264293

265294
@click.command(help="Open an interactive shell to a deployment pod")
266295
@click.argument("deployment_id", type=int)
267-
@click.option("--pod", default=None, help="Specific pod name (auto-selects first running pod)")
268-
@click.option("--shell", "shell_type", default=None, type=click.Choice(["bash", "sh", "zsh"]), help="Shell type")
296+
@click.option(
297+
"--pod", default=None, help="Specific pod name (auto-selects first running pod)"
298+
)
299+
@click.option(
300+
"--shell",
301+
"shell_type",
302+
default=None,
303+
type=click.Choice(["bash", "sh", "zsh"]),
304+
help="Shell type",
305+
)
269306
@handle_exception
270307
def shell(deployment_id, pod, shell_type):
271308
if not sys.stdin.isatty():
@@ -274,23 +311,36 @@ def shell(deployment_id, pod, shell_type):
274311
with get_centml_client() as cclient:
275312
pod_name = _resolve_pod(cclient, deployment_id, pod)
276313

277-
ws_url = _build_ws_url(settings.CENTML_PLATFORM_API_URL, deployment_id, pod_name, shell_type)
314+
ws_url = _build_ws_url(
315+
settings.CENTML_PLATFORM_API_URL, deployment_id, pod_name, shell_type
316+
)
278317
token = auth.get_centml_token()
279318
exit_code = asyncio.run(_interactive_session(ws_url, token))
280319
sys.exit(exit_code)
281320

282321

283-
@click.command(help="Execute a command in a deployment pod", context_settings={"ignore_unknown_options": True})
322+
@click.command(
323+
help="Execute a command in a deployment pod",
324+
context_settings={"ignore_unknown_options": True},
325+
)
284326
@click.argument("deployment_id", type=int)
285327
@click.argument("command", nargs=-1, required=True, type=click.UNPROCESSED)
286328
@click.option("--pod", default=None, help="Specific pod name")
287-
@click.option("--shell", "shell_type", default=None, type=click.Choice(["bash", "sh", "zsh"]), help="Shell type")
329+
@click.option(
330+
"--shell",
331+
"shell_type",
332+
default=None,
333+
type=click.Choice(["bash", "sh", "zsh"]),
334+
help="Shell type",
335+
)
288336
@handle_exception
289337
def exec_cmd(deployment_id, command, pod, shell_type):
290338
with get_centml_client() as cclient:
291339
pod_name = _resolve_pod(cclient, deployment_id, pod)
292340

293-
ws_url = _build_ws_url(settings.CENTML_PLATFORM_API_URL, deployment_id, pod_name, shell_type)
341+
ws_url = _build_ws_url(
342+
settings.CENTML_PLATFORM_API_URL, deployment_id, pod_name, shell_type
343+
)
294344
token = auth.get_centml_token()
295345
cmd_str = " ".join(command)
296346
exit_code = asyncio.run(_exec_session(ws_url, token, cmd_str))

0 commit comments

Comments
 (0)