diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | app/tests/test_hub.py | 31 | ||||
| -rw-r--r-- | app/tests/test_tui.py | 19 | ||||
| -rw-r--r-- | app/ui/hub.py | 7 | ||||
| -rw-r--r-- | app/ui/tui.py | 14 |
5 files changed, 70 insertions, 3 deletions
@@ -18,7 +18,7 @@ The converter sends text extracted from your books to a locally running TTS serv - Python 3.12+ - ffmpeg - +## Quick Start ```bash git clone https://git.historia.vg/git/tts-audiobook-generator diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index b3a17e8..749d2e6 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -160,6 +160,37 @@ class HubMenuTests(unittest.TestCase): ["Convert books...", "Set up a backend...", "Configure a backend...", "Server...", "Settings...", "Quit"]) + def test_ffmpeg_warning_shown_when_missing(self): + # ffmpeg not on PATH → a red notice is passed above the table. + captured = {} + + def fake_menu(stdscr, title, options, **kwargs): + captured["notice_lines"] = kwargs.get("notice_lines") + return "quit" + + screen = FakeScreen() + with patch.object(hub.tui, "menu", fake_menu), \ + patch.object(hub, "detect_all", return_value=[]), \ + patch.object(hub.shutil, "which", return_value=None): + hub._hub_menu(screen) + self.assertEqual(captured["notice_lines"], + [("Warning: ffmpeg not installed!", "err")]) + + def test_ffmpeg_warning_hidden_when_installed(self): + # ffmpeg on PATH → no notice is passed at all. + captured = {} + + def fake_menu(stdscr, title, options, **kwargs): + captured["notice_lines"] = kwargs.get("notice_lines") + return "quit" + + screen = FakeScreen() + with patch.object(hub.tui, "menu", fake_menu), \ + patch.object(hub, "detect_all", return_value=[]), \ + patch.object(hub.shutil, "which", return_value="/usr/bin/ffmpeg"): + hub._hub_menu(screen) + self.assertIsNone(captured["notice_lines"]) + def test_convert_with_no_available_backend_offers_setup(self): # One installed-but-not-ready backend → Convert is offered. The # convert menu lists no available backend, so only "Set up a diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py index c121d55..c87374f 100644 --- a/app/tests/test_tui.py +++ b/app/tests/test_tui.py @@ -300,6 +300,25 @@ class MenuTableTests(TuiTestCase): table_title="Backend status", table_rows=self.ROWS) self.assert_inside_border(screen) + def test_notice_line_is_red_and_above_the_table(self): + screen = FakeScreen(keys=[10]) + tui.menu(screen, "Hub", [("Quit", "quit")], + table_title="Backend status", table_rows=self.ROWS, + notice_lines=[("Warning: ffmpeg not installed!", "err")]) + x0, _ = self.dialog_box(screen) + margin = x0 + 1 + tui.Frame.LIST_MARGIN + x, attr = next((x, a) for _, x, text, a in screen.strings + if text == "Warning: ffmpeg not installed!") + self.assertEqual(x, margin) + self.assertEqual(attr, tui._THEME["err"]) + # The notice sits above the table title ("Backend status"). + notice_y = next(y for y, _, text, _ in screen.strings + if text == "Warning: ffmpeg not installed!") + title_y = next(y for y, _, text, _ in screen.strings + if text == "Backend status") + self.assertLess(notice_y, title_y) + self.assert_inside_border(screen) + class ConfirmTests(TuiTestCase): def test_tab_switches_and_enter_activates(self): diff --git a/app/ui/hub.py b/app/ui/hub.py index 6a94096..5b18869 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -14,6 +14,7 @@ main menu. import json import re +import shutil from pathlib import Path from typing import Optional, Tuple @@ -84,9 +85,13 @@ def _hub_menu(stdscr) -> Optional[tuple]: options.append(("Settings...", "settings")) options.append(("Quit", "quit")) rows = [(st.label, *_status_mark(st)) for st in statuses] + notice_lines = None + if shutil.which("ffmpeg") is None: + notice_lines = [("Warning: ffmpeg not installed!", "err")] choice = tui.menu( stdscr, "tts-audiobook-generator", options, - table_title="Backend status", table_rows=rows) + table_title="Backend status", table_rows=rows, + notice_lines=notice_lines) if choice is None or choice == "quit": return None if choice == "convert": diff --git a/app/ui/tui.py b/app/ui/tui.py index 9047f36..c3d79f6 100644 --- a/app/ui/tui.py +++ b/app/ui/tui.py @@ -611,7 +611,8 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0, help_lines: Optional[Sequence[str]] = None, back_value: object = None, table_title: Optional[str] = None, - table_rows: Optional[Sequence[tuple]] = None): + table_rows: Optional[Sequence[tuple]] = None, + notice_lines: Optional[Sequence[Tuple[str, str]]] = None): """Show OPTIONS as (label, value) pairs; return the chosen value. The cursor starts on DEFAULT_INDEX; Enter returns the highlighted @@ -628,6 +629,11 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0, hub to show each backend's state (unavailable / installed / running) in matching columns with color. + NOTICE_LINES render above the table (and after HELP_LINES): each + entry is (text, kind) where KIND is a theme key, so the hub can warn + in red (e.g. "Warning: ffmpeg not installed!") without polluting the + status table. + Esc (or 'q') aborts the wizard unless BACK_VALUE is given (not None), in which case Esc returns it so the caller can fall back a screen. """ @@ -642,6 +648,12 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0, frame.mark(line, frame.theme["dim"]) if help_lines: frame.mark("") + if notice_lines: + for text, kind in notice_lines: + frame.mark(text, + frame.theme.get(kind, frame.theme["body"]), + align="left") + frame.mark("") if table_rows: if table_title: frame.mark(table_title, frame.theme["dim"], align="left") |
