aboutsummaryrefslogtreecommitdiff
path: root/app/ui
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 04:07:06 -0400
committerhistoria <historiavg@proton.me>2026-08-24 04:07:06 -0400
commit9fb2434dcc8a1ed7bd085b453859d473de64448a (patch)
tree8a5ebe4db5defe6c42a7e8adf84f9b7f6ce26db5 /app/ui
parent73b466fbc054e80b50e318b49643aee8a03c784b (diff)
downloadtts-audiobook-generator-9fb2434dcc8a1ed7bd085b453859d473de64448a.tar.gz
fix: menu tui up/down, port explanation in menu
Diffstat (limited to 'app/ui')
-rw-r--r--app/ui/hub.py4
-rw-r--r--app/ui/tui.py48
2 files changed, 35 insertions, 17 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 0e227da..c3efce6 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -403,7 +403,9 @@ def _settings_menu(stdscr) -> None:
{"key": "qwen_custom_port", "label": "qwen-tts CustomVoice port",
"kind": "text",
"value": str(_port_from_url(config.QWEN_API_URL, 7860)),
- "validate": _validate_port},
+ "validate": _validate_port,
+ "note": "Ports apply to servers this tool starts and detecting "
+ "local servers"},
{"key": "qwen_clone_port", "label": "qwen-tts Base (clone) port",
"kind": "text",
"value": str(_port_from_url(config.CLONE_API_URL, 7861)),
diff --git a/app/ui/tui.py b/app/ui/tui.py
index 12c7d4f..7b6c740 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -755,16 +755,22 @@ def form(scr, title: str, fields: Sequence[dict],
"kind": "text", "value": "250",
"validate": lambda s: None if s.isdigit() else "digits only"}
- Each field renders as a left-justified ``Label: value`` row. Up/Down
- (or k/j) move the cursor; Enter on a ``choice`` row opens a single
- choice menu, Enter on a ``text`` row opens a line editor (reusing its
- VALIDATE for that one field). Tab, Left/Right or Up/Down move focus to
- the Save/Cancel buttons — Up from the first field and Down from the
- last field step straight onto them; Enter on Save validates every text
- field (the first failure flashes in red and re-focuses that row) and
- returns ``{key: value}``, Enter on Cancel returns BACK_VALUE. Esc (or
- 'q') returns BACK_VALUE / aborts as in menu(). Values are edited in
- place in the FIELDS dicts, so Cancel simply discards them.
+ Fields render as a two-column table: each label is padded to the
+ widest label so every value starts in the same column. An optional
+ ``note`` string on a field renders as a dim, non-selectable line in
+ a blank-line frame above that field's row — a section divider with a
+ short explanation. Up/Down (or k/j) move the cursor; Enter on a
+ ``choice`` row opens a single choice menu, Enter on a ``text`` row
+ opens a line editor (reusing its VALIDATE for that one field). Tab,
+ Left/Right, j or k at the ends of the list move focus to the
+ Save/Cancel buttons — Down from the last field and Up from the first
+ field both land on Save (the fields wrap onto the buttons); on the
+ buttons, arrows/j/k/Tab return to the fields. Enter on Save validates
+ every text field (the first failure flashes in red and re-focuses
+ that row) and returns ``{key: value}``, Enter on Cancel returns
+ BACK_VALUE. Esc (or 'q') returns BACK_VALUE / aborts as in menu().
+ Values are edited in place in the FIELDS dicts, so Cancel simply
+ discards them.
"""
if not fields:
raise ValueError("form() needs at least one field")
@@ -775,17 +781,26 @@ def form(scr, title: str, fields: Sequence[dict],
on_buttons = False
btn_index = 0
edit_cancel = object() # sentinel: backed out of a field editor
+ label_w = max(len(field["label"]) for field in fields)
while True:
frame.rows = []
for line in help_lines or []:
frame.mark(line, frame.theme["dim"])
if help_lines:
frame.mark("")
- base = len(frame.rows)
+ field_rows: List[int] = [] # field index -> row index
for field in fields:
- frame.mark(f"{field['label']}: {field['value']}",
- selectable=True, align="left")
- frame.cursor = None if on_buttons else base + cursor
+ if field.get("note"):
+ frame.mark("")
+ frame.mark(field["note"], frame.theme["dim"], align="left")
+ frame.mark("")
+ field_rows.append(len(frame.rows))
+ name = f"{field['label']}:".ljust(label_w + 1)
+ frame.mark_segments(
+ [(name, frame.theme["body"]),
+ (" " + field["value"], frame.theme["input"])],
+ selectable=True, align="left")
+ frame.cursor = None if on_buttons else field_rows[cursor]
frame.buttons = (["Save", "Cancel"], btn_index if on_buttons else None)
frame.draw()
curses = frame.curses
@@ -795,7 +810,8 @@ def form(scr, title: str, fields: Sequence[dict],
if key in _CANCEL_KEYS:
raise WizardCancelled()
if on_buttons:
- if key in (9, curses.KEY_BTAB, curses.KEY_UP, curses.KEY_DOWN):
+ if key in (9, curses.KEY_BTAB, curses.KEY_UP, curses.KEY_DOWN,
+ ord("j"), ord("k")):
on_buttons = False
elif key in (curses.KEY_LEFT, curses.KEY_RIGHT,
ord("h"), ord("l")):
@@ -823,7 +839,7 @@ def form(scr, title: str, fields: Sequence[dict],
btn_index = 0 # Save
elif key in (curses.KEY_UP, ord("k")) and cursor == 0:
on_buttons = True
- btn_index = 1 # Cancel
+ btn_index = 0 # Save (wraps around from the top)
else:
moved = frame.motion(key, cursor, len(fields), wrap=True)
if moved is not None: