diff options
| author | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
| commit | 6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch) | |
| tree | 55321760a8103bc6b5d79489fac4135a60e6e3ba /app/ui/viewkit.py | |
| parent | dc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff) | |
| download | tts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz | |
slop refactor/dedup
Diffstat (limited to 'app/ui/viewkit.py')
| -rw-r--r-- | app/ui/viewkit.py | 86 |
1 files changed, 72 insertions, 14 deletions
diff --git a/app/ui/viewkit.py b/app/ui/viewkit.py index e54aada..7619db9 100644 --- a/app/ui/viewkit.py +++ b/app/ui/viewkit.py @@ -15,7 +15,7 @@ render methods build on. import threading import time from queue import Empty, Queue -from typing import List, Optional +from typing import Callable, List, Optional from ui import tui @@ -147,7 +147,14 @@ class ScreenView: return key def _prompt_cancel(self) -> bool: - """Esc/q: confirm cancel, then wait for the worker to wind down.""" + """Esc/q: confirm cancel, then wait for the worker to wind down. + + The join is best-effort: a worker wedged in un-killable work + (a stuck subprocess, a hung network call) is left running — the + view reports the cancel and returns, and the worker's daemon + thread dies with the process. Callers must not assume the thread + has stopped (see _join_worker). + """ self._blocking() try: answer = tui.confirm(self.scr, "Cancel this step?", default=False, @@ -158,9 +165,16 @@ class ScreenView: return False self.cancelling = True self._cancel.set() - self._worker.join(timeout=60) + self._join_worker() return True + def _join_worker(self, timeout: float = 60.0) -> None: + """Join the worker if it exists and was started; never raise.""" + worker = self._worker + if worker is None or not worker.is_alive(): + return + worker.join(timeout=timeout) + def _blocking(self) -> None: """Make getch block (used while a confirm dialog owns the screen).""" try: @@ -180,6 +194,45 @@ class ScreenView: # Shared drawing primitives # ---------------------------------------------------------------------- +class LineSplitter: + """A file-like that feeds each ``\\n``/``\\r``-terminated line to a sink. + + Carriage-return progress (git/tqdm) is treated as a line terminator, + so the sink sees each progress update immediately and the last full + line always reflects the latest state. ``flush()`` emits the + unterminated tail; ``isatty()`` is False. Both the task view's + console-mirror writer and the run view's log appender build on it, + so their line-splitting cannot drift apart. + """ + + def __init__(self, sink: Callable[[str], None]): + self._sink = sink + self._buffer = "" + + def write(self, text: str) -> int: + if not text: + return 0 + self._buffer += text + while True: + cut = min((cut for cut in (self._buffer.find("\n"), + self._buffer.find("\r")) + if cut >= 0), default=-1) + if cut < 0: + break + line, self._buffer = self._buffer[:cut], self._buffer[cut + 1:] + if line: + self._sink(line) + return len(text) + + def flush(self) -> None: + if self._buffer: + self._sink(self._buffer) + self._buffer = "" + + def isatty(self) -> bool: + return False + + def _text(scr, theme, y, x, text, attr) -> None: """addstr wrapper that ignores out-of-bounds errors.""" try: @@ -188,23 +241,28 @@ def _text(scr, theme, y, x, text, attr) -> None: pass -def _box(scr, curses, theme, height, width) -> None: - """Draw the full-screen frame.""" +def _rect_box(scr, curses, theme, x: int, y: int, w: int, h: int) -> None: + """Draw a box around the rectangle ``(x, y, w, h)``.""" border = theme["border"] try: - scr.addch(0, 0, curses.ACS_ULCORNER, border) - scr.addch(0, width - 1, curses.ACS_URCORNER, border) - scr.addch(height - 1, 0, curses.ACS_LLCORNER, border) - scr.addch(height - 1, width - 1, curses.ACS_LRCORNER, border) - scr.hline(0, 1, curses.ACS_HLINE, width - 2, border) - scr.hline(height - 1, 1, curses.ACS_HLINE, width - 2, border) - for y in range(1, height - 1): - scr.addch(y, 0, curses.ACS_VLINE, border) - scr.addch(y, width - 1, curses.ACS_VLINE, border) + scr.addch(y, x, curses.ACS_ULCORNER, border) + scr.addch(y, x + w - 1, curses.ACS_URCORNER, border) + scr.addch(y + h - 1, x, curses.ACS_LLCORNER, border) + scr.addch(y + h - 1, x + w - 1, curses.ACS_LRCORNER, border) + scr.hline(y, x + 1, curses.ACS_HLINE, w - 2, border) + scr.hline(y + h - 1, x + 1, curses.ACS_HLINE, w - 2, border) + for yy in range(y + 1, y + h - 1): + scr.addch(yy, x, curses.ACS_VLINE, border) + scr.addch(yy, x + w - 1, curses.ACS_VLINE, border) except Exception: pass +def _box(scr, curses, theme, height, width) -> None: + """Draw the full-screen frame.""" + _rect_box(scr, curses, theme, 0, 0, width, height) + + def _sep(scr, curses, theme, y, width) -> None: """A horizontal separator line inside the frame.""" try: |
