aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 05:04:09 -0400
committerhistoria <historiavg@proton.me>2026-08-24 05:04:09 -0400
commit4f3d16e9d532efd6a6bda283925efeb8c93b51c7 (patch)
treebd05c90a6fa47ee17bc521fde5519d19bab6a3d0
parent0512a932bbf3b87619b6a250f87eb7904d22d1f7 (diff)
downloadtts-audiobook-generator-4f3d16e9d532efd6a6bda283925efeb8c93b51c7.tar.gz
feat: q behavior mirrors esc, port settings reordered
-rw-r--r--app/tests/test_hub.py8
-rw-r--r--app/tests/test_tui.py31
-rw-r--r--app/ui/hub.py20
-rw-r--r--app/ui/tui.py29
4 files changed, 51 insertions, 37 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index 61f0da1..a009e3c 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -646,8 +646,8 @@ class SettingsTests(unittest.TestCase):
hub._settings_menu(None)
self.assertEqual([f["key"] for f in captured["fields"]],
["audio_format", "audio_bitrate", "language",
- "chunk_size", "qwen_custom_port", "qwen_clone_port",
- "faster_port", "audiocpp_port"])
+ "chunk_size", "audiocpp_port", "faster_port",
+ "qwen_custom_port", "qwen_clone_port"])
kinds = {f["key"]: f["kind"] for f in captured["fields"]}
self.assertEqual(kinds["audio_format"], "choice")
self.assertEqual(kinds["audio_bitrate"], "text")
@@ -658,9 +658,9 @@ class SettingsTests(unittest.TestCase):
# The ports section note hangs off the first port field so it
# renders between the output settings and the ports.
notes = {f["key"]: f.get("note") for f in captured["fields"]}
- self.assertTrue(notes["qwen_custom_port"])
+ self.assertTrue(notes["audiocpp_port"])
self.assertIsNone(notes["audio_format"])
- self.assertIsNone(notes["audiocpp_port"])
+ self.assertIsNone(notes["qwen_custom_port"])
self.assertEqual(applied, [{"audio_format": "ogg",
"audio_bitrate": "192k",
"language": "English",
diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py
index e7fd976..d121c9a 100644
--- a/app/tests/test_tui.py
+++ b/app/tests/test_tui.py
@@ -215,11 +215,17 @@ class MenuTests(TuiTestCase):
tui.menu(screen, "Pick", self.OPTIONS, back_value=marker),
marker)
- def test_q_still_aborts_with_back_value(self):
+ def test_q_returns_back_value_like_esc(self):
marker = object()
screen = FakeScreen(keys=[ord("q")])
+ self.assertIs(
+ tui.menu(screen, "Pick", self.OPTIONS, back_value=marker),
+ marker)
+
+ def test_q_aborts_without_back_value(self):
+ screen = FakeScreen(keys=[ord("q")])
with self.assertRaises(tui.WizardCancelled):
- tui.menu(screen, "Pick", self.OPTIONS, back_value=marker)
+ tui.menu(screen, "Pick", self.OPTIONS)
class MenuTableTests(TuiTestCase):
@@ -459,11 +465,12 @@ class FormTests(TuiTestCase):
tui.form(screen, "Settings", self._fields(), back_value=marker),
marker)
- def test_q_still_aborts_with_back_value(self):
+ def test_q_returns_back_value_like_esc(self):
marker = object()
screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
- tui.form(screen, "Settings", self._fields(), back_value=marker)
+ self.assertIs(
+ tui.form(screen, "Settings", self._fields(), back_value=marker),
+ marker)
def test_choice_field_picks_another_value(self):
# Enter opens the choice menu on 'm4b' (index 1); Down moves to
@@ -646,12 +653,13 @@ class BrowseDirectoryTests(TuiTestCase):
back_value=marker),
marker)
- def test_q_still_aborts_with_back_value(self):
+ def test_q_returns_back_value_like_esc(self):
marker = object()
screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
+ self.assertIs(
tui.browse_directory(screen, "Pick", start=self.root,
- back_value=marker)
+ back_value=marker),
+ marker)
class CheckboxTreeTests(TuiTestCase):
@@ -717,12 +725,13 @@ class CheckboxTreeTests(TuiTestCase):
back_value=marker),
marker)
- def test_q_still_aborts_with_back_value(self):
+ def test_q_returns_back_value_like_esc(self):
marker = object()
screen = FakeScreen(keys=[ord("q")])
- with self.assertRaises(tui.WizardCancelled):
+ self.assertIs(
tui.checkbox_tree(screen, "Pick models", self.FAMILIES,
- back_value=marker)
+ back_value=marker),
+ marker)
class SuspendTests(TuiTestCase):
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 5f3d039..ef396e5 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -8,8 +8,8 @@ Each backend's setup wizard runs in its own curses session, so the hub
collects a "command" inside its own wrapper, returns to the plain terminal,
and then dispatches — no nested curses sessions.
-Esc on the main menu quits the hub. Esc inside a sub-menu falls back to the
-main menu.
+Esc on the main menu quits the hub ('q' mirrors Esc on every screen).
+Esc inside a sub-menu falls back to the main menu.
"""
import json
@@ -426,23 +426,23 @@ def _settings_menu(stdscr) -> None:
"value": config.LANGUAGE, "validate": _validate_language},
{"key": "chunk_size", "label": "Chunk size (words)", "kind": "text",
"value": str(config.CHUNK_SIZE), "validate": _validate_chunk_size},
- {"key": "qwen_custom_port", "label": "qwen-tts CustomVoice port",
+ {"key": "audiocpp_port", "label": "audio.cpp port",
"kind": "text",
- "value": str(_port_from_url(config.QWEN_API_URL, 7860)),
+ "value": str(_port_from_url(config.AUDIOCPP_API_URL, 8080)),
"validate": _validate_port,
"note": "Ports apply to servers this tool starts and detecting "
"local servers"},
- {"key": "qwen_clone_port", "label": "qwen-tts Base port",
- "kind": "text",
- "value": str(_port_from_url(config.CLONE_API_URL, 7861)),
- "validate": _validate_port},
{"key": "faster_port", "label": "faster-qwen3-tts port",
"kind": "text",
"value": str(_port_from_url(config.FASTER_API_URL, 8000)),
"validate": _validate_port},
- {"key": "audiocpp_port", "label": "audio.cpp port",
+ {"key": "qwen_custom_port", "label": "qwen-tts CustomVoice port",
"kind": "text",
- "value": str(_port_from_url(config.AUDIOCPP_API_URL, 8080)),
+ "value": str(_port_from_url(config.QWEN_API_URL, 7860)),
+ "validate": _validate_port},
+ {"key": "qwen_clone_port", "label": "qwen-tts Base port",
+ "kind": "text",
+ "value": str(_port_from_url(config.CLONE_API_URL, 7861)),
"validate": _validate_port},
]
result = tui.form(stdscr, "Settings", fields, back_value=_GO_BACK)
diff --git a/app/ui/tui.py b/app/ui/tui.py
index bf17f7d..6d768dd 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -24,9 +24,10 @@ Common key bindings:
instead, so the caller can fall back a screen
(confirm() historically names this cancel_value)
-On screens without typed text (menus, confirm, tree, browser) 'q' also
-aborts — even when a back_value is set, so Esc means "back" while 'q'
-still means "quit". Inside text editors 'q' is an ordinary character.
+On screens without typed text (menus, confirm, tree, browser) 'q'
+behaves exactly like Esc: it goes back when a back/cancel value is set,
+otherwise it aborts the wizard. Inside text editors 'q' is an ordinary
+character.
When the terminal has no color support the theme degrades to
bold/reverse/dim.
"""
@@ -93,7 +94,8 @@ def flash(scr, text: str, kind: str = "warn") -> None:
raise WizardCancelled()
-# Esc and 'q' both abort on screens without typed text ('q' is an
+# On screens without typed text, Esc and 'q' mean the same thing: go
+# back when a back/cancel value is set, abort otherwise ('q' is an
# ordinary character inside text editors).
_CANCEL_KEYS = (27, ord("q"))
@@ -637,7 +639,8 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0,
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.
+ in which case either key returns it so the caller can fall back a
+ screen.
"""
if not options:
raise ValueError("menu() needs at least one option")
@@ -676,7 +679,7 @@ def menu(scr, title: str, options: Sequence[tuple], default_index: int = 0,
frame.cursor = base + cursor
frame.draw()
key = frame.get_key(cancel_keys=())
- if key == 27 and back_value is not None:
+ if key in _CANCEL_KEYS and back_value is not None:
return back_value
if key in _CANCEL_KEYS:
raise WizardCancelled()
@@ -720,6 +723,7 @@ def line_edit(scr, title: str, default: str,
frame.draw()
curses = frame.curses
key = frame.get_key(cancel_keys=()) # handle Esc manually below
+ # 'q' is an ordinary character in a text editor; only Esc cancels.
if key == 27 and back_value is not None:
return back_value
if key == 27:
@@ -807,7 +811,7 @@ def form(scr, title: str, fields: Sequence[dict],
frame.draw()
curses = frame.curses
key = frame.get_key(cancel_keys=())
- if key == 27 and back_value is not None:
+ if key in _CANCEL_KEYS and back_value is not None:
return back_value
if key in _CANCEL_KEYS:
raise WizardCancelled()
@@ -921,8 +925,8 @@ def browse_directory(scr, title: str,
lets a subdirectory that already looks like the target (e.g. an
'audio.cpp' checkout containing 'model_specs/') be picked in one
keystroke. 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.
+ (not None), in which case either key returns it so the caller can
+ fall back a screen.
"""
footer = ("Up/Down = move Enter = open/use Left = parent "
"e = type path Esc = cancel")
@@ -993,7 +997,7 @@ def browse_directory(scr, title: str,
frame.draw()
curses = frame.curses
key = frame.get_key(cancel_keys=())
- if key == 27 and back_value is not None:
+ if key in _CANCEL_KEYS and back_value is not None:
return back_value
if key in _CANCEL_KEYS:
raise WizardCancelled()
@@ -1072,7 +1076,8 @@ def checkbox_tree(scr, title: str, families: List[dict],
family has more than one option — a single option needs no tag.
Family and option rows are left-justified like a DOS list. 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.
+ which case either key returns it so the caller can fall back a
+ screen.
"""
if not families:
raise ValueError("checkbox_tree() needs at least one family")
@@ -1141,7 +1146,7 @@ def checkbox_tree(scr, title: str, families: List[dict],
frame.draw()
curses = frame.curses
key = frame.get_key(cancel_keys=())
- if key == 27 and back_value is not None:
+ if key in _CANCEL_KEYS and back_value is not None:
return back_value
if key in _CANCEL_KEYS:
raise WizardCancelled()