From 4f3d16e9d532efd6a6bda283925efeb8c93b51c7 Mon Sep 17 00:00:00 2001 From: historia Date: Mon, 24 Aug 2026 05:04:09 -0400 Subject: feat: q behavior mirrors esc, port settings reordered --- app/tests/test_hub.py | 8 ++++---- app/tests/test_tui.py | 31 ++++++++++++++++++++----------- app/ui/hub.py | 20 ++++++++++---------- app/ui/tui.py | 29 +++++++++++++++++------------ 4 files changed, 51 insertions(+), 37 deletions(-) (limited to 'app') 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() -- cgit v1.2.3