aboutsummaryrefslogtreecommitdiff
path: root/app/ui
diff options
context:
space:
mode:
Diffstat (limited to 'app/ui')
-rw-r--r--app/ui/tui.py151
1 files changed, 92 insertions, 59 deletions
diff --git a/app/ui/tui.py b/app/ui/tui.py
index f1abea1..0391998 100644
--- a/app/ui/tui.py
+++ b/app/ui/tui.py
@@ -1158,18 +1158,21 @@ def checkbox_tree(scr, title: str, families: List[dict],
],
}
- Space on a family row checks its recommended option (or clears every
- option when one is already checked); Space on an option row toggles
- that option. Tab/Right expands or collapses the family under the
- cursor. Enter returns the flat list of (family_index, option_key)
- pairs for every checked option, in tree order; at least one checked
- option is required. Nothing is checked by default, and with
- EXPAND_ALL every family starts expanded. CHECKED (a set of
- (family_index, option_key) pairs) pre-checks those options instead,
- expanding every family that holds a checked option and placing the
- cursor on the first such family — the "modify an existing config"
- entry point. A "[recommended]" tag is shown only when a family has
- more than one option — a single option needs no tag.
+ Space or Enter on a family row checks its recommended option (or
+ clears every option when one is already checked); Space or Enter on
+ an option row toggles that option. Right/l expands or collapses the
+ family under the cursor, Left/h collapses it. Tab (or Up/Down at the
+ ends of the list) moves the focus to the Confirm/Back buttons: Enter
+ on Confirm returns the flat list of (family_index, option_key) pairs
+ for every checked option, in tree order, and requires at least one
+ checked option; Enter on Back returns BACK_VALUE. Nothing is checked
+ by default and every family starts collapsed; with EXPAND_ALL every
+ family starts expanded. CHECKED (a set of (family_index, option_key)
+ pairs) pre-checks those options instead, expanding every family that
+ holds a checked option and placing the cursor on the first such
+ family — the "modify an existing config" entry point. A
+ "[recommended]" tag is shown only when a 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 either key returns it so the caller can fall back a
@@ -1177,18 +1180,14 @@ def checkbox_tree(scr, title: str, families: List[dict],
"""
if not families:
raise ValueError("checkbox_tree() needs at least one family")
- footer = footer or ("Up/Down = move Tab/Right = expand Space = check "
- "Enter = accept Esc = cancel")
+ footer = footer or ("Up/Down = move Enter/Space = check "
+ "Left/Right = expand Tab = buttons Esc = cancel")
frame = Frame(scr, title, footer)
expanded = {index for index in range(len(families))} if expand_all else set()
checked = set(checked or ()) # (family_index, option_key)
- if checked:
- for index, _option_key in checked:
- expanded.add(index)
- expanded.add(0)
- else:
- expanded.add(0)
+ for index, _option_key in checked:
+ expanded.add(index)
def family_checked(index: int) -> bool:
return any(pair[0] == index for pair in checked)
@@ -1211,6 +1210,8 @@ def checkbox_tree(scr, title: str, families: List[dict],
first_checked = min((index for index, _option_key in checked),
default=None)
cursor = 0
+ on_buttons = False
+ btn_index = 0
while True:
nodes = visible_nodes()
if first_checked is not None:
@@ -1249,9 +1250,14 @@ def checkbox_tree(scr, title: str, families: List[dict],
segments.append((" [recommended]", frame.theme["warn"]))
frame.mark_segments(segments, indent=2, selectable=True,
align="left")
- frame.cursor = cursor
- node = nodes[cursor]
- frame.status = (families[node[1]].get("detail", ""), "info")
+ frame.cursor = None if on_buttons else cursor
+ frame.buttons = (["Confirm", "Back"],
+ btn_index if on_buttons else None)
+ if on_buttons:
+ frame.status = None
+ else:
+ node = nodes[cursor]
+ frame.status = (families[node[1]].get("detail", ""), "info")
frame.draw()
curses = frame.curses
key = frame.get_key(cancel_keys=())
@@ -1259,41 +1265,68 @@ def checkbox_tree(scr, title: str, families: List[dict],
return back_value
if key in _CANCEL_KEYS:
raise WizardCancelled()
- moved = frame.motion(key, cursor, len(nodes), wrap=True)
- if moved is not None:
- cursor = moved
- elif key in (9, curses.KEY_RIGHT, ord("l")) and node[0] == "family":
- index = node[1]
- if index in expanded:
- expanded.discard(index)
+ if on_buttons:
+ if key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("h"), ord("l")):
+ btn_index = 1 - btn_index
+ elif key in (curses.KEY_UP, ord("k"), curses.KEY_BTAB):
+ on_buttons = False
+ cursor = len(nodes) - 1 if nodes else 0
+ elif key in (curses.KEY_DOWN, ord("j"), 9):
+ on_buttons = False
+ cursor = 0
+ elif key in (10, 13):
+ if btn_index == 0: # Confirm
+ selection = accept()
+ if selection:
+ return selection
+ frame.flash("Check at least one model package", "err")
+ else: # Back
+ return back_value
+ else:
+ node = nodes[cursor]
+ if key in (curses.KEY_DOWN, ord("j")) \
+ and cursor == len(nodes) - 1:
+ on_buttons = True
+ btn_index = 0
+ elif key in (curses.KEY_UP, ord("k")) and cursor == 0:
+ on_buttons = True
+ btn_index = 0
+ elif key in (9, curses.KEY_BTAB):
+ on_buttons = True
+ btn_index = 0
else:
- expanded.add(index)
- elif key == curses.KEY_LEFT and node[0] == "family":
- expanded.discard(node[1])
- elif key == ord(" "):
- if node[0] == "family":
- index = node[1]
- options = families[index]["options"]
- if family_checked(index):
- for option in options:
- checked.discard((index, option["key"]))
- else:
- for option in options:
- if option.get("recommended"):
- checked.add((index, option["key"]))
- break
+ moved = frame.motion(key, cursor, len(nodes), wrap=True)
+ if moved is not None:
+ cursor = moved
+ elif key in (curses.KEY_RIGHT, ord("l")) \
+ and node[0] == "family":
+ index = node[1]
+ if index in expanded:
+ expanded.discard(index)
else:
- if options:
- checked.add((index, options[0]["key"]))
- expanded.add(index)
- else:
- _, index, option_key = node
- if (index, option_key) in checked:
- checked.discard((index, option_key))
- else:
- checked.add((index, option_key))
- elif key in (10, 13): # Enter: accept the checked selection
- selection = accept()
- if selection:
- return selection
- frame.flash("Check at least one model package (Space)", "err")
+ expanded.add(index)
+ elif key in (curses.KEY_LEFT, ord("h")) \
+ and node[0] == "family":
+ expanded.discard(node[1])
+ elif key in (10, 13, ord(" ")):
+ if node[0] == "family":
+ index = node[1]
+ options = families[index]["options"]
+ if family_checked(index):
+ for option in options:
+ checked.discard((index, option["key"]))
+ else:
+ for option in options:
+ if option.get("recommended"):
+ checked.add((index, option["key"]))
+ break
+ else:
+ if options:
+ checked.add((index, options[0]["key"]))
+ expanded.add(index)
+ else:
+ _, index, option_key = node
+ if (index, option_key) in checked:
+ checked.discard((index, option_key))
+ else:
+ checked.add((index, option_key))