aboutsummaryrefslogtreecommitdiff
path: root/ui/tui.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 02:19:50 -0400
committerhistoria <historiavg@proton.me>2026-08-24 02:19:50 -0400
commit9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97 (patch)
treec14c950f642107c196c1db23d96dbf69355eff54 /ui/tui.py
parentc02d66b2d3221c0c5f5e8f2cb2ae218f1e325a0a (diff)
downloadtts-audiobook-generator-9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97.tar.gz
feat: settings menu in tui
Diffstat (limited to 'ui/tui.py')
-rw-r--r--ui/tui.py106
1 files changed, 106 insertions, 0 deletions
diff --git a/ui/tui.py b/ui/tui.py
index c73fb09..9047f36 100644
--- a/ui/tui.py
+++ b/ui/tui.py
@@ -726,6 +726,112 @@ def line_edit(scr, title: str, default: str,
# ---------------------------------------------------------------------------
+# Widget: multi-field settings form with Save/Cancel buttons
+# ---------------------------------------------------------------------------
+
+def form(scr, title: str, fields: Sequence[dict],
+ back_value: object = None,
+ help_lines: Optional[Sequence[str]] = None) -> Optional[dict]:
+ """Edit several labeled fields on one screen, then Save or Cancel.
+
+ FIELDS is a list of dicts, one per row, shaped like::
+
+ {"key": "audio_format", "label": "Audio format",
+ "kind": "choice", "value": "m4b",
+ "choices": ["mp3", "m4b", "ogg", "flac"]}
+ {"key": "chunk_size", "label": "Chunk size",
+ "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 or the arrow keys move focus to the
+ Save/Cancel buttons; 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")
+ frame = Frame(scr, title,
+ "Up/Down = move Enter = edit Tab = Save/Cancel "
+ "Esc = cancel")
+ cursor = 0
+ on_buttons = False
+ btn_index = 0
+ edit_cancel = object() # sentinel: backed out of a field editor
+ 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)
+ for field in fields:
+ frame.mark(f"{field['label']}: {field['value']}",
+ selectable=True, align="left")
+ frame.cursor = None if on_buttons else base + cursor
+ frame.buttons = (["Save", "Cancel"], btn_index if on_buttons else None)
+ frame.draw()
+ curses = frame.curses
+ key = frame.get_key(cancel_keys=())
+ if key == 27 and back_value is not None:
+ return back_value
+ if key in _CANCEL_KEYS:
+ raise WizardCancelled()
+ if on_buttons:
+ if key in (9, curses.KEY_BTAB, curses.KEY_UP, curses.KEY_DOWN):
+ on_buttons = False
+ elif key in (curses.KEY_LEFT, curses.KEY_RIGHT,
+ ord("h"), ord("l")):
+ btn_index = 1 - btn_index
+ elif key in (10, 13):
+ if btn_index == 0: # Save
+ for index, field in enumerate(fields):
+ validate = field.get("validate")
+ if field.get("kind") == "text" and validate:
+ error = validate(field["value"])
+ if error is not None:
+ on_buttons = False
+ cursor = index
+ frame.flash(error, "err")
+ break
+ else:
+ return {field["key"]: field["value"]
+ for field in fields}
+ else: # Cancel
+ return back_value
+ else:
+ moved = frame.motion(key, cursor, len(fields), wrap=True)
+ if moved is not None:
+ cursor = moved
+ elif key in (9, curses.KEY_BTAB, curses.KEY_LEFT,
+ curses.KEY_RIGHT, ord("h"), ord("l")):
+ on_buttons = True
+ btn_index = 0
+ elif key in (10, 13):
+ field = fields[cursor]
+ if field.get("kind") == "choice":
+ choices = list(field.get("choices") or [])
+ default = choices.index(field["value"]) \
+ if field["value"] in choices else 0
+ chosen = menu(scr, field["label"],
+ [(c, c) for c in choices],
+ default_index=default,
+ back_value=edit_cancel)
+ if chosen is not edit_cancel:
+ field["value"] = chosen
+ else:
+ edited = line_edit(scr, field["label"], field["value"],
+ validate=field.get("validate"),
+ back_value=edit_cancel)
+ if edited is not edit_cancel:
+ field["value"] = edited
+
+
+# ---------------------------------------------------------------------------
# Widget: directory browser
# ---------------------------------------------------------------------------