mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-26 17:12:35 +00:00
fix(kconfig): Repair menuconfig help buttons and preserve pending toggles
The configurator had four defects that made buttons and selection behave
erratically in `make configure`.
lxdialog front-end:
- Pressing <Help> in a subsystem menu discarded every toggle made since the
menu was opened, because dialog reports only the focused tag and the result
was thrown away. Pass --help-status and fold the returned state back into
the config before showing help.
- <Help> always showed the generic subsystem blurb even though all 41 options
carry their own help text. Resolve the focused tag and show that item's help.
- The main menu prompt promised <Exit> but dialog rendered <Cancel>; label the
button to match.
curses fallback:
- The Help button raised AttributeError and killed the UI at top level: the
root node is the MENU_TREE list itself, and the fallback argument
`current_menu.get("help", ...)` is evaluated eagerly even when the item has
its own help.
- The '?' hotkey was nested inside the <Enter> branch where `key` can only be
a newline, so it was unreachable dead code.
- The button-bar spacing went negative on a narrow dialog and pushed the last
buttons through the right border; centre and clamp the row instead.
Front-end selection no longer swallows exceptions silently, so a crash reports
itself instead of looking like an unresponsive UI.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0161Jh7uCEMLYEMNPsjC1LKf
This commit is contained in:
1 parent
4d9223c8cc
commit
7929e48b4e
1 file changed
+127
-37
+127
-37
@@ -282,6 +282,59 @@ def save_config(cfg):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Linux lxdialog Runner (Uses /usr/bin/dialog for authentic Linux GUI Look)
|
# Linux lxdialog Runner (Uses /usr/bin/dialog for authentic Linux GUI Look)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
def _help_tokens(payload):
|
||||||
|
"""Split a dialog --help-button payload into bare tags.
|
||||||
|
|
||||||
|
dialog emits "HELP <focused-tag>" and, with --help-status, appends the
|
||||||
|
tags that are currently on. Quoting depends on the dialog build, so the
|
||||||
|
tokens are unquoted here rather than at each call site.
|
||||||
|
"""
|
||||||
|
tokens = [t.strip('"') for t in payload.split()]
|
||||||
|
if tokens and tokens[0] == "HELP":
|
||||||
|
tokens = tokens[1:]
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _help_tag(payload):
|
||||||
|
"""Return just the focused tag from a dialog help payload."""
|
||||||
|
tokens = _help_tokens(payload)
|
||||||
|
return tokens[0] if tokens else None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_selection(cfg, items, selected_tags, is_radiolist):
|
||||||
|
"""Fold a dialog checklist/radiolist result back into the config dict."""
|
||||||
|
selected = {t.strip('"') for t in selected_tags}
|
||||||
|
if is_radiolist:
|
||||||
|
sel_item = next((i for i in items if i["id"] in selected), None)
|
||||||
|
if not sel_item:
|
||||||
|
return
|
||||||
|
group = sel_item.get("group")
|
||||||
|
if group == "arch":
|
||||||
|
cfg["CONFIG_ARCH"] = sel_item["val"]
|
||||||
|
cfg["CONFIG_ARCH_X86_64"] = (sel_item["val"] == "x86_64")
|
||||||
|
cfg["CONFIG_ARCH_AARCH64"] = (sel_item["val"] == "aarch64")
|
||||||
|
cfg["CONFIG_ARCH_ARMV8I"] = (sel_item["val"] == "armv8i")
|
||||||
|
elif group == "opt":
|
||||||
|
cfg["CONFIG_OPTIMIZATION"] = sel_item["val"]
|
||||||
|
else:
|
||||||
|
# Only fold in the toggleable rows; anything else in the menu keeps
|
||||||
|
# whatever value it already had.
|
||||||
|
for item in items:
|
||||||
|
if item.get("type") == "bool":
|
||||||
|
cfg[item["id"]] = (item["id"] in selected)
|
||||||
|
|
||||||
|
|
||||||
|
def _menu_help(menu, default="No help available."):
|
||||||
|
"""Help text for a menu node.
|
||||||
|
|
||||||
|
The curses browser uses the MENU_TREE list itself as the root node, so
|
||||||
|
this has to tolerate a list as well as a menu dict.
|
||||||
|
"""
|
||||||
|
if isinstance(menu, dict):
|
||||||
|
return menu.get("help", default)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def run_dialog_gui():
|
def run_dialog_gui():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
backtitle = "SUB-OS v0.2.0-lts Modular Monolithic Linux Kernel Configuration"
|
backtitle = "SUB-OS v0.2.0-lts Modular Monolithic Linux Kernel Configuration"
|
||||||
@@ -301,6 +354,7 @@ def run_dialog_gui():
|
|||||||
"--backtitle", backtitle,
|
"--backtitle", backtitle,
|
||||||
"--title", "SUB-OS Kernel Configuration",
|
"--title", "SUB-OS Kernel Configuration",
|
||||||
"--extra-button", "--extra-label", "Save",
|
"--extra-button", "--extra-label", "Save",
|
||||||
|
"--cancel-label", "Exit",
|
||||||
"--help-button",
|
"--help-button",
|
||||||
"--menu",
|
"--menu",
|
||||||
"Arrow keys navigate the menu. <Enter> selects submenus --->.\n"
|
"Arrow keys navigate the menu. <Enter> selects submenus --->.\n"
|
||||||
@@ -330,8 +384,20 @@ def run_dialog_gui():
|
|||||||
msg_cmd = ["dialog", "--backtitle", backtitle, "--msgbox", f"Configuration saved successfully to {CONFIG_FILE} and {AUTOCONF_HEADER}!", "7", "65"]
|
msg_cmd = ["dialog", "--backtitle", backtitle, "--msgbox", f"Configuration saved successfully to {CONFIG_FILE} and {AUTOCONF_HEADER}!", "7", "65"]
|
||||||
subprocess.run(msg_cmd)
|
subprocess.run(msg_cmd)
|
||||||
elif ret == 2: # Help button
|
elif ret == 2: # Help button
|
||||||
help_text = "SUB-OS Kernel Configuration Utility (lxdialog GUI)\n\nNavigate through each subsystem and configure features according to your target hardware needs."
|
# dialog reports the focused row as "HELP <tag>"; show that
|
||||||
subprocess.run(["dialog", "--backtitle", backtitle, "--title", "Main Menu Help", "--msgbox", help_text, "12", "70"])
|
# subsystem's own help rather than one generic blurb.
|
||||||
|
focus = _help_tag(sel)
|
||||||
|
focused = next((m for m in MENU_TREE if m["id"] == focus), None)
|
||||||
|
if focused:
|
||||||
|
help_title = f"{focused['title']} - Help"
|
||||||
|
help_text = focused.get("help", "No additional help available for this subsystem.")
|
||||||
|
else:
|
||||||
|
help_title = "Main Menu Help"
|
||||||
|
help_text = ("SUB-OS Kernel Configuration Utility (lxdialog GUI)\n\n"
|
||||||
|
"Navigate through each subsystem and configure features "
|
||||||
|
"according to your target hardware needs.")
|
||||||
|
subprocess.run(["dialog", "--backtitle", backtitle, "--title", help_title,
|
||||||
|
"--msgbox", help_text, "16", "72"])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Submenu Display
|
# Submenu Display
|
||||||
@@ -367,6 +433,7 @@ def run_dialog_gui():
|
|||||||
"--backtitle", backtitle,
|
"--backtitle", backtitle,
|
||||||
"--title", submenu["title"],
|
"--title", submenu["title"],
|
||||||
"--help-button",
|
"--help-button",
|
||||||
|
"--help-status",
|
||||||
dialog_type,
|
dialog_type,
|
||||||
"Use <Space> to toggle features [*], <Enter> to confirm, <Help> for info.",
|
"Use <Space> to toggle features [*], <Enter> to confirm, <Help> for info.",
|
||||||
"20", "78", "10"
|
"20", "78", "10"
|
||||||
@@ -377,31 +444,28 @@ def run_dialog_gui():
|
|||||||
out = res.stderr.strip()
|
out = res.stderr.strip()
|
||||||
|
|
||||||
if ret == 0: # OK / Confirm
|
if ret == 0: # OK / Confirm
|
||||||
selected_tags = [t.strip('"') for t in out.split()]
|
_apply_selection(cfg, items, [t.strip('"') for t in out.split()], is_radiolist)
|
||||||
if is_radiolist:
|
|
||||||
if selected_tags:
|
|
||||||
sel_id = selected_tags[0]
|
|
||||||
sel_item = next((i for i in items if i["id"] == sel_id), None)
|
|
||||||
if sel_item:
|
|
||||||
group = sel_item.get("group")
|
|
||||||
if group == "arch":
|
|
||||||
cfg["CONFIG_ARCH"] = sel_item["val"]
|
|
||||||
cfg["CONFIG_ARCH_X86_64"] = (sel_item["val"] == "x86_64")
|
|
||||||
cfg["CONFIG_ARCH_AARCH64"] = (sel_item["val"] == "aarch64")
|
|
||||||
cfg["CONFIG_ARCH_ARMV8I"] = (sel_item["val"] == "armv8i")
|
|
||||||
elif group == "opt":
|
|
||||||
cfg["CONFIG_OPTIMIZATION"] = sel_item["val"]
|
|
||||||
else:
|
|
||||||
for item in items:
|
|
||||||
cfg[item["id"]] = (item["id"] in selected_tags)
|
|
||||||
current_menu_id = "main"
|
current_menu_id = "main"
|
||||||
|
|
||||||
elif ret == 1 or ret == 255: # Cancel or Esc
|
elif ret == 1 or ret == 255: # Cancel or Esc
|
||||||
current_menu_id = "main"
|
current_menu_id = "main"
|
||||||
|
|
||||||
elif ret == 2: # Help button
|
elif ret == 2: # Help button
|
||||||
help_text = submenu.get("help", "No additional help available for this subsystem.")
|
# With --help-status the payload is "HELP <focused-tag> <on-tags...>".
|
||||||
subprocess.run(["dialog", "--backtitle", backtitle, "--title", f"{submenu['title']} - Help", "--msgbox", help_text, "14", "72"])
|
# Re-apply the on-tags first so the toggles made before pressing
|
||||||
|
# <Help> survive the round trip, then show the focused item's help.
|
||||||
|
tokens = _help_tokens(out)
|
||||||
|
focus = tokens[0] if tokens else None
|
||||||
|
_apply_selection(cfg, items, tokens[1:], is_radiolist)
|
||||||
|
focused = next((i for i in items if i["id"] == focus), None)
|
||||||
|
if focused:
|
||||||
|
help_title = f"{focused['label']} - Help"
|
||||||
|
help_text = focused.get("help", "No additional help available for this option.")
|
||||||
|
else:
|
||||||
|
help_title = f"{submenu['title']} - Help"
|
||||||
|
help_text = submenu.get("help", "No additional help available for this subsystem.")
|
||||||
|
subprocess.run(["dialog", "--backtitle", backtitle, "--title", help_title,
|
||||||
|
"--msgbox", help_text, "16", "72"])
|
||||||
|
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
||||||
@@ -564,15 +628,21 @@ def run_curses_gui(stdscr):
|
|||||||
|
|
||||||
# 6. Bottom Action Button Bar
|
# 6. Bottom Action Button Bar
|
||||||
btn_y = dy + dh - 2
|
btn_y = dy + dh - 2
|
||||||
btn_spacing = (dw - sum(len(b) + 4 for b in buttons)) // (len(buttons) + 1)
|
btn_texts = [f"< {b} >" for b in buttons]
|
||||||
cur_bx = dx + btn_spacing
|
# Centre the row and clamp it: the old spacing maths went negative on a
|
||||||
|
# narrow dialog and pushed the last buttons through the right border.
|
||||||
|
total_w = sum(len(t) for t in btn_texts)
|
||||||
|
gap = max(1, (dw - 4 - total_w) // max(1, len(btn_texts) - 1))
|
||||||
|
row_w = total_w + gap * (len(btn_texts) - 1)
|
||||||
|
cur_bx = dx + max(2, (dw - row_w) // 2)
|
||||||
|
|
||||||
for b_idx, btn_name in enumerate(buttons):
|
for b_idx, b_text in enumerate(btn_texts):
|
||||||
|
if cur_bx + len(b_text) > dx + dw - 2:
|
||||||
|
break
|
||||||
is_active_btn = (b_idx == btn_cursor)
|
is_active_btn = (b_idx == btn_cursor)
|
||||||
b_text = f"< {btn_name} >"
|
|
||||||
b_attr = (curses.color_pair(6) | curses.A_BOLD) if (is_active_btn and has_color) else (curses.A_REVERSE if is_active_btn else box_attr)
|
b_attr = (curses.color_pair(6) | curses.A_BOLD) if (is_active_btn and has_color) else (curses.A_REVERSE if is_active_btn else box_attr)
|
||||||
stdscr.addstr(btn_y, cur_bx, b_text, b_attr)
|
stdscr.addstr(btn_y, cur_bx, b_text, b_attr)
|
||||||
cur_bx += len(b_text) + max(2, btn_spacing)
|
cur_bx += len(b_text) + gap
|
||||||
|
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
try:
|
try:
|
||||||
@@ -641,10 +711,8 @@ def run_curses_gui(stdscr):
|
|||||||
else:
|
else:
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
break
|
break
|
||||||
elif btn_cursor == 2 or key == ord('?'): # Help
|
elif btn_cursor == 2: # Help
|
||||||
sel = items[cursor] if cursor < len(items) else None
|
_show_help(stdscr, current_menu, items, cursor, max_y, max_x)
|
||||||
help_msg = sel.get("help", current_menu.get("help", "No help available.")) if sel else "SUB-OS Kernel Configurator"
|
|
||||||
show_popup(stdscr, "Option Help", help_msg, max_y, max_x)
|
|
||||||
elif btn_cursor == 3: # Save
|
elif btn_cursor == 3: # Save
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
||||||
@@ -652,6 +720,10 @@ def run_curses_gui(stdscr):
|
|||||||
cfg = dict(DEFAULTS)
|
cfg = dict(DEFAULTS)
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
show_popup(stdscr, "Reset Defaults", "Default configuration options loaded!", max_y, max_x)
|
show_popup(stdscr, "Reset Defaults", "Default configuration options loaded!", max_y, max_x)
|
||||||
|
elif key == ord('?'):
|
||||||
|
# Previously nested under the <Enter> branch, where it could never
|
||||||
|
# match, so the documented '?' help hotkey did nothing.
|
||||||
|
_show_help(stdscr, current_menu, items, cursor, max_y, max_x)
|
||||||
elif key in [ord('s'), ord('S')]:
|
elif key in [ord('s'), ord('S')]:
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
||||||
@@ -662,6 +734,17 @@ def run_curses_gui(stdscr):
|
|||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def _show_help(stdscr, current_menu, items, cursor, max_y, max_x):
|
||||||
|
"""Show help for the focused row, falling back to the enclosing menu."""
|
||||||
|
sel = items[cursor] if 0 <= cursor < len(items) else None
|
||||||
|
if sel is None:
|
||||||
|
show_popup(stdscr, "Help", _menu_help(current_menu, "SUB-OS Kernel Configurator"), max_y, max_x)
|
||||||
|
return
|
||||||
|
title = sel.get("label") or sel.get("title") or "Option"
|
||||||
|
help_msg = sel.get("help") or _menu_help(current_menu)
|
||||||
|
show_popup(stdscr, f"{title} - Help", help_msg, max_y, max_x)
|
||||||
|
|
||||||
|
|
||||||
def show_popup(stdscr, title, message, max_y, max_x):
|
def show_popup(stdscr, title, message, max_y, max_x):
|
||||||
pw = min(64, max_x - 6)
|
pw = min(64, max_x - 6)
|
||||||
ph = min(12, max_y - 6)
|
ph = min(12, max_y - 6)
|
||||||
@@ -740,19 +823,26 @@ def main():
|
|||||||
try:
|
try:
|
||||||
run_dialog_gui()
|
run_dialog_gui()
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# Falling through silently used to make a crash look like the UI
|
||||||
|
# simply refusing to respond; say what happened before retrying.
|
||||||
|
print(f"menuconfig: lxdialog front-end failed ({exc}); "
|
||||||
|
f"falling back to the curses UI.", file=sys.stderr)
|
||||||
|
|
||||||
# Fallback to Curses GUI replica
|
# Fallback to Curses GUI replica
|
||||||
try:
|
try:
|
||||||
import curses
|
import curses
|
||||||
curses.wrapper(run_curses_gui)
|
curses.wrapper(run_curses_gui)
|
||||||
print(f"*** Configuration saved to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
print(f"*** Configuration saved to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
||||||
except Exception:
|
return
|
||||||
# Fallback if no TTY
|
except Exception as exc:
|
||||||
cfg = load_config()
|
print(f"menuconfig: curses UI unavailable ({exc}); "
|
||||||
save_config(cfg)
|
f"writing the current configuration non-interactively.", file=sys.stderr)
|
||||||
print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
|
||||||
|
# Last resort: no usable TTY at all.
|
||||||
|
cfg = load_config()
|
||||||
|
save_config(cfg)
|
||||||
|
print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in new issue
Block a user