aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-02 17:16:05 -0400
committerhistoria <historiavg@proton.me>2026-09-02 17:16:05 -0400
commit717a3dc112519ae7cf39a212a4e3ec9e1ba17f28 (patch)
tree119ad113ddce14414ae3293316cf7ed8c57cd5d3 /app/ui/hub.py
parent8579517a35ef1865fc9b428899d73d52dcb27a14 (diff)
downloadtts-audiobook-generator-717a3dc112519ae7cf39a212a4e3ec9e1ba17f28.tar.gz
feat: auto-update via settings
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py70
1 files changed, 68 insertions, 2 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index c003b16..67680db 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -30,6 +30,7 @@ from pathlib import Path
from typing import Callable, Optional, Tuple
import logging_kit
+import selfupdate
from backends import (
REGISTRY,
@@ -170,7 +171,9 @@ class _Hub:
backend (qwen offers its per-model weight (un)installer there),
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.
+ the latest upstream version in one task-view run), update the
+ generator itself (git checkout moved to the remote's HEAD, keeping
+ locally modified files such as config.py), 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
@@ -222,6 +225,8 @@ class _Hub:
options.append(("Install Backend", "install"))
if any(_updatable(info, by_key) for info in REGISTRY):
options.append(("Update Backends", "update"))
+ if selfupdate.is_git_checkout():
+ options.append(("Update Generator", "update_self"))
if any(_uninstallable(info, by_key) for info in REGISTRY):
options.append(("Uninstall Backend", "uninstall"))
@@ -229,7 +234,8 @@ class _Hub:
self.stdscr, "Configure Backends", options,
back_value=tui.Wizard.BACK,
help_lines=["Install, update, configure, or remove a TTS "
- "backend."],
+ "backend. 'Update Generator' refreshes this "
+ "tool's own checkout instead."],
table_rows=_status_rows(statuses),
notice_lines=_notice_lines())
if choice is tui.Wizard.BACK:
@@ -242,6 +248,10 @@ class _Hub:
_update_backends_action(self.stdscr)
invalidate_detect_cache()
continue # an inline action: re-show this same menu
+ if choice == "update_self":
+ _update_self_action(self.stdscr)
+ invalidate_detect_cache()
+ continue # an inline action: re-show this same menu
if choice == "uninstall":
return self.screen_uninstall
if choice == "download_models":
@@ -759,6 +769,62 @@ def _update_backends_action(stdscr) -> None:
"Backends' to retry.", "err")
+def _update_self_action(stdscr) -> None:
+ """Run the "Update Generator" action inside the TUI.
+
+ Moves the generator's own git checkout to the remote's default-branch
+ HEAD (fetch + hard reset — the same flow the backend checkouts use).
+ User state cannot be overwritten: everything untracked (app/envs,
+ input/output/voices, the backend checkouts, logs) is outside the
+ reset's reach, and locally modified tracked files — the user-editable
+ app/converter/config.py above all — are snapshotted before the reset
+ and written back afterwards (a kept file whose upstream version
+ changed is reported for a manual merge). A dirty checkout confirms
+ first (declining, Esc included, aborts before anything runs); the run
+ itself streams in the task view like the other inline actions. After:
+ a successful run flashes the moved commit range (or "already up to
+ date") plus the restart reminder — the fresh code loads on relaunch,
+ where requirements re-install runs automatically — while cancel and
+ failure flash their usual warn/err. The menu re-shows either way.
+ """
+ if not selfupdate.is_git_checkout():
+ tui.flash(stdscr, "This install is not a git checkout — update by "
+ "re-cloning the repository.", "err")
+ return
+ before = selfupdate.current_commit()
+ modified = selfupdate.modified_tracked_files()
+ if modified:
+ body = ["These locally modified files are kept as they are:",
+ ""]
+ body += modified[:6]
+ if len(modified) > 6:
+ body.append(f"...and {len(modified) - 6} more")
+ if tui.confirm(stdscr, "Update the generator?", body=body,
+ default=False, cancel_value=False) is not True:
+ return
+
+ def work(emit, cancel):
+ return selfupdate.update_generator(emit=emit, cancel=cancel)
+
+ rc = taskview.run_steps(
+ stdscr, "Update Generator",
+ [taskview.TaskStep("Fetch and reset the checkout", work)])
+ after = selfupdate.current_commit()
+ if rc == 130:
+ tui.flash(stdscr, "Update cancelled — re-run 'Update Generator' "
+ "any time.", "warn")
+ elif rc:
+ tui.flash(stdscr, "The generator update did not complete — see the "
+ "log above and retry.", "err")
+ elif before is not None and before == after:
+ tui.flash(stdscr, f"The generator is already up to date ({before}).",
+ "ok")
+ else:
+ tui.flash(stdscr, f"Generator updated {before or '?'} → {after}. "
+ "Restart to apply — requirements re-install runs "
+ "automatically on next launch.", "ok")
+
+
def _status_mark(status: Optional[BackendStatus]) -> Tuple[str, str, str]:
"""Map a backend's state to (status_text, status_kind, name_kind).