aboutsummaryrefslogtreecommitdiff
path: root/app/ui/viewkit.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
committerhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
commit6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch)
tree55321760a8103bc6b5d79489fac4135a60e6e3ba /app/ui/viewkit.py
parentdc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff)
downloadtts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz
slop refactor/dedup
Diffstat (limited to 'app/ui/viewkit.py')
-rw-r--r--app/ui/viewkit.py86
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: