diff options
| author | historia <historiavg@proton.me> | 2026-08-25 17:15:01 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-25 17:15:01 -0400 |
| commit | 60dadfba145cefd35bced0634949172112da3691 (patch) | |
| tree | e003626940bdcf2cde482561c777deb864308dba | |
| parent | 775b716d86f5e681807698522e56c3d0f861c5bf (diff) | |
| download | tts-audiobook-generator-60dadfba145cefd35bced0634949172112da3691.tar.gz | |
feat: display console output when stopping server after generation
| -rw-r--r-- | app/tests/test_runview.py | 20 | ||||
| -rw-r--r-- | app/ui/runview.py | 47 |
2 files changed, 59 insertions, 8 deletions
diff --git a/app/tests/test_runview.py b/app/tests/test_runview.py index 73cae5f..df697c8 100644 --- a/app/tests/test_runview.py +++ b/app/tests/test_runview.py @@ -119,6 +119,12 @@ class StateTransitionTests(_FakeTui, unittest.TestCase): view.handle_event({"kind": "server_down"}) self.assertEqual(view.server, "down") + def test_server_stopped_event_marks_server_stopped(self): + view, _ = self.make_view() + view.server = "stopping" + view.handle_event({"kind": "server_stopped"}) + self.assertEqual(view.server, "stopped") + class RenderTests(_FakeTui, unittest.TestCase): def _strings(self, screen): @@ -149,6 +155,20 @@ class RenderTests(_FakeTui, unittest.TestCase): self.assertIn("book.txt", text) self.assertIn("press any key", text) + def test_stopping_status_shows_elapsed(self): + view, screen = self.make_view() + view.handle_event({"kind": "book", "index": 1, "total": 1, + "name": "book.txt"}) + view.handle_event({"kind": "book_done", "name": "book.txt", + "ok": True}) + view.handle_event({"kind": "done", "ok": 1, "total": 1}) + view.server = "stopping" + view.stop_started = 999.0 + view.render() + text = self._strings(screen) + self.assertIn("stopping", text) + self.assertIn("(1s)", text) + def test_error_screen_shows_detail_and_log(self): view, screen = self.make_view(log_path="/tmp/audiobook.log") view.handle_event({"kind": "book", "index": 1, "total": 1, 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"]) |
