From 60dadfba145cefd35bced0634949172112da3691 Mon Sep 17 00:00:00 2001 From: historia Date: Tue, 25 Aug 2026 17:15:01 -0400 Subject: feat: display console output when stopping server after generation --- app/ui/runview.py | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) (limited to 'app/ui/runview.py') diff --git a/app/ui/runview.py b/app/ui/runview.py index d65978d..f016922 100644 --- a/app/ui/runview.py +++ b/app/ui/runview.py @@ -44,6 +44,7 @@ _SERVER_STATES = { "starting": ("starting", "warn"), "ready": ("ready", "ok"), "processing": ("processing", "ok"), + "stopping": ("stopping", "warn"), "down": ("not responding", "err"), "error": ("error", "err"), "stopped": ("stopped", "info"), @@ -113,6 +114,7 @@ class RunView: self.finished_at: Optional[float] = None self.boot_started: Optional[float] = None self.convert_started: Optional[float] = None + self.stop_started: Optional[float] = None self.server_log_path = "" # -- threads --------------------------------------------------- self._queue: Queue = Queue() @@ -156,6 +158,8 @@ class RunView: elif kind == "server_down": if self.phase == "convert": self.server = "down" + elif kind == "server_stopped": + self.server = "stopped" elif kind == "book": self.phase = "convert" self.book = (event.get("index"), event.get("total"), @@ -349,7 +353,13 @@ class RunView: return True def _confirm_stop_server(self) -> None: - """Ask whether to stop the server this run started (once).""" + """Ask whether to stop the server this run started (once). + + On "yes" the stop runs on a background thread while the screen keeps + redrawing the server panel — showing "stopping" with an elapsed clock, + mirroring the boot screen — so the SIGTERM grace period never freezes + the TUI. Returns once the server is gone. + """ if not self.started_server or self._server_stopped_confirmed: return self._server_stopped_confirmed = True @@ -357,13 +367,32 @@ class RunView: if not name or not servers.alive(name): return self._blocking() - answer = tui.confirm(self.scr, - f"Stop the '{name}' server now?", default=True, - cancel_value=False) - if answer: - with contextlib.redirect_stdout(io.StringIO()): - servers.stop(name) - self.server = "stopped" + try: + answer = tui.confirm(self.scr, + f"Stop the '{name}' server now?", default=True, + cancel_value=False) + finally: + self._nonblocking() + if not answer: + return + self.server = "stopping" + self.stop_started = self._now() + done = threading.Event() + + def _stop() -> None: + try: + with contextlib.redirect_stdout(io.StringIO()): + servers.stop(name) + finally: + self._queue.put({"kind": "server_stopped"}) + done.set() + + threading.Thread(target=_stop, daemon=True).start() + while not done.wait(_DRAW_TIMEOUT_MS / 1000.0): + self._drain() + self.render() + self._drain() + self.render() def _blocking(self) -> None: """Make getch block (used while a confirm dialog owns the screen).""" @@ -417,6 +446,8 @@ class RunView: self.server, (self.server, "info")) if self.server == "starting" and self.boot_started is not None: state_text += f" ({int(self._now() - self.boot_started)}s)" + elif self.server == "stopping" and self.stop_started is not None: + state_text += f" ({int(self._now() - self.stop_started)}s)" if self.config.autostart_spec is None and self.server == "ready": state_text += " (external)" _text(scr, theme, y, inner_x, "Status".ljust(label_w), theme["dim"]) -- cgit v1.2.3