From 9dd4f9595be3b1d76a3a07dc3eca90cfaf8a3f97 Mon Sep 17 00:00:00 2001 From: historia Date: Mon, 24 Aug 2026 02:19:50 -0400 Subject: feat: settings menu in tui --- ui/tui.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) (limited to 'ui/tui.py') diff --git a/ui/tui.py b/ui/tui.py index c73fb09..9047f36 100644 --- a/ui/tui.py +++ b/ui/tui.py @@ -725,6 +725,112 @@ def line_edit(scr, title: str, default: str, text += chr(key) +# --------------------------------------------------------------------------- +# 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 # --------------------------------------------------------------------------- -- cgit v1.2.3