From 4c3ba1b68107208fe32c223588576a7bdca19a12 Mon Sep 17 00:00:00 2001 From: historia Date: Thu, 27 Aug 2026 19:44:30 -0400 Subject: feat: nest start/stop server menu in configure backend menu --- app/tests/test_hub.py | 70 ++++++++++++++++++++++++++++----------------------- app/ui/hub.py | 26 +++++++++---------- 2 files changed, 51 insertions(+), 45 deletions(-) (limited to 'app') diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 3550f5b..daa4abd 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -195,7 +195,7 @@ class HubMenuTests(unittest.TestCase): self.assertEqual(labels, ["Configure Backends", "Settings", "Help", "Quit"]) - def test_menu_has_all_five_when_one_installed(self): + def test_menu_lists_convert_when_one_installed(self): captured = {} def fake_menu(stdscr, title, options, **kwargs): @@ -212,8 +212,8 @@ class HubMenuTests(unittest.TestCase): labels = [label for label, _ in captured["options"]] self.assertEqual( labels, - ["Generate Audiobooks", "Configure Backends", - "Start/Stop Backend Servers", "Settings", "Help", "Quit"]) + ["Generate Audiobooks", "Configure Backends", "Settings", + "Help", "Quit"]) # The status table is passed through, one row per backend. self.assertEqual(captured["rows"], [("qwen-tts", "installed", "ok", "body")]) @@ -241,9 +241,10 @@ class HubMenuTests(unittest.TestCase): [("audio.cpp", "unavailable", "err", "dim"), ("qwen-tts", "running [remote]", "ok", "body")]) - def test_menu_hides_server_when_only_running(self): + def test_server_menu_hidden_from_main_when_only_running(self): # Running but not installed (an external server) still unlocks - # Convert — but Start/Stop needs the backend on this machine. + # Convert — but Start/Stop needs the backend on this machine, and + # lives in Configure Backends. captured = {} def fake_menu(stdscr, title, options, **kwargs): @@ -297,7 +298,7 @@ class HubMenuTests(unittest.TestCase): # Installed-but-not-ready backends → Convert is offered, but the # convert flow has nothing to list: it flashes a hint (no "Configure # Backends" detour anymore) and returns to the main menu. Then - # quit: 6 main-menu options, Quit is the 6th (Down x5). + # quit: 5 main-menu options, Quit is the 5th (Down x4). from backends import BackendStatus statuses = [BackendStatus("audiocpp", "audio.cpp", installed=True, configured=False), @@ -312,11 +313,10 @@ class HubMenuTests(unittest.TestCase): with patch.object(hub, "detect_all", return_value=statuses), \ patch.object(hub.tui, "flash", fake_flash): - # Convert(Enter) → flash → main menu; Down x5 -> Quit, Enter. + # Convert(Enter) → flash → main menu; Down x4 -> Quit, Enter. screen = FakeScreen(keys=[10, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, - FakeCurses.KEY_DOWN, 10]) result = hub._Hub(screen).run() self.assertIsNone(result) @@ -430,13 +430,14 @@ class SubmenuStatusTableTests(unittest.TestCase): return_value="/usr/bin/ffmpeg"): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) - # Install (faster uninstalled), then Uninstall (qwen installed); - # no audio.cpp means no model actions. qwen has no Configure entry - # (its wizard asks nothing to configure). + # Start/Stop (qwen installed), then Install (faster uninstalled), + # then Uninstall; no audio.cpp means no model actions. qwen has no + # Configure entry (its wizard asks nothing to configure). self.assertEqual([label for label, _ in captured["options"]], - ["Install Backend", "Uninstall Backend"]) - # ...the shared status table carries the states instead. - self.assertEqual(captured["table_title"], "Backend Status") + ["Start/Stop Backend Servers", "Install Backend", + "Uninstall Backend"]) + # ...the shared status table carries the states instead (no title). + self.assertIsNone(captured.get("table_title")) self.assertEqual( captured["table_rows"], [("qwen-tts", "installed", "ok", "body"), @@ -480,10 +481,11 @@ class SubmenuStatusTableTests(unittest.TestCase): self.assertIs(result, tui.Wizard.BACK) # Update sits between Install and Uninstall; it is offered once for # the whole set of installed backends (faster has nothing on disk - # and so contributes nothing). + # and so contributes nothing). Start/Stop heads the tail actions + # (qwen is installed). self.assertEqual([label for label, _ in captured["options"]], - ["Install Backend", "Update Backends", - "Uninstall Backend"]) + ["Start/Stop Backend Servers", "Install Backend", + "Update Backends", "Uninstall Backend"]) def test_configure_backends_menu_audiocpp_model_actions(self): captured = {} @@ -513,13 +515,13 @@ class SubmenuStatusTableTests(unittest.TestCase): labels = self._labels(captured["options"]) # The missing-model download heads the menu as the recommended next # step (yellow suffix), separated from the rest by a blank line; - # Configure + Uninstall follow. The backend is built, so no "Build" - # action is offered. Deleting unused models now lives inside the - # "Configure audio.cpp" wizard, not here. + # Start/Stop + Configure + Uninstall follow. The backend is built, + # so no "Build" action is offered. Deleting unused models now lives + # inside the "Configure audio.cpp" wizard, not here. self.assertEqual( labels, ["Download Missing Models (audio.cpp)", "Configure audio.cpp", - "Uninstall Backend"]) + "Start/Stop Backend Servers", "Uninstall Backend"]) self.assertEqual(captured["options"][0], ("Download Missing Models (audio.cpp)", "download_models", ("[recommended]", "warn"))) @@ -587,7 +589,8 @@ class SubmenuStatusTableTests(unittest.TestCase): captured = {} infos = [BackendInfo("audiocpp", "audio.cpp", lambda: None, lambda: 0)] # Built but no server.json: only Configure (the next step) plus - # Uninstall — no Build, no Download, no Install entry. + # Start/Stop and Uninstall — no Build, no Download, no Install + # entry. statuses = [BackendStatus("audiocpp", "audio.cpp", installed=True, configured=False)] with tempfile.TemporaryDirectory() as td: @@ -605,7 +608,8 @@ class SubmenuStatusTableTests(unittest.TestCase): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) self.assertEqual(self._labels(captured["options"]), - ["Configure audio.cpp", "Uninstall Backend"]) + ["Configure audio.cpp", "Start/Stop Backend Servers", + "Uninstall Backend"]) def test_configure_menu_offers_qwens_per_model_manager(self): # qwen ships a dedicated configure screen (per-model weight @@ -626,7 +630,8 @@ class SubmenuStatusTableTests(unittest.TestCase): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) self.assertEqual(self._labels(captured["options"]), - ["Configure qwen-tts", "Uninstall Backend"]) + ["Configure qwen-tts", "Start/Stop Backend Servers", + "Uninstall Backend"]) self.assertEqual(captured["table_rows"], [("qwen-tts", "installed", "ok", "body")]) @@ -682,7 +687,7 @@ class SubmenuStatusTableTests(unittest.TestCase): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) self.assertEqual(self._labels(captured["options"]), - ["Uninstall Backend"]) + ["Start/Stop Backend Servers", "Uninstall Backend"]) def test_convert_menu_builds_one_form_with_backend_field(self): captured = {} @@ -738,7 +743,7 @@ class SubmenuStatusTableTests(unittest.TestCase): patch.object(hub.shutil, "which", return_value="/x"): result = hub._Hub(None).screen_configure() self.assertIs(result, tui.Wizard.BACK) - self.assertEqual(captured["table_title"], "Backend Status") + self.assertIsNone(captured.get("table_title")) self.assertEqual( captured["table_rows"], [("qwen-tts", "installed", "ok", "body")]) @@ -767,7 +772,7 @@ class SubmenuStatusTableTests(unittest.TestCase): ["audio.cpp"]) # The running/stopped state lives in the status table above the # menu (not on the entries, whose colors the selection bar covers). - self.assertEqual(captured["table_title"], "Server Status") + self.assertIsNone(captured.get("table_title")) self.assertEqual(captured["table_rows"], [("audio.cpp", "stopped", "err", "body")]) @@ -3243,7 +3248,8 @@ class HubNavigationTests(unittest.TestCase): configured=True, servers=specs) registry = [self._info("qwen", "qwen-tts")] titles = [] - script = ["server", specs[0], tui.Wizard.BACK, tui.Wizard.BACK] + script = ["configure_backends", "server", specs[0], + tui.Wizard.BACK, tui.Wizard.BACK, tui.Wizard.BACK] def menu(stdscr, title, options, **kwargs): titles.append(title) @@ -3260,11 +3266,13 @@ class HubNavigationTests(unittest.TestCase): return_value=None): hub._Hub(None).run() # Selecting a server toggles it directly (no action sub-menu), then - # Esc steps back one screen at a time to the server list and main. + # Esc steps back one screen at a time: server list → Configure + # Backends → main menu. self.assertEqual( titles, - ["tts-audiobook-generator", "Start / Stop A Server", - "Start / Stop A Server", "tts-audiobook-generator"]) + ["tts-audiobook-generator", "Configure Backends", + "Start / Stop A Server", "Start / Stop A Server", + "Configure Backends", "tts-audiobook-generator"]) def test_esc_on_main_menu_quits(self): titles = self._drive([tui.Wizard.BACK], [], []) diff --git a/app/ui/hub.py b/app/ui/hub.py index e99a2e1..68417f7 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -120,19 +120,16 @@ class _Hub: statuses = detect_all() options = [("Configure Backends", "configure_backends")] # Converting works against an external (remote) server too, but - # configuring one and starting/stopping its servers need it on - # this machine. + # configuring one needs it on this machine. if any(st.installed or st.running for st in statuses): options.insert(0, ("Generate Audiobooks", "convert")) - if any(st.installed for st in statuses): - options.append(("Start/Stop Backend Servers", "server")) options.append(("Settings", "settings")) options.append(("Help", "help")) options.append(("Quit", "quit")) choice = tui.menu( self.stdscr, "tts-audiobook-generator", options, back_value=tui.Wizard.BACK, - table_title="Backend Status", table_rows=_status_rows(statuses), + table_rows=_status_rows(statuses), notice_lines=_notice_lines()) if choice is tui.Wizard.BACK or choice == "quit": return None @@ -140,8 +137,6 @@ class _Hub: return self.screen_convert if choice == "configure_backends": return self.screen_configure - if choice == "server": - return self.screen_server if choice == "help": return self.screen_help return self.screen_settings @@ -158,9 +153,9 @@ class _Hub: separated from the rest by a blank line. The remaining actions are populated from the detected statuses: configure each configurable backend (qwen offers its per-model weight (un)installer there), - install (backends with nothing on disk), update (every installed - backend refreshed to the latest upstream version in one task-view - run), and uninstall. + start/stop the installed backends' local servers, install (backends + with nothing on disk), update (every installed backend refreshed to + the latest upstream version in one task-view run), and uninstall. Selecting one pushes the next screen; Esc pops back to the main menu. The Build action downloads any missing models alongside the build (a split view), so it heals a configured-but-unbuilt backend @@ -206,6 +201,8 @@ class _Hub: options += [(f"Configure {info.label}", ("configure", info.key)) for info in installed if _configurable(info)] + if any(st.installed for st in statuses): + options.append(("Start/Stop Backend Servers", "server")) if any(_installable(info, by_key) for info in REGISTRY): options.append(("Install Backend", "install")) if any(_updatable(info, by_key) for info in REGISTRY): @@ -218,13 +215,14 @@ class _Hub: back_value=tui.Wizard.BACK, help_lines=["Install, update, configure, or remove a TTS " "backend."], - table_title="Backend Status", table_rows=_status_rows(statuses), notice_lines=_notice_lines()) if choice is tui.Wizard.BACK: return tui.Wizard.BACK if choice == "install": return self.screen_install + if choice == "server": + return self.screen_server if choice == "update": _update_backends_action(self.stdscr) invalidate_detect_cache() @@ -361,7 +359,6 @@ class _Hub: title = "Uninstall Backend" if installed_only else "Install Backend" key = tui.menu(self.stdscr, title, options, back_value=tui.Wizard.BACK, - table_title="Backend Status", table_rows=_status_rows(statuses), notice_lines=_notice_lines()) if key is tui.Wizard.BACK: @@ -527,8 +524,9 @@ class _Hub: return tui.Wizard.BACK spec = tui.menu(self.stdscr, "Start / Stop A Server", options, back_value=tui.Wizard.BACK, - help_lines=["Start/stop local servers manually."], - table_title="Server Status", + help_lines=["Start/stop local servers manually.", + "'Generate Audiobooks' handles this " + "automatically."], table_rows=rows, notice_lines=_notice_lines()) if spec is tui.Wizard.BACK: -- cgit v1.2.3