mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-25 16:52:29 +00:00
Login: - Add console_login() (security/auth.c): an interactive boot-time username / password gate rendered on the framebuffer console before any session starts. It blocks until valid credentials are entered, masks the password, scrubs it from the stack, and sets the current user. Default account is SUB / SUB, shown as a hint. Wired into the boot handoff in kernel/main.c ahead of both the GUI desktop and the emergency TTY shell. Configure: - scripts/kconfig/menuconfig.py: fix the architecture (and optimization) picker. The lxdialog front-end used --radiolist, where <Enter> confirms the ORIGINAL choice and only <Space> moves the dot -- so highlighting a new architecture and pressing <Enter> appeared to do nothing. Single-choice groups now use --menu (with --default-item), where <Enter> selects the highlighted row directly. Also stop the unconditional save-on-exit from overriding an explicit "No" at the exit confirmation. Verified: SUB/SUB logs in (injected via QEMU monitor) -> "Login successful" -> desktop starts; the desktop stays blocked until login. dialog --menu selection of aarch64 persists to .config/autoconf.h and retargets `make`. Clean build on x86_64/aarch64/armv8i. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
872 lines
41 KiB
Python
Executable File
872 lines
41 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
SUB-OS Linux Kernel-Style Interactive Kconfig Configurator (lxdialog TUI/GUI)
|
|
Recreates the exact visual appearance, colors, floating dialogs, drop shadows,
|
|
and interactive controls of the Linux kernel 'make menuconfig' system.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
CONFIG_FILE = ".config"
|
|
AUTOCONF_HEADER = "include/config/autoconf.h"
|
|
|
|
DEFAULTS = {
|
|
# Architecture
|
|
"CONFIG_ARCH": "x86_64",
|
|
"CONFIG_ARCH_X86_64": True,
|
|
"CONFIG_ARCH_AARCH64": False,
|
|
"CONFIG_ARCH_ARMV8I": False,
|
|
|
|
# General Setup
|
|
"CONFIG_SUBOS_HOSTNAME": "sub-os",
|
|
"CONFIG_SUBOS_VERSION": "0.2.0-lts",
|
|
"CONFIG_SMP": True,
|
|
"CONFIG_PREEMPT": True,
|
|
"CONFIG_OPTIMIZATION": "-O2",
|
|
|
|
# Memory Management
|
|
"CONFIG_MM_PMM": True,
|
|
"CONFIG_MM_SLAB": True,
|
|
"CONFIG_MM_VMA": True,
|
|
|
|
# Filesystems
|
|
"CONFIG_FS_VFS": True,
|
|
"CONFIG_FS_FAT32": True,
|
|
"CONFIG_FS_EXT2": True,
|
|
"CONFIG_FS_RAMFS": True,
|
|
"CONFIG_FS_DEVFS": True,
|
|
"CONFIG_FS_PROCFS": True,
|
|
"CONFIG_FS_SYSFS": True,
|
|
|
|
# Storage Drivers
|
|
"CONFIG_DRV_NVME": True,
|
|
"CONFIG_DRV_AHCI": True,
|
|
"CONFIG_DRV_ATA": True,
|
|
"CONFIG_DRV_VIRTIO_BLK": True,
|
|
"CONFIG_DRV_RAMDISK": True,
|
|
|
|
# Network Drivers & Stack
|
|
"CONFIG_NET_STACK": True,
|
|
"CONFIG_DRV_E1000": True,
|
|
"CONFIG_DRV_RTL8139": True,
|
|
"CONFIG_DRV_VIRTIO_NET": True,
|
|
"CONFIG_NET_HTTPD": True,
|
|
"CONFIG_NET_SSHD": True,
|
|
"CONFIG_NET_FILTER": True,
|
|
|
|
# Audio & Display
|
|
"CONFIG_DRV_SOUND_HDA": True,
|
|
"CONFIG_DRV_SOUND_AC97": True,
|
|
"CONFIG_DRV_BOCHS_VBE": True,
|
|
"CONFIG_DRV_CANVAS_2D": True,
|
|
|
|
# Bus, Sensors & Crypto
|
|
"CONFIG_DRV_USB_XHCI": True,
|
|
"CONFIG_DRV_HWMON": True,
|
|
"CONFIG_DRV_VIRTIO_RNG": True,
|
|
"CONFIG_DRV_PTY": True,
|
|
|
|
# Security & Advanced
|
|
"CONFIG_SECURITY_LSM": True,
|
|
"CONFIG_SECURITY_KEYRING": True,
|
|
"CONFIG_BPF_VM": True,
|
|
"CONFIG_IO_URING": True,
|
|
"CONFIG_USERLAND_LAZYBOX": True,
|
|
}
|
|
|
|
MENU_TREE = [
|
|
{
|
|
"id": "arch",
|
|
"title": "Target architecture selection",
|
|
"help": "Choose the target CPU instruction set architecture.\n\n"
|
|
" x86_64: 64-bit AMD64 / Intel 64 PC architecture.\n"
|
|
" aarch64: ARMv8-A 64-bit Cortex/Neoverse architecture.\n"
|
|
" armv8i: 32-bit ARM (AArch32) architecture.\n\n"
|
|
"Symbol: CONFIG_ARCH",
|
|
"items": [
|
|
{"id": "CONFIG_ARCH_X86_64", "tag": "x86_64", "label": "x86_64 (64-Bit AMD64 / Intel 64)", "type": "radio", "group": "arch", "val": "x86_64",
|
|
"help": "Builds for 64-bit x86 PCs with GDT, IDT, PIC, PIT, and Multiboot loader."},
|
|
{"id": "CONFIG_ARCH_AARCH64", "tag": "aarch64", "label": "aarch64 (ARMv8-A 64-Bit Cortex-A57/Neoverse)", "type": "radio", "group": "arch", "val": "aarch64",
|
|
"help": "Builds for 64-bit ARM with GICv2, Generic Timer, and 39-bit MMU."},
|
|
{"id": "CONFIG_ARCH_ARMV8I", "tag": "armv8i", "label": "armv8i (32-Bit ARM / AArch32 Profile)", "type": "radio", "group": "arch", "val": "armv8i",
|
|
"help": "Builds for 32-bit ARM with VMSAv7 Section MMU, GICv2, and VFP/NEON."},
|
|
]
|
|
},
|
|
{
|
|
"id": "opt",
|
|
"title": "Compiler optimizations and code generation",
|
|
"help": "Select GCC optimization level for kernel compilation.\n\n"
|
|
" -O2: Recommended balanced speed and size optimization.\n"
|
|
" -O3: Aggressive auto-vectorization and loop unrolling.\n"
|
|
" -Os: Minimal binary footprint.\n"
|
|
" -O0: Debugging mode without register optimizations.\n\n"
|
|
"Symbol: CONFIG_OPTIMIZATION",
|
|
"items": [
|
|
{"id": "CONFIG_OPT_O2", "tag": "O2", "label": "-O2 (High performance, recommended)", "type": "radio", "group": "opt", "val": "-O2", "help": "Standard kernel optimization level."},
|
|
{"id": "CONFIG_OPT_O3", "tag": "O3", "label": "-O3 (Aggressive vectorization & loop unrolling)", "type": "radio", "group": "opt", "val": "-O3", "help": "Maximum speed optimization."},
|
|
{"id": "CONFIG_OPT_OS", "tag": "Os", "label": "-Os (Optimize for minimal binary size)", "type": "radio", "group": "opt", "val": "-Os", "help": "Smallest kernel footprint."},
|
|
{"id": "CONFIG_OPT_O0", "tag": "O0", "label": "-O0 (Disable optimizations for GDB debugging)", "type": "radio", "group": "opt", "val": "-O0", "help": "Unoptimized debug build."},
|
|
]
|
|
},
|
|
{
|
|
"id": "general",
|
|
"title": "General kernel setup and core subsystems",
|
|
"help": "Configure core multitasking, synchronization, and kernel execution parameters.",
|
|
"items": [
|
|
{"id": "CONFIG_SMP", "tag": "SMP", "label": "Symmetric Multi-Processing (SMP) support", "type": "bool",
|
|
"help": "Enables multi-core scheduler and per-CPU load balancing."},
|
|
{"id": "CONFIG_PREEMPT", "tag": "PREEMPT", "label": "Preemptive kernel scheduling model", "type": "bool",
|
|
"help": "Allows low-latency preemption for real-time task responsiveness."},
|
|
{"id": "CONFIG_BPF_VM", "tag": "BPF_VM", "label": "In-Kernel eBPF register virtual machine", "type": "bool",
|
|
"help": "Enables in-kernel safe bytecode execution for tracing and network filters."},
|
|
{"id": "CONFIG_IO_URING", "tag": "IO_URING", "label": "High-performance io_uring ring engine", "type": "bool",
|
|
"help": "Provides zero-syscall asynchronous submission and completion queues."},
|
|
]
|
|
},
|
|
{
|
|
"id": "mm",
|
|
"title": "Memory management & object allocators",
|
|
"help": "Configure physical memory management, slab caches, and virtual address spaces.",
|
|
"items": [
|
|
{"id": "CONFIG_MM_PMM", "tag": "MM_PMM", "label": "Physical Page Frame Allocator (PMM)", "type": "bool",
|
|
"help": "4KB bitmap-backed physical page frame allocator."},
|
|
{"id": "CONFIG_MM_SLAB", "tag": "MM_SLAB", "label": "SLUB / SLAB high-speed object cache", "type": "bool",
|
|
"help": "Fast object caches for kernel structures (VFS inodes, sockets, tasks)."},
|
|
{"id": "CONFIG_MM_VMA", "tag": "MM_VMA", "label": "Virtual Memory Areas (VMA) and mmap subsystem", "type": "bool",
|
|
"help": "Process address space mappings and demand paging."},
|
|
]
|
|
},
|
|
{
|
|
"id": "fs",
|
|
"title": "Virtual File Systems & Storage Device Drivers",
|
|
"help": "Configure storage hardware controllers and mounted file system drivers.",
|
|
"items": [
|
|
{"id": "CONFIG_FS_VFS", "tag": "FS_VFS", "label": "Virtual File System (VFS) core framework", "type": "bool",
|
|
"help": "Provides POSIX open, read, write, close, and mount abstraction."},
|
|
{"id": "CONFIG_FS_FAT32", "tag": "FS_FAT32", "label": "FAT32 / VFAT file system driver", "type": "bool",
|
|
"help": "High-performance FAT32 filesystem driver mounted at /mnt/fat32."},
|
|
{"id": "CONFIG_FS_EXT2", "tag": "FS_EXT2", "label": "Second Extended (EXT2) Linux file system", "type": "bool",
|
|
"help": "Standard Linux EXT2 filesystem driver with direct inode indexing."},
|
|
{"id": "CONFIG_FS_SYSFS", "tag": "FS_SYSFS", "label": "Sysfs KObject virtual hierarchy (/sys)", "type": "bool",
|
|
"help": "Exports kernel device tree and runtime telemetry to /sys."},
|
|
{"id": "CONFIG_DRV_NVME", "tag": "DRV_NVME", "label": "NVM Express (NVMe PCIe SSD) driver", "type": "bool",
|
|
"help": "High-throughput PCIe NVMe solid-state storage driver."},
|
|
{"id": "CONFIG_DRV_AHCI", "tag": "DRV_AHCI", "label": "AHCI SATA 6Gb/s controller driver", "type": "bool",
|
|
"help": "Serial ATA AHCI controller driver for hard drives and SSDs."},
|
|
{"id": "CONFIG_DRV_ATA", "tag": "DRV_ATA", "label": "Legacy Parallel ATA / IDE storage driver", "type": "bool",
|
|
"help": "IDE / PATA drive support for legacy hardware."},
|
|
{"id": "CONFIG_DRV_VIRTIO_BLK", "tag": "DRV_VIRTIO_BLK", "label": "VirtIO paravirtualized block device (/dev/vda)", "type": "bool",
|
|
"help": "Fast virtual storage device for QEMU / KVM hypervisors."},
|
|
{"id": "CONFIG_DRV_RAMDISK", "tag": "DRV_RAMDISK", "label": "RAM disk block driver (/dev/ram0)", "type": "bool",
|
|
"help": "In-memory dynamic block storage for initrd and testing."},
|
|
]
|
|
},
|
|
{
|
|
"id": "net",
|
|
"title": "Networking subsystem & hardware adapters",
|
|
"help": "Configure network stack protocols, packet filtering, and ethernet adapters.",
|
|
"items": [
|
|
{"id": "CONFIG_NET_STACK", "tag": "NET_STACK", "label": "TCP/IP protocol stack (IPv4, UDP, TCP)", "type": "bool",
|
|
"help": "Kernel socket networking stack with ARP, DHCP, DNS, UDP, and TCP."},
|
|
{"id": "CONFIG_DRV_E1000", "tag": "DRV_E1000", "label": "Intel 82540EM Gigabit Ethernet NIC driver", "type": "bool",
|
|
"help": "Standard Intel Gigabit PCI network interface card driver."},
|
|
{"id": "CONFIG_DRV_RTL8139", "tag": "DRV_RTL8139", "label": "Realtek RTL8139 Fast Ethernet NIC driver", "type": "bool",
|
|
"help": "100Mbps Realtek PCI network interface card driver."},
|
|
{"id": "CONFIG_DRV_VIRTIO_NET", "tag": "DRV_VIRTIO_NET", "label": "VirtIO 10-Gigabit paravirtualized NIC", "type": "bool",
|
|
"help": "Paravirtualized network adapter with zero-copy TX/RX rings."},
|
|
{"id": "CONFIG_NET_FILTER", "tag": "NET_FILTER", "label": "NetFilter stateful packet firewall (iptables)", "type": "bool",
|
|
"help": "Stateful packet inspection firewall and NAT engine."},
|
|
{"id": "CONFIG_NET_HTTPD", "tag": "NET_HTTPD", "label": "In-kernel embedded HTTP REST web server", "type": "bool",
|
|
"help": "Built-in HTTP server exposing REST APIs on port 80."},
|
|
{"id": "CONFIG_NET_SSHD", "tag": "NET_SSHD", "label": "In-kernel Secure Shell (SSH 2.0) daemon", "type": "bool",
|
|
"help": "Built-in encrypted SSH server on port 22 with auth."},
|
|
]
|
|
},
|
|
{
|
|
"id": "drivers",
|
|
"title": "Device Drivers, Graphics & Sound",
|
|
"help": "Configure multimedia, sound cards, display adapters, and USB controllers.",
|
|
"items": [
|
|
{"id": "CONFIG_DRV_CANVAS_2D", "tag": "DRV_CANVAS_2D", "label": "2D TrueColor Rasterizer Canvas graphics engine", "type": "bool",
|
|
"help": "Framebuffer rasterizer supporting lines, rectangles, and fonts."},
|
|
{"id": "CONFIG_DRV_BOCHS_VBE", "tag": "DRV_BOCHS_VBE", "label": "Bochs / VBE dynamic resolution video adapter", "type": "bool",
|
|
"help": "VBE/Bochs graphics controller for graphical resolution modes."},
|
|
{"id": "CONFIG_DRV_SOUND_HDA", "tag": "DRV_SOUND_HDA", "label": "Intel High Definition Audio (Azalia HDA) driver", "type": "bool",
|
|
"help": "PCI Intel HD Audio codec and stream controller."},
|
|
{"id": "CONFIG_DRV_SOUND_AC97", "tag": "DRV_SOUND_AC97", "label": "Analog Devices AC'97 sound codec driver", "type": "bool",
|
|
"help": "Legacy PCI AC'97 sound controller."},
|
|
{"id": "CONFIG_DRV_USB_XHCI", "tag": "DRV_USB_XHCI", "label": "USB 3.0 Extensible Host Controller (xHCI)", "type": "bool",
|
|
"help": "USB 3.0 Host Controller with device enumeration."},
|
|
{"id": "CONFIG_DRV_HWMON", "tag": "DRV_HWMON", "label": "HWMON CPU temperature & fan sensor driver", "type": "bool",
|
|
"help": "Hardware monitoring sensors for CPU temperature and fan RPM."},
|
|
{"id": "CONFIG_DRV_VIRTIO_RNG", "tag": "DRV_VIRTIO_RNG", "label": "VirtIO true hardware random generator (/dev/hwrng)", "type": "bool",
|
|
"help": "Paravirtualized entropy source feeding the kernel CSPRNG."},
|
|
{"id": "CONFIG_DRV_PTY", "tag": "DRV_PTY", "label": "Unix98 Pseudo-Terminal (PTY) subsystem (/dev/pts)", "type": "bool",
|
|
"help": "Pseudo-terminals for interactive terminal sessions and SSH."},
|
|
]
|
|
},
|
|
{
|
|
"id": "security",
|
|
"title": "Security options & Cryptographic Keyrings",
|
|
"help": "Configure kernel security modules, authentication, and root keyrings.",
|
|
"items": [
|
|
{"id": "CONFIG_SECURITY_LSM", "tag": "SECURITY_LSM", "label": "Linux Security Module (LSM) access control", "type": "bool",
|
|
"help": "Mandatory access control and POSIX capabilities enforcement."},
|
|
{"id": "CONFIG_SECURITY_KEYRING", "tag": "SECURITY_KEYRING", "label": "X.509 cryptographic keyring & certificates", "type": "bool",
|
|
"help": "System trust keyring verifying kernel modules and signatures."},
|
|
]
|
|
},
|
|
{
|
|
"id": "userland",
|
|
"title": "Userland Core & LazyBox Utility Suite",
|
|
"help": "Configure userland binaries, shell applets, and command-line tools.",
|
|
"items": [
|
|
{"id": "CONFIG_USERLAND_LAZYBOX", "tag": "LAZYBOX", "label": "LazyBox multi-call utility suite (70+ Linux tools)", "type": "bool",
|
|
"help": "Embedded BusyBox-like multi-call binary with sh, nano, neofetch, ls, etc."},
|
|
]
|
|
}
|
|
]
|
|
|
|
def load_config():
|
|
cfg = dict(DEFAULTS)
|
|
if os.path.exists(CONFIG_FILE):
|
|
with open(CONFIG_FILE, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if "=" in line:
|
|
k, v = line.split("=", 1)
|
|
k = k.strip()
|
|
v = v.strip().strip('"')
|
|
if v == "y":
|
|
cfg[k] = True
|
|
elif v == "n":
|
|
cfg[k] = False
|
|
else:
|
|
cfg[k] = v
|
|
return cfg
|
|
|
|
def save_config(cfg):
|
|
os.makedirs(os.path.dirname(AUTOCONF_HEADER), exist_ok=True)
|
|
with open(CONFIG_FILE, "w") as f_cfg, open(AUTOCONF_HEADER, "w") as f_hdr:
|
|
f_cfg.write("# Automatically generated by SUB-OS Linux-Style Kconfig Configurator\n# Do not edit directly\n\n")
|
|
f_hdr.write("/* Automatically generated by SUB-OS Kconfig. Do not edit. */\n#ifndef _AUTOCONF_H\n#define _AUTOCONF_H\n\n")
|
|
|
|
# Write Architecture
|
|
arch = cfg.get("CONFIG_ARCH", "x86_64")
|
|
f_cfg.write(f"CONFIG_ARCH={arch}\n")
|
|
f_hdr.write(f'#define CONFIG_ARCH "{arch}"\n')
|
|
|
|
for k, v in sorted(cfg.items()):
|
|
if k == "CONFIG_ARCH":
|
|
continue
|
|
if isinstance(v, bool):
|
|
if v:
|
|
f_cfg.write(f"{k}=y\n")
|
|
f_hdr.write(f"#define {k} 1\n")
|
|
else:
|
|
f_cfg.write(f"# {k} is not set\n")
|
|
elif isinstance(v, str):
|
|
f_cfg.write(f'{k}="{v}"\n')
|
|
f_hdr.write(f'#define {k} "{v}"\n')
|
|
elif isinstance(v, int):
|
|
f_cfg.write(f"{k}={v}\n")
|
|
f_hdr.write(f"#define {k} {v}\n")
|
|
|
|
f_hdr.write("\n#endif /* _AUTOCONF_H */\n")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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():
|
|
cfg = load_config()
|
|
backtitle = "SUB-OS v0.2.0-lts Modular Monolithic Linux Kernel Configuration"
|
|
|
|
current_menu_id = "main"
|
|
|
|
while True:
|
|
if current_menu_id == "main":
|
|
# Build Main Menu Options
|
|
menu_opts = []
|
|
for menu in MENU_TREE:
|
|
menu_opts.extend([menu["id"], f"{menu['title']} --->"])
|
|
|
|
cmd = [
|
|
"dialog",
|
|
"--clear",
|
|
"--backtitle", backtitle,
|
|
"--title", "SUB-OS Kernel Configuration",
|
|
"--extra-button", "--extra-label", "Save",
|
|
"--cancel-label", "Exit",
|
|
"--help-button",
|
|
"--menu",
|
|
"Arrow keys navigate the menu. <Enter> selects submenus --->.\n"
|
|
"Press <Save> to write .config, <Exit> to finish, <Help> for information.",
|
|
"20", "78", "10"
|
|
] + menu_opts
|
|
|
|
res = subprocess.run(cmd, stderr=subprocess.PIPE, text=True)
|
|
ret = res.returncode
|
|
sel = res.stderr.strip()
|
|
|
|
if ret == 0: # Select
|
|
current_menu_id = sel
|
|
elif ret == 1 or ret == 255: # Exit or Esc
|
|
# Confirmation dialog
|
|
exit_cmd = [
|
|
"dialog", "--backtitle", backtitle,
|
|
"--title", "Save Kernel Configuration?",
|
|
"--yesno", "Do you wish to save your new kernel configuration?",
|
|
"7", "60"
|
|
]
|
|
if subprocess.run(exit_cmd).returncode == 0:
|
|
save_config(cfg)
|
|
break
|
|
elif ret == 3: # Extra button (Save)
|
|
save_config(cfg)
|
|
msg_cmd = ["dialog", "--backtitle", backtitle, "--msgbox", f"Configuration saved successfully to {CONFIG_FILE} and {AUTOCONF_HEADER}!", "7", "65"]
|
|
subprocess.run(msg_cmd)
|
|
elif ret == 2: # Help button
|
|
# dialog reports the focused row as "HELP <tag>"; show that
|
|
# 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:
|
|
# Submenu Display
|
|
submenu = next((m for m in MENU_TREE if m["id"] == current_menu_id), None)
|
|
if not submenu:
|
|
current_menu_id = "main"
|
|
continue
|
|
|
|
items = submenu["items"]
|
|
is_radio = any(i.get("type") == "radio" for i in items)
|
|
|
|
item_opts = []
|
|
if is_radio:
|
|
# Single-choice group (architecture, optimization). Use a --menu,
|
|
# not a --radiolist: in a radiolist the arrow keys only move the
|
|
# highlight and <Space> is required to move the (*) dot, so a user
|
|
# who highlights another architecture and presses <Enter> confirms
|
|
# the ORIGINAL choice -- the selection appears to do nothing. With
|
|
# --menu, <Enter> selects the highlighted row directly.
|
|
default_item = None
|
|
for item in items:
|
|
item_opts.extend([item["id"], item["label"]])
|
|
group = item.get("group")
|
|
cur = cfg.get("CONFIG_ARCH") if group == "arch" else (
|
|
cfg.get("CONFIG_OPTIMIZATION") if group == "opt" else None)
|
|
if cur is not None and cur == item["val"]:
|
|
default_item = item["id"]
|
|
|
|
cmd = ["dialog", "--clear", "--backtitle", backtitle,
|
|
"--title", submenu["title"], "--help-button"]
|
|
if default_item:
|
|
cmd += ["--default-item", default_item]
|
|
cmd += ["--menu",
|
|
"Highlight a choice with the arrow keys and press <Enter> to select it.",
|
|
"20", "78", "10"] + item_opts
|
|
else:
|
|
for item in items:
|
|
status = "ON" if cfg.get(item["id"], False) else "OFF"
|
|
item_opts.extend([item["id"], item["label"], status])
|
|
|
|
cmd = ["dialog", "--clear", "--backtitle", backtitle,
|
|
"--title", submenu["title"], "--help-button", "--help-status",
|
|
"--checklist",
|
|
"Use <Space> to toggle features [*], <Enter> to confirm, <Help> for info.",
|
|
"20", "78", "10"] + item_opts
|
|
|
|
res = subprocess.run(cmd, stderr=subprocess.PIPE, text=True)
|
|
ret = res.returncode
|
|
out = res.stderr.strip()
|
|
|
|
if ret == 0: # OK / Confirm
|
|
if is_radio:
|
|
# --menu returns just the highlighted tag.
|
|
_apply_selection(cfg, items, [out.strip('"')], True)
|
|
else:
|
|
_apply_selection(cfg, items, [t.strip('"') for t in out.split()], False)
|
|
current_menu_id = "main"
|
|
|
|
elif ret == 1 or ret == 255: # Cancel or Esc
|
|
current_menu_id = "main"
|
|
|
|
elif ret == 2: # Help button
|
|
if is_radio:
|
|
# --menu help payload is just "HELP <focused-tag>".
|
|
focus = _help_tag(out)
|
|
else:
|
|
# --help-status payload is "HELP <focused-tag> <on-tags...>";
|
|
# re-apply the on-tags so toggles made before <Help> survive.
|
|
tokens = _help_tokens(out)
|
|
focus = tokens[0] if tokens else None
|
|
_apply_selection(cfg, items, tokens[1:], False)
|
|
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"])
|
|
|
|
# Saving is handled inside the loop: the <Save> button writes immediately,
|
|
# and <Exit> asks first and writes only on "Yes". Saving again here would
|
|
# override an explicit "No", so just report the resulting state.
|
|
print(f"*** Kernel configuration ready: {CONFIG_FILE} (ARCH={cfg.get('CONFIG_ARCH', 'x86_64')}) ***")
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Pure Python Curses GUI Renderer (Pixel-Perfect Linux lxdialog Replica)
|
|
# -----------------------------------------------------------------------------
|
|
def run_curses_gui(stdscr):
|
|
import curses
|
|
try:
|
|
curses.curs_set(0)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
curses.use_default_colors()
|
|
except Exception:
|
|
pass
|
|
|
|
# Authentic Linux Kernel lxdialog Palette
|
|
has_color = False
|
|
try:
|
|
if curses.has_colors():
|
|
has_color = True
|
|
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Screen Backdrop
|
|
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) # Dialog Window Body
|
|
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_WHITE) # Window Title / Hotkeys
|
|
curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) # Selected Highlighted Item
|
|
curses.init_pair(5, curses.COLOR_BLUE, curses.COLOR_WHITE) # Subtext / Checkbox Brackets
|
|
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN) # Button Inverted Highlight
|
|
curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_BLACK) # Window Drop Shadow
|
|
except Exception:
|
|
has_color = False
|
|
|
|
cfg = load_config()
|
|
menu_stack = []
|
|
current_menu = MENU_TREE
|
|
cursor = 0
|
|
btn_cursor = 0 # 0: Select, 1: Exit, 2: Help, 3: Save, 4: Defaults
|
|
buttons = [" Select ", " Exit ", " Help ", " Save ", "Defaults"]
|
|
|
|
while True:
|
|
max_y, max_x = stdscr.getmaxyx()
|
|
if max_y < 20 or max_x < 70:
|
|
stdscr.clear()
|
|
stdscr.addstr(0, 0, "Terminal window too small for menuconfig. Please resize!")
|
|
stdscr.refresh()
|
|
key = stdscr.getch()
|
|
if key in [ord('q'), ord('Q'), 27]:
|
|
break
|
|
continue
|
|
|
|
# 1. Fill Screen Backdrop (Royal Blue)
|
|
stdscr.bkgd(' ', curses.color_pair(1) if has_color else 0)
|
|
stdscr.clear()
|
|
|
|
# Top Backtitle
|
|
backtitle = "SUB-OS v0.2.0-lts Modular Monolithic Linux Kernel Configuration"
|
|
stdscr.addstr(0, 2, backtitle, curses.A_BOLD)
|
|
arch_info = f"Arch: [{cfg.get('CONFIG_ARCH', 'x86_64')}] Opt: [{cfg.get('CONFIG_OPTIMIZATION', '-O2')}]"
|
|
if max_x - len(arch_info) - 4 > len(backtitle) + 4:
|
|
stdscr.addstr(0, max_x - len(arch_info) - 3, arch_info, curses.A_BOLD)
|
|
|
|
# 2. Compute Floating Dialog Box Dimensions
|
|
dw = min(78, max_x - 4)
|
|
dh = min(22, max_y - 3)
|
|
dx = (max_x - dw) // 2
|
|
dy = (max_y - dh) // 2
|
|
|
|
# 3. Draw 3D Drop Shadow
|
|
if has_color:
|
|
for sy in range(dy + 1, dy + dh + 1):
|
|
stdscr.addstr(sy, dx + dw, " ", curses.color_pair(7))
|
|
stdscr.addstr(dy + dh, dx + 2, " " * (dw + 2), curses.color_pair(7))
|
|
|
|
# 4. Draw Dialog Window Box
|
|
box_attr = curses.color_pair(2) if has_color else 0
|
|
for by in range(dy, dy + dh):
|
|
stdscr.addstr(by, dx, " " * dw, box_attr)
|
|
|
|
# Window Border
|
|
title = " SUB-OS Kernel Configuration " if not menu_stack else f" {current_menu.get('title', 'Subsystem')} "
|
|
for x in range(dx, dx + dw):
|
|
stdscr.addch(dy, x, curses.ACS_HLINE, box_attr)
|
|
stdscr.addch(dy + dh - 1, x, curses.ACS_HLINE, box_attr)
|
|
for y in range(dy, dy + dh):
|
|
stdscr.addch(y, dx, curses.ACS_VLINE, box_attr)
|
|
stdscr.addch(y, dx + dw - 1, curses.ACS_VLINE, box_attr)
|
|
stdscr.addch(dy, dx, curses.ACS_ULCORNER, box_attr)
|
|
stdscr.addch(dy, dx + dw - 1, curses.ACS_URCORNER, box_attr)
|
|
stdscr.addch(dy + dh - 1, dx, curses.ACS_LLCORNER, box_attr)
|
|
stdscr.addch(dy + dh - 1, dx + dw - 1, curses.ACS_LRCORNER, box_attr)
|
|
|
|
# Dialog Title Banner
|
|
title_x = dx + (dw - len(title)) // 2
|
|
stdscr.addstr(dy, title_x, title, (curses.color_pair(3) | curses.A_BOLD) if has_color else curses.A_BOLD)
|
|
|
|
# Instructions Header
|
|
inst1 = "Arrow keys navigate the menu. <Enter> selects submenus --->."
|
|
inst2 = "Press <Y> to include, <N> to exclude, <Space> to toggle [*]."
|
|
stdscr.addstr(dy + 1, dx + 3, inst1[:dw - 6], box_attr)
|
|
stdscr.addstr(dy + 2, dx + 3, inst2[:dw - 6], box_attr)
|
|
|
|
# 5. Inner Scroll Box for Items
|
|
items = current_menu if isinstance(current_menu, list) else current_menu.get("items", [])
|
|
box_top = dy + 4
|
|
box_height = dh - 8
|
|
box_left = dx + 3
|
|
box_width = dw - 6
|
|
|
|
# Draw Inner Frame
|
|
for by in range(box_top, box_top + box_height):
|
|
stdscr.addstr(by, box_left, " " * box_width, box_attr)
|
|
for x in range(box_left, box_left + box_width):
|
|
stdscr.addch(box_top, x, curses.ACS_HLINE, box_attr)
|
|
stdscr.addch(box_top + box_height - 1, x, curses.ACS_HLINE, box_attr)
|
|
for y in range(box_top, box_top + box_height):
|
|
stdscr.addch(y, box_left, curses.ACS_VLINE, box_attr)
|
|
stdscr.addch(y, box_left + box_width - 1, curses.ACS_VLINE, box_attr)
|
|
stdscr.addch(box_top, box_left, curses.ACS_ULCORNER, box_attr)
|
|
stdscr.addch(box_top, box_left + box_width - 1, curses.ACS_URCORNER, box_attr)
|
|
stdscr.addch(box_top + box_height - 1, box_left, curses.ACS_LLCORNER, box_attr)
|
|
stdscr.addch(box_top + box_height - 1, box_left + box_width - 1, curses.ACS_LRCORNER, box_attr)
|
|
|
|
visible_count = box_height - 2
|
|
scroll_offset = max(0, cursor - visible_count + 1) if cursor >= visible_count else 0
|
|
|
|
# Render Item Rows
|
|
for i in range(visible_count):
|
|
item_idx = scroll_offset + i
|
|
if item_idx >= len(items):
|
|
break
|
|
item = items[item_idx]
|
|
iy = box_top + 1 + i
|
|
ix = box_left + 2
|
|
|
|
is_cur = (item_idx == cursor)
|
|
row_attr = (curses.color_pair(4) | curses.A_BOLD) if (is_cur and has_color) else (box_attr | curses.A_REVERSE if is_cur else box_attr)
|
|
|
|
if "items" in item: # Submenu
|
|
display_text = f" {item['title']} --->"
|
|
elif item.get("type") == "bool":
|
|
val = cfg.get(item["id"], False)
|
|
state = "[*]" if val else "[ ]"
|
|
display_text = f" {state} {item['label']}"
|
|
elif item.get("type") == "radio":
|
|
group = item.get("group")
|
|
if group == "arch":
|
|
val = (cfg.get("CONFIG_ARCH") == item["val"])
|
|
elif group == "opt":
|
|
val = (cfg.get("CONFIG_OPTIMIZATION") == item["val"])
|
|
else:
|
|
val = False
|
|
state = "(*)" if val else "( )"
|
|
display_text = f" {state} {item['label']}"
|
|
else:
|
|
display_text = f" {item.get('title', '')}"
|
|
|
|
row_str = display_text.ljust(box_width - 4)[:box_width - 4]
|
|
stdscr.addstr(iy, ix, row_str, row_attr)
|
|
|
|
# 6. Bottom Action Button Bar
|
|
btn_y = dy + dh - 2
|
|
btn_texts = [f"< {b} >" for b in buttons]
|
|
# 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, b_text in enumerate(btn_texts):
|
|
if cur_bx + len(b_text) > dx + dw - 2:
|
|
break
|
|
is_active_btn = (b_idx == btn_cursor)
|
|
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)
|
|
cur_bx += len(b_text) + gap
|
|
|
|
stdscr.refresh()
|
|
try:
|
|
key = stdscr.getch()
|
|
except Exception:
|
|
break
|
|
|
|
# Keyboard Navigation
|
|
if key in [curses.KEY_UP, ord('k'), ord('K')]:
|
|
if cursor > 0:
|
|
cursor -= 1
|
|
elif key in [curses.KEY_DOWN, ord('j'), ord('J')]:
|
|
if cursor < len(items) - 1:
|
|
cursor += 1
|
|
elif key in [curses.KEY_LEFT, ord('h'), ord('H')]:
|
|
btn_cursor = (btn_cursor - 1) % len(buttons)
|
|
elif key in [curses.KEY_RIGHT, ord('l'), ord('L'), 9]: # Tab or Right
|
|
btn_cursor = (btn_cursor + 1) % len(buttons)
|
|
elif key in [ord('y'), ord('Y')]:
|
|
if cursor < len(items):
|
|
sel = items[cursor]
|
|
if sel.get("type") == "bool":
|
|
cfg[sel["id"]] = True
|
|
elif key in [ord('n'), ord('N')]:
|
|
if cursor < len(items):
|
|
sel = items[cursor]
|
|
if sel.get("type") == "bool":
|
|
cfg[sel["id"]] = False
|
|
elif key == ord(' '):
|
|
if cursor < len(items):
|
|
sel = items[cursor]
|
|
if sel.get("type") == "bool":
|
|
cfg[sel["id"]] = not cfg.get(sel["id"], False)
|
|
elif sel.get("type") == "radio":
|
|
group = sel.get("group")
|
|
if group == "arch":
|
|
cfg["CONFIG_ARCH"] = sel["val"]
|
|
cfg["CONFIG_ARCH_X86_64"] = (sel["val"] == "x86_64")
|
|
cfg["CONFIG_ARCH_AARCH64"] = (sel["val"] == "aarch64")
|
|
cfg["CONFIG_ARCH_ARMV8I"] = (sel["val"] == "armv8i")
|
|
elif group == "opt":
|
|
cfg["CONFIG_OPTIMIZATION"] = sel["val"]
|
|
elif key in [curses.KEY_ENTER, 10, 13]:
|
|
# Trigger active bottom button
|
|
if btn_cursor == 0: # Select
|
|
if cursor < len(items):
|
|
sel = items[cursor]
|
|
if "items" in sel:
|
|
menu_stack.append((current_menu, cursor))
|
|
current_menu = sel
|
|
cursor = 0
|
|
elif sel.get("type") == "bool":
|
|
cfg[sel["id"]] = not cfg.get(sel["id"], False)
|
|
elif sel.get("type") == "radio":
|
|
group = sel.get("group")
|
|
if group == "arch":
|
|
cfg["CONFIG_ARCH"] = sel["val"]
|
|
cfg["CONFIG_ARCH_X86_64"] = (sel["val"] == "x86_64")
|
|
cfg["CONFIG_ARCH_AARCH64"] = (sel["val"] == "aarch64")
|
|
cfg["CONFIG_ARCH_ARMV8I"] = (sel["val"] == "armv8i")
|
|
elif group == "opt":
|
|
cfg["CONFIG_OPTIMIZATION"] = sel["val"]
|
|
elif btn_cursor == 1: # Exit
|
|
if menu_stack:
|
|
current_menu, cursor = menu_stack.pop()
|
|
else:
|
|
save_config(cfg)
|
|
break
|
|
elif btn_cursor == 2: # Help
|
|
_show_help(stdscr, current_menu, items, cursor, max_y, max_x)
|
|
elif btn_cursor == 3: # Save
|
|
save_config(cfg)
|
|
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
|
elif btn_cursor == 4: # Defaults
|
|
cfg = dict(DEFAULTS)
|
|
save_config(cfg)
|
|
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')]:
|
|
save_config(cfg)
|
|
show_popup(stdscr, "Save Configuration", f"Configuration successfully saved to {CONFIG_FILE} and {AUTOCONF_HEADER}!", max_y, max_x)
|
|
elif key in [ord('q'), ord('Q'), 27]: # Esc or Q
|
|
if menu_stack:
|
|
current_menu, cursor = menu_stack.pop()
|
|
else:
|
|
save_config(cfg)
|
|
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):
|
|
pw = min(64, max_x - 6)
|
|
ph = min(12, max_y - 6)
|
|
px = (max_x - pw) // 2
|
|
py = (max_y - ph) // 2
|
|
import curses
|
|
|
|
# Draw popup box
|
|
for y in range(py, py + ph):
|
|
stdscr.addstr(y, px, " " * pw, curses.A_REVERSE)
|
|
for x in range(px, px + pw):
|
|
stdscr.addch(py, x, curses.ACS_HLINE, curses.A_REVERSE)
|
|
stdscr.addch(py + ph - 1, x, curses.ACS_HLINE, curses.A_REVERSE)
|
|
for y in range(py, py + ph):
|
|
stdscr.addch(y, px, curses.ACS_VLINE, curses.A_REVERSE)
|
|
stdscr.addch(y, px + pw - 1, curses.ACS_VLINE, curses.A_REVERSE)
|
|
|
|
stdscr.addstr(py, px + (pw - len(title) - 2) // 2, f" {title} ", curses.A_BOLD | curses.A_REVERSE)
|
|
|
|
# Word wrap message lines
|
|
lines = []
|
|
for raw_line in message.split("\n"):
|
|
while len(raw_line) > pw - 4:
|
|
split_idx = raw_line[:pw - 4].rfind(" ")
|
|
if split_idx <= 0:
|
|
split_idx = pw - 4
|
|
lines.append(raw_line[:split_idx])
|
|
raw_line = raw_line[split_idx:].lstrip()
|
|
lines.append(raw_line)
|
|
|
|
for i, line in enumerate(lines[:ph - 4]):
|
|
stdscr.addstr(py + 2 + i, px + 2, line[:pw - 4], curses.A_REVERSE)
|
|
|
|
btn_str = "< OK >"
|
|
stdscr.addstr(py + ph - 2, px + (pw - len(btn_str)) // 2, btn_str, curses.A_BOLD)
|
|
stdscr.refresh()
|
|
stdscr.getch()
|
|
|
|
def defconfig(arch="x86_64"):
|
|
cfg = dict(DEFAULTS)
|
|
cfg["CONFIG_ARCH"] = arch
|
|
if arch == "x86_64":
|
|
cfg["CONFIG_ARCH_X86_64"] = True
|
|
cfg["CONFIG_ARCH_AARCH64"] = False
|
|
cfg["CONFIG_ARCH_ARMV8I"] = False
|
|
elif arch in ["aarch64", "arm64"]:
|
|
cfg["CONFIG_ARCH_X86_64"] = False
|
|
cfg["CONFIG_ARCH_AARCH64"] = True
|
|
cfg["CONFIG_ARCH_ARMV8I"] = False
|
|
elif arch in ["armv8i", "armv81", "arm32", "arm", "armv8", "aarch32"]:
|
|
cfg["CONFIG_ARCH_X86_64"] = False
|
|
cfg["CONFIG_ARCH_AARCH64"] = False
|
|
cfg["CONFIG_ARCH_ARMV8I"] = True
|
|
save_config(cfg)
|
|
print(f"*** Default configuration for '{arch}' written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
|
|
|
def main():
|
|
if len(sys.argv) > 1:
|
|
cmd = sys.argv[1]
|
|
if cmd in ["--defconfig", "defconfig"]:
|
|
arch = sys.argv[2] if len(sys.argv) > 2 else "x86_64"
|
|
defconfig(arch)
|
|
return
|
|
elif cmd == "--x86_64":
|
|
defconfig("x86_64")
|
|
return
|
|
elif cmd in ["--aarch64", "--armv8", "--arm64"]:
|
|
defconfig("aarch64")
|
|
return
|
|
elif cmd in ["--armv8i", "--armv81", "--arm32", "--arm", "--aarch32"]:
|
|
defconfig("armv8i")
|
|
return
|
|
|
|
# Check for /usr/bin/dialog for authentic Linux lxdialog GUI
|
|
if shutil.which("dialog") and sys.stdin.isatty():
|
|
try:
|
|
run_dialog_gui()
|
|
return
|
|
except Exception as exc:
|
|
# 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 the curses GUI replica, but only when we genuinely have an
|
|
# interactive terminal on both ends. Probing curses without a real TTY makes
|
|
# it fail deep inside the wrapper's teardown (e.g. "nocbreak() returned
|
|
# ERR"), which looked like a crash; gate on isatty so a piped or CI
|
|
# invocation drops straight to the non-interactive writer instead.
|
|
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
try:
|
|
import curses
|
|
curses.wrapper(run_curses_gui)
|
|
print(f"*** Configuration saved to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
|
return
|
|
except Exception as exc:
|
|
print(f"menuconfig: curses UI unavailable ({exc}); "
|
|
f"writing the current configuration non-interactively.", file=sys.stderr)
|
|
else:
|
|
print("menuconfig: no interactive terminal detected; "
|
|
"writing the current configuration non-interactively.", file=sys.stderr)
|
|
|
|
# Last resort: no usable TTY at all. Preserve an existing .config if present,
|
|
# otherwise fall back to the architecture default so the build never stalls.
|
|
cfg = load_config()
|
|
save_config(cfg)
|
|
print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|