From 6cfcd564c0684c52618235e6366f4a81c02b9a5b Mon Sep 17 00:00:00 2001 From: historia Date: Tue, 1 Sep 2026 14:32:05 -0400 Subject: slop refactor/dedup --- app/ui/runview.py | 55 +++++++++++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 30 deletions(-) (limited to 'app/ui/runview.py') diff --git a/app/ui/runview.py b/app/ui/runview.py index 21ac08c..005db59 100644 --- a/app/ui/runview.py +++ b/app/ui/runview.py @@ -37,6 +37,7 @@ from typing import Callable, List, Optional from backends import common, servers from ui import tui +from ui import viewkit from ui.viewkit import (TERMINAL_PHASES as _TERMINAL, DRAW_TIMEOUT_MS as _DRAW_TIMEOUT_MS, ScreenView, _box, _fit, _format_elapsed, _sep, @@ -58,42 +59,23 @@ _SERVER_STATES = { _MONITOR_INTERVAL = 2.0 -class _LogAppender: +class _LogAppender(viewkit.LineSplitter): """A file-like that appends redirected console output to the run's log. The run view owns the screen, so anything a conversion prints to stdout/stderr outside the progress events would otherwise be swallowed - silently; this mirrors it line by line into the run's dated log file + silently; this mirrors it line by line (\\n and \\r — see + viewkit.LineSplitter) into the run's dated log file (RunConfig.log_path, the audiobook_ day stream), prefixed with the same timestamp format the converter's log records use. Best-effort: write errors are swallowed, and an empty path disables logging. """ def __init__(self, path: str): + super().__init__(self._append_line) self._path = path - self._buffer = "" - - def write(self, text: str) -> int: - if not text: - return 0 - self._buffer += text - while True: - cut = self._buffer.find("\n") - if cut < 0: - break - line, self._buffer = self._buffer[:cut], self._buffer[cut + 1:] - self._append(line) - return len(text) - - def flush(self) -> None: - if self._buffer: - self._append(self._buffer) - self._buffer = "" - - def isatty(self) -> bool: - return False - def _append(self, line: str) -> None: + def _append_line(self, line: str) -> None: if not self._path or not line.strip(): return try: @@ -274,6 +256,10 @@ class RunView(ScreenView): if event.get("cancelled"): self.cancelled = True self._finish("cancelled") + elif total == 0 and ok == 0 and not self.error_message: + # An empty run (no books found, or all skipped): a clean + # no-op, not a failure — there was nothing that could fail. + self._finish("done") elif total and ok >= total and not self.error_message: self._finish("done") else: @@ -404,21 +390,30 @@ class RunView(ScreenView): going. """ self._blocking() - answer = tui.confirm(self.scr, "Cancel processing?", default=False, - cancel_value=False) - if not answer: + try: + answer = tui.confirm(self.scr, "Cancel processing?", default=False, + cancel_value=False) + finally: self._nonblocking() + if not answer: return False self.cancelling = True self._cancel.set() # Wind the worker down BEFORE offering the server stop: killing the # server under a still-running request turns the cancellation into # request failures (reported as "failed" instead of "cancelled"). - self._worker.join(timeout=60) + # The join is best-effort — a wedged worker delays but cannot veto + # the flow below. + self._join_worker() # When this run booted the server, offer to shut it down too (the # boot path kills it itself when cancelled before ready); by now - # the worker is done, so nothing is mid-request. - self._confirm_stop_server() + # the worker is done (or wedged beyond saving), so nothing further + # is mid-request from this view's side. + self._blocking() + try: + self._confirm_stop_server() + finally: + self._nonblocking() self._drain() self.render() # One more key press acknowledges the final screen. -- cgit v1.2.3