From cccdaf72093e107c1fc6d654084a0f4368cf26ce Mon Sep 17 00:00:00 2001 From: Per-Arne Hellarvik Date: Mon, 2 Nov 2026 20:41:24 +0100 Subject: [PATCH] Reset the terminal before opening the LXC shell Write ESC c, the sequence `reset` sends, inside suspend() right before ssh. The container shell then starts on a clean screen with no leftover TUI terminal modes. It runs locally, so the container does not need `reset` installed and `pct enter` is unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/plans/guest-tui-mvp.md | 4 ++++ src/cv4pve_tui/app.py | 8 ++++++++ tests/test_app.py | 14 ++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/docs/plans/guest-tui-mvp.md b/docs/plans/guest-tui-mvp.md index b0097c6..7843813 100644 --- a/docs/plans/guest-tui-mvp.md +++ b/docs/plans/guest-tui-mvp.md @@ -210,6 +210,10 @@ root's `authorized_keys` (verified: `ssh -o BatchMode=yes root@mainframe pct sta - Exit 255 (ssh itself failed) -> error notify naming the guest and the code. Other exit codes are the container shell's last status (`exit` after a failed command gives 1) and are not errors. +- Inside `suspend()`, right before ssh, the terminal gets a full reset (`ESC c`, what + `reset` sends) so the container shell starts on a clean screen without leftover TUI + terminal modes (user request). Done locally, not in the container: no dependency on + `reset` existing there, and `pct enter` stays unchanged. - `OSError` from `subprocess.run` is caught *inside* the `suspend()` block: Textual 8.2.8's `suspend()` has no try/finally, so an exception escaping it leaves the app suspended. diff --git a/src/cv4pve_tui/app.py b/src/cv4pve_tui/app.py index 2aad45b..bbb8664 100644 --- a/src/cv4pve_tui/app.py +++ b/src/cv4pve_tui/app.py @@ -1,6 +1,7 @@ """Textual app for managing Proxmox guests through cv4pve-cli.""" import subprocess +import sys from textual import work from textual.app import App, ComposeResult, SuspendNotSupported @@ -358,6 +359,7 @@ class GuestApp(App): try: with self.suspend(): try: # inside: Textual's suspend() skips the resume if its body raises + _reset_terminal() code = subprocess.run(cmd, check=False).returncode except OSError as exc: error = _message(exc) @@ -426,6 +428,12 @@ class GuestApp(App): self._start_job(guest, "edit", job) +def _reset_terminal() -> None: + """Full terminal reset (ESC c, what `reset` sends): clean screen, no leftover TUI modes.""" + sys.stdout.write("\033c") + sys.stdout.flush() + + def _scaled(value, unit: int): return "-" if value is None else round(value / unit) diff --git a/tests/test_app.py b/tests/test_app.py index ffc866b..1811d43 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -907,3 +907,17 @@ async def test_table_reloads_after_shell(fake, monkeypatch): await press_t(app, pilot) await settle(app, pilot) assert fake.list_calls > before + + +async def test_terminal_is_reset_inside_suspend_before_ssh(fake, monkeypatch): + # user request: a clean screen (and no leftover TUI terminal modes) in the container shell + fake.guests = [RUNNING_CT] + app = make_app() + events = patch_shell(monkeypatch, app) + resets = [] + monkeypatch.setattr( + "cv4pve_tui.app._reset_terminal", lambda: resets.append(list(events)) + ) + async with app.run_test() as pilot: + await press_t(app, pilot) + assert resets == [[("enter", None)]] # inside suspend, before ssh ran