diff options
| author | historia <historiavg@proton.me> | 2026-08-25 14:37:16 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-25 14:37:16 -0400 |
| commit | 061406b608c5a77b33259506648e0166367b2b9f (patch) | |
| tree | c18633630d160016af285389a629da53a3a4161c /app/ui/hub.py | |
| parent | 0badc06550ed2e46c7c4b9f83db755737ffc0412 (diff) | |
| download | tts-audiobook-generator-061406b608c5a77b33259506648e0166367b2b9f.tar.gz | |
feat: simpler menu for start/stop backend servers
Diffstat (limited to 'app/ui/hub.py')
| -rw-r--r-- | app/ui/hub.py | 161 |
1 files changed, 120 insertions, 41 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py index 285a1cf..29edf9b 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -25,6 +25,7 @@ import io import json import re import shutil +import sys import urllib.parse from datetime import datetime from pathlib import Path @@ -366,59 +367,137 @@ class _Hub: # -- servers -------------------------------------------------------- def screen_server(self): + """One flat menu of every installed backend's servers. + + A status table above the menu shows each server's live state — + "running" (green) or "stopped" (red) — and selecting an entry + toggles it directly (starts a stopped server, stops a running one) + without an extra action menu. The state lives in the table, not on + the entries, because the menu's selection bar would cover inline + colors. A backend with a single server is labelled by its name; a + multi-server backend (qwen: CustomVoice + Base) gets one entry per + server, suffixed with the server name. + """ statuses = detect_all() candidates = [st for st in statuses if st.installed] if not candidates: tui.flash(self.stdscr, "No backend is installed yet — use " "'Configure backends' first.") return tui.Wizard.BACK - options = [(st.label, st.key) for st in candidates] - key = tui.menu(self.stdscr, "Start / Stop a server", 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: - return tui.Wizard.BACK - status = next((s for s in statuses if s.key == key), None) - if status is None: - return tui.Wizard.BACK - specs = status.servers - if not specs: - tui.flash(self.stdscr, f"{status.label} has no server " - "configured. Run 'Configure backends' first.") - return tui.Wizard.BACK - if len(specs) == 1: - return functools.partial(self._server_action, specs[0]) - return functools.partial(self._server_spec, status, specs) - - def _server_spec(self, status, specs): - options = [(f"{s.name} ({'running' if common.server_running(s.url) else 'stopped'})", - s.name) for s in specs] - name = tui.menu(self.stdscr, f"{status.label} server", options, - back_value=tui.Wizard.BACK) - if name is tui.Wizard.BACK: + options = [] + rows = [] + for st in candidates: + specs = st.servers + for spec in specs: + running = common.server_running(spec.url) + label = st.label if len(specs) == 1 \ + else f"{st.label} — {spec.name}" + options.append((label, spec)) + rows.append((label, + "running" if running else "stopped", + "ok" if running else "err", "body")) + if not options: + tui.flash(self.stdscr, "No backend server is configured yet — " + "use 'Configure backends' first.") return tui.Wizard.BACK - spec = next((s for s in specs if s.name == name), None) - if spec is None: + spec = tui.menu(self.stdscr, "Start / Stop a server", options, + back_value=tui.Wizard.BACK, + help_lines=["Select a server to start or stop it."], + table_title="Server status", + table_rows=rows, + notice_lines=_notice_lines()) + if spec is tui.Wizard.BACK: return tui.Wizard.BACK - return functools.partial(self._server_action, spec) + return functools.partial(self._server_toggle, spec) - def _server_action(self, spec): + def _server_toggle(self, spec): + """Start SPEC's server when stopped, stop it when running. + + Runs inside the task view (no console drop); the server module's + plain-console output is tee'd to a log file under ``app/logs`` so + nothing is lost, and on failure a flash points the user at that + file. Returns BACK so the stack lands back on the server list, + which re-reads each server's live state. + """ running = common.server_running(spec.url) - action = tui.menu( - self.stdscr, - f"{spec.name} ({'running' if running else 'stopped'})", - [("Start", "start"), ("Stop", "stop")], - back_value=tui.Wizard.BACK) - if action is tui.Wizard.BACK: - return tui.Wizard.BACK - with tui.suspend(self.stdscr): + action = "stop" if running else "start" + step, log_path = _server_action_step(spec, action) + taskview.run_steps(self.stdscr, f"{action.capitalize()} " + f"{spec.name} server", [step], + wait_on_finish=False) + # Re-check the server instead of trusting the step's exit code + # (cancel and failure both come back non-zero): did the toggle take? + now_running = common.server_running(spec.url) + if action == "start" and not now_running: + tui.flash(self.stdscr, f"Could not start the {spec.name} " + f"server. See the log: {log_path}", "err") + elif action == "stop" and now_running: + tui.flash(self.stdscr, f"Could not stop the {spec.name} " + f"server. See the log: {log_path}", "err") + return tui.Wizard.BACK + + +class _TeeWriter: + """A file-like that mirrors writes to a log file and an inner stream. + + Used to capture the server module's plain-console output (the task view + already redirects stdout to its line-writer) into a persistent log file + under ``servers.LOG_DIR`` without losing the on-screen log tail. + """ + + def __init__(self, logf, inner): + self._logf = logf + self._inner = inner + + def write(self, text): + if not text: + return 0 + try: + self._logf.write(text) + except OSError: + pass + try: + self._inner.write(text) + except OSError: + pass + return len(text) + + def flush(self): + try: + self._logf.flush() + except OSError: + pass + try: + self._inner.flush() + except OSError: + pass + + +def _server_action_step(spec, action: str): + """Build a task step that starts/stops SPEC's server, logged to a file. + + ACTION is "start" or "stop". The step runs inside the task view (no + console drop): the server module's output is tee'd to + ``<servers.LOG_DIR>/<name>-<action>.log`` and to the view's log tail. + Returns ``(TaskStep, log_path)`` so the caller can point the user at the + file on failure. + """ + log_path = servers.LOG_DIR / f"{spec.name}-{action}.log" + title = (f"Start {spec.name} server" if action == "start" + else f"Stop {spec.name} server") + + def work(emit, cancel): + servers.LOG_DIR.mkdir(parents=True, exist_ok=True) + inner = sys.stdout # the task view's line-writer, when run in TUI + with log_path.open("w", encoding="utf-8") as logf, \ + contextlib.redirect_stdout(_TeeWriter(logf, inner)): if action == "start": - servers.start(spec) + ok = servers.start(spec, cancel=cancel) else: - servers.stop(spec.name) - return tui.Wizard.BACK + ok = servers.stop(spec.name) + return 0 if ok else 1 + + return taskview.TaskStep(title, work), log_path def _installable(info, by_key: dict) -> bool: |
