From: Lunaixsky Date: Sun, 25 Aug 2024 00:55:17 +0000 (+0100) Subject: Menuconfig Implementation and auto-qemu refactoring (#44) X-Git-Url: https://scm.lunaixsky.com/lunaix-os.git/commitdiff_plain/50b4ecfb1b28e9b1dfc57b6a876fcdf938092152?hp=a136ca38d83fae60994a54f5da88120e545895e1 Menuconfig Implementation and auto-qemu refactoring (#44) * unify the chardev backend definitions * add --dry option for dry-running * allow supply extra raw qemu options * implement ncurses based tui framework * basic lunamenu framework built on top libtui * (libtui) add scrollable, list, textbox * (menu) add listview, dialogue * interface LunaConfig into menuconfig * remove flickering when new context being added and redrawn * add ability to navigate, edit the config node * adjust the layout parameters * some refactors * add help dialogue * layout refinement * add as hot key to navigate up level * add confirmation dialogue for exiting * refactors * add user friendly alias to LConfig terms * fix the focusing problem with textbox * bypass the configuration for user program generation * bypass the showing of config view during usr program generation, use default value instead * house keeping stuff * add terminal dimension check and fallback to prompt based * replenish the help messages * fix: udiv64 is not used when doing u64 division --- diff --git a/lunaix-os/LConfig b/lunaix-os/LConfig index 36be155..1c151e3 100644 --- a/lunaix-os/LConfig +++ b/lunaix-os/LConfig @@ -4,7 +4,7 @@ include("kernel") include("arch") include("hal") -@Term("Version") +@Term("Kernel Version") @ReadOnly def lunaix_ver(): """ @@ -16,7 +16,7 @@ def lunaix_ver(): seq_num = int(time.time() / 3600) default("%s dev-2024_%d"%(v(arch), seq_num)) -@Collection +@Collection("Kernel Debug and Testing") def debug_and_testing(): """ General settings for kernel debugging feature diff --git a/lunaix-os/arch/LConfig b/lunaix-os/arch/LConfig index 95403e3..7afea85 100644 --- a/lunaix-os/arch/LConfig +++ b/lunaix-os/arch/LConfig @@ -1,24 +1,34 @@ include("x86/LConfig") -@Collection +@Collection("Platform") def architecture_support(): """ Config ISA related features """ - @Term + @Term("Architecture") def arch(): - """ Config ISA support """ - type(["i386", "x86_64", "aarch64", "rv64"]) - default("i386") + """ + Config ISA support + """ + # type(["i386", "x86_64", "aarch64", "rv64"]) + type(["i386", "x86_64"]) + default("x86_64") env_val = env("ARCH") if env_val: set_value(env_val) - @Term + @Term("Base operand size") @ReadOnly def arch_bits(): + """ + Defines the base size of a general register of the + current selected ISA. + + This the 'bits' part when we are talking about a CPU + """ + type(["64", "32"]) match v(arch): case "i386": diff --git a/lunaix-os/arch/x86/LConfig b/lunaix-os/arch/x86/LConfig index 7580a36..2b34524 100644 --- a/lunaix-os/arch/x86/LConfig +++ b/lunaix-os/arch/x86/LConfig @@ -4,7 +4,7 @@ def x86_configurations(): add_to_collection(architecture_support) - @Term + @Term("Use SSE2/3/4 extension") def x86_enable_sse_feature(): """ Config whether to allow using SSE feature for certain @@ -15,14 +15,14 @@ def x86_configurations(): default(False) - @Term + @Term("Bootloader Model") def x86_bl(): """ Select the bootloader interface Supported interface - mb: multiboot compliant - mb2: multiboot2 compliant + mb: multiboot compliance + mb2: multiboot2 compliance none: do not use any interface """ diff --git a/lunaix-os/hal/LConfig b/lunaix-os/hal/LConfig index 35385c9..63598db 100644 --- a/lunaix-os/hal/LConfig +++ b/lunaix-os/hal/LConfig @@ -2,7 +2,7 @@ include("char") include("bus") include("ahci") -@Collection +@Collection("Devices & Peripherials") def hal(): """ Lunaix hardware asbtraction layer """ diff --git a/lunaix-os/hal/ahci/LConfig b/lunaix-os/hal/ahci/LConfig index b957881..a199abf 100644 --- a/lunaix-os/hal/ahci/LConfig +++ b/lunaix-os/hal/ahci/LConfig @@ -1,10 +1,10 @@ -@Collection +@Collection("AHCI") def sata_ahci(): add_to_collection(hal) - @Term + @Term("Enable AHCI support") def ahci_enable(): """ Enable the support of SATA AHCI. Must require PCI at current stage """ diff --git a/lunaix-os/hal/bus/LConfig b/lunaix-os/hal/bus/LConfig index 74e3dbf..e4281d2 100644 --- a/lunaix-os/hal/bus/LConfig +++ b/lunaix-os/hal/bus/LConfig @@ -1,17 +1,17 @@ -@Collection +@Collection("Buses & Interconnects") def bus_if(): """ System/platform bus interface """ add_to_collection(hal) - @Term + @Term("PCI") def pci_enable(): """ Peripheral Component Interconnect (PCI) Bus """ type(bool) default(True) - @Term + @Term("PCI Express") def pcie_ext(): """ Enable support of PCI-Express extension """ type(bool) @@ -19,7 +19,7 @@ def bus_if(): return v(pci_enable) - @Term + @Term("Use PMIO for PCI") def pci_pmio(): """ Use port-mapped I/O interface for controlling PCI """ type(bool) diff --git a/lunaix-os/hal/char/LConfig b/lunaix-os/hal/char/LConfig index 788bfb4..8e662fa 100644 --- a/lunaix-os/hal/char/LConfig +++ b/lunaix-os/hal/char/LConfig @@ -1,21 +1,25 @@ include("uart") -@Collection +@Collection("Character Devices") def char_device(): """ Controlling support of character devices """ add_to_collection(hal) - @Term + @Term("VGA 80x25 text-mode console") def vga_console(): """ Enable VGA console device (text mode only) """ type(bool) default(True) - @Term + @Term("VGA character game device") def chargame_console(): - """ Enable VGA Charactor Game console device (text mode only) """ + """ + Enable VGA Charactor Game console device (text mode only) + + You normally don't need to include this, unless you want some user space fun ;) + """ type(bool) default(False) \ No newline at end of file diff --git a/lunaix-os/kernel/block/blkbuf.c b/lunaix-os/kernel/block/blkbuf.c index 4f255c9..302cd96 100644 --- a/lunaix-os/kernel/block/blkbuf.c +++ b/lunaix-os/kernel/block/blkbuf.c @@ -3,6 +3,7 @@ #include #include #include +#include LOG_MODULE("blkbuf") @@ -17,7 +18,8 @@ static struct cake_pile* bb_pile; static inline u64_t __tolba(struct blkbuf_cache* cache, unsigned int blk_id) { - return ((u64_t)cache->blksize * (u64_t)blk_id) / cache->blkdev->blk_size; + return udiv64(((u64_t)cache->blksize * (u64_t)blk_id), + cache->blkdev->blk_size); } static void diff --git a/lunaix-os/kernel/fs/LConfig b/lunaix-os/kernel/fs/LConfig index 37a9695..194e186 100644 --- a/lunaix-os/kernel/fs/LConfig +++ b/lunaix-os/kernel/fs/LConfig @@ -1,18 +1,18 @@ -@Collection +@Collection("File Systems") def file_system(): """ Config feature related to file system supports """ add_to_collection(kernel_feature) - @Term + @Term("ext2 support") def fs_ext2(): """ Enable ext2 file system support """ type(bool) default(True) - @Term + @Term("iso9660 support") def fs_iso9660(): """ Enable iso9660 file system support """ diff --git a/lunaix-os/kernel/mm/LConfig b/lunaix-os/kernel/mm/LConfig index 7db89f5..17247b0 100644 --- a/lunaix-os/kernel/mm/LConfig +++ b/lunaix-os/kernel/mm/LConfig @@ -1,86 +1,86 @@ -@Collection +@Collection("Memory Management") def memory_subsystem(): """ Config the memory subsystem """ - @Collection + @Collection("Physical Memory") def physical_mm(): """ Physical memory manager """ - @Term + @Term("Allocation policy") def pmalloc_method(): """ Allocation policy for phiscal memory """ type(["simple", "buddy", "ncontig"]) default("simple") - @Group + @Group("Simple") def pmalloc_simple_po_thresholds(): - @Term + @Term("Maximum cached order-0 free pages") def pmalloc_simple_max_po0(): """ free list capacity for order-0 pages """ type(int) default(4096) - @Term + @Term("Maximum cached order-1 free pages") def pmalloc_simple_max_po1(): """ free list capacity for order-1 pages """ type(int) default(2048) - @Term + @Term("Maximum cached order-2 free pages") def pmalloc_simple_max_po2(): """ free list capacity for order-2 pages """ type(int) default(2048) - @Term + @Term("Maximum cached order-3 free pages") def pmalloc_simple_max_po3(): """ free list capacity for order-3 pages """ type(int) default(2048) - @Term + @Term("Maximum cached order-4 free pages") def pmalloc_simple_max_po4(): """ free list capacity for order-4 pages """ type(int) default(512) - @Term + @Term("Maximum cached order-5 free pages") def pmalloc_simple_max_po5(): """ free list capacity for order-5 pages """ type(int) default(512) - @Term + @Term("Maximum cached order-6 free pages") def pmalloc_simple_max_po6(): """ free list capacity for order-6 pages """ type(int) default(128) - @Term + @Term("Maximum cached order-7 free pages") def pmalloc_simple_max_po7(): """ free list capacity for order-7 pages """ type(int) default(128) - @Term + @Term("Maximum cached order-8 free pages") def pmalloc_simple_max_po8(): """ free list capacity for order-8 pages """ type(int) default(64) - @Term + @Term("Maximum cached order-9 free pages") def pmalloc_simple_max_po9(): """ free list capacity for order-9 pages """ diff --git a/lunaix-os/makeinc/lunabuild.mkinc b/lunaix-os/makeinc/lunabuild.mkinc index d246a6e..aee8e49 100644 --- a/lunaix-os/makeinc/lunabuild.mkinc +++ b/lunaix-os/makeinc/lunabuild.mkinc @@ -7,15 +7,16 @@ lbuild_opts := --lconfig-file LConfig all_lconfigs = $(shell find $(CURDIR) -name "LConfig") +LCONFIG_FLAGS += --config $(lbuild_opts) --config-save $(lconfig_save) + export $(lconfig_save): $(all_lconfigs) @echo restarting configuration... - @$(LBUILD) --config $(lbuild_opts) --config-save $(lconfig_save) --force\ - -o $(lbuild_dir)/ + @$(LBUILD) $(LCONFIG_FLAGS) --force -o $(lbuild_dir)/ export $(lbuild_config_h): $(lconfig_save) - @$(LBUILD) --config $(lbuild_opts) --config-save $(lconfig_save) -o $(@D) + @$(LBUILD) $(LCONFIG_FLAGS) -o $(@D) export $(lbuild_mkinc): $(lbuild_config_h) @@ -24,5 +25,5 @@ $(lbuild_mkinc): $(lbuild_config_h) .PHONY: config export config: $(all_lconfigs) - @$(LBUILD) --config $(lbuild_opts) --config-save $(lconfig_save) --force\ + @$(LBUILD) $(LCONFIG_FLAGS) --force\ -o $(lbuild_dir)/ diff --git a/lunaix-os/scripts/build-tools/integration/libmenu.py b/lunaix-os/scripts/build-tools/integration/libmenu.py new file mode 100644 index 0000000..3b0492e --- /dev/null +++ b/lunaix-os/scripts/build-tools/integration/libmenu.py @@ -0,0 +1,261 @@ +import curses +import integration.libtui as tui +from integration.libtui import ColorScope, TuiColor, Alignment, EventType + +def create_buttons(main_ctx, btn_defs, sizes = "*,*"): + size_defs = ",".join(['*'] * len(btn_defs)) + + layout = tui.FlexLinearLayout(main_ctx, "buttons", size_defs) + layout.orientation(tui.FlexLinearLayout.LANDSCAPE) + layout.set_size(*(sizes.split(',')[:2])) + layout.set_padding(1, 1, 1, 1) + layout.set_alignment(Alignment.CENTER | Alignment.BOT) + + for i, btn_def in enumerate(btn_defs): + but1 = tui.TuiButton(main_ctx, "b1") + but1.set_text(btn_def["text"]) + but1.set_click_callback(btn_def["onclick"]) + but1.set_alignment(Alignment.CENTER) + + layout.set_cell(i, but1) + + return layout + +def create_title(ctx, title): + _t = tui.TuiLabel(ctx, "label") + _t.set_text(title) + _t.set_local_pos(1, 0) + _t.set_alignment(Alignment.TOP | Alignment.CENTER) + _t.hightlight(True) + _t.pad_around(True) + return _t + +class ListView(tui.TuiObject): + def __init__(self, context, id): + super().__init__(context, id) + + self.__create_layout() + + self.__sel_changed = None + self.__sel = None + + def __create_layout(self): + hint_moveup = tui.TuiLabel(self._context, "movup") + hint_moveup.override_color(ColorScope.HINT) + hint_moveup.set_text("^^^ - MORE") + hint_moveup.set_visbility(False) + hint_moveup.set_alignment(Alignment.TOP) + + hint_movedown = tui.TuiLabel(self._context, "movdown") + hint_movedown.override_color(ColorScope.HINT) + hint_movedown.set_text("vvv - MORE") + hint_movedown.set_visbility(False) + hint_movedown.set_alignment(Alignment.BOT) + + list_ = tui.SimpleList(self._context, "list") + list_.set_size("*", "*") + list_.set_alignment(Alignment.CENTER | Alignment.TOP) + + list_.set_onselected_cb(self._on_selected) + list_.set_onselection_change_cb(self._on_sel_changed) + + scroll = tui.TuiScrollable(self._context, "scroll") + scroll.set_size("*", "*") + scroll.set_alignment(Alignment.CENTER) + scroll.set_content(list_) + + layout = tui.FlexLinearLayout( + self._context, f"main_layout", "2,*,2") + layout.set_size("*", "*") + layout.set_alignment(Alignment.CENTER) + layout.orientation(tui.FlexLinearLayout.PORTRAIT) + layout.set_parent(self) + + layout.set_cell(0, hint_moveup) + layout.set_cell(1, scroll) + layout.set_cell(2, hint_movedown) + + self.__hint_up = hint_moveup + self.__hint_down = hint_movedown + self.__list = list_ + self.__scroll = scroll + self.__layout = layout + + def add_item(self, item): + self.__list.add_item(item) + + def clear(self): + self.__list.clear() + + def on_draw(self): + super().on_draw() + + more_above = not self.__scroll.reached_top() + more_below = not self.__scroll.reached_last() + self.__hint_up.set_visbility(more_above) + self.__hint_down.set_visbility(more_below) + + self.__layout.on_draw() + + def on_layout(self): + super().on_layout() + self.__layout.on_layout() + + def _on_sel_changed(self, listv, prev, new): + h = self.__scroll._size.y() + self.__scroll.set_scrollY((new + 1) // h * h) + + if self.__sel_changed: + self.__sel_changed(listv, prev, new) + + def _on_selected(self, listv, index, item): + if self.__sel: + self.__sel(listv, index, item) + + def set_onselected_cb(self, cb): + self.__sel = cb + + def set_onselect_changed_cb(self, cb): + self.__sel_changed = cb + +class Dialogue(tui.TuiContext): + Pending = 0 + Yes = 1 + No = 2 + Abort = 3 + def __init__(self, session, title = "", content = "", input=False, + ok_btn = "OK", no_btn = "No", abort_btn = None): + super().__init__(session) + + self.__btns = [ + { "text": ok_btn, "onclick": lambda x: self._ok_onclick() } + ] + + if no_btn: + self.__btns.append({ + "text": no_btn, + "onclick": lambda x: self._no_onclick() + }) + if abort_btn: + self.__btns.append({ + "text": abort_btn, + "onclick": lambda x: self._abort_onclick() + }) + + self.__title_txt = title + self.__status = Dialogue.Pending + self.__content = content + self.__input_dialog = input + self._textbox = None + + self.set_size("70", "0.5*") + self.set_alignment(Alignment.CENTER) + + def set_content(self, content): + self.__content = content + + def set_input_dialogue(self, yes): + self.__input_dialog = yes + + def prepare(self): + self.__create_layout(self.__title_txt) + + def _handle_key_event(self, key): + if key == 27: + self.__close() + return + super()._handle_key_event(key) + + + def _ok_onclick(self): + self.__status = Dialogue.Yes + self.__close() + + def _no_onclick(self): + self.__status = Dialogue.No + self.__close() + + def _abort_onclick(self): + self.__status = Dialogue.Abort + self.__close() + + def __create_layout(self, title): + panel = tui.TuiPanel(self, "panel") + layout = tui.FlexLinearLayout(self, "layout", "*,3") + btn_grp = create_buttons(self, self.__btns) + t = create_title(self, title) + content = self.__create_content() + + self.__title = t + self.__layout = layout + self.__panel = panel + + panel._dyn_size.set(self._dyn_size) + panel._local_pos.set(self._local_pos) + panel.set_alignment(self._align) + panel.drop_shadow(1, 2) + panel.border(True) + + layout.orientation(tui.FlexLinearLayout.PORTRAIT) + layout.set_size("*", "*") + layout.set_padding(4, 1, 1, 1) + + t.set_alignment(Alignment.CENTER | Alignment.TOP) + + layout.set_cell(0, content) + layout.set_cell(1, btn_grp) + + panel.add(t) + panel.add(layout) + + self.set_root(panel) + + def __create_content(self): + text = None + if isinstance(self.__content, str): + text = tui.TuiTextBlock(self, "tb") + text.set_size("0.6*", "0.5*") + text.set_alignment(Alignment.CENTER) + text.set_text(self.__content) + elif self.__content is not None: + return self.__content + + if not self.__input_dialog: + self.set_size(h = "20") + return text + + tb = tui.TuiTextBox(self, "input") + tb.set_size("0.5*", "3") + tb.set_alignment(Alignment.CENTER) + + if text: + layout = tui.FlexLinearLayout(self, "layout", "*,5") + layout.orientation(tui.FlexLinearLayout.PORTRAIT) + layout.set_size("*", "*") + layout.set_cell(0, text) + layout.set_cell(1, tb) + else: + layout = tb + self.set_size(h = "10") + + self.set_curser_mode(1) + + self._textbox = tb + + return layout + + def __close(self): + self.session().pop_context() + + def status(self): + return self.__status + + def show(self, title=None): + if title: + self.__title.set_text(title) + self.session().push_context(self) + + +def show_dialog(session, title, text): + dia = Dialogue(session, title=title, content=text, no_btn=None) + dia.show() \ No newline at end of file diff --git a/lunaix-os/scripts/build-tools/integration/libtui.py b/lunaix-os/scripts/build-tools/integration/libtui.py new file mode 100644 index 0000000..30e28f6 --- /dev/null +++ b/lunaix-os/scripts/build-tools/integration/libtui.py @@ -0,0 +1,1267 @@ +# +# libtui - TUI framework using ncurses +# (c) 2024 Lunaixsky +# +# I sware, this is the last time I ever touch +# any sort of the GUI messes. +# + +import curses +import re +import curses.panel as cpanel +import curses.textpad as textpad +import textwrap + +def __invoke_fn(obj, fn, *args): + try: + fn(*args) + except: + _id = obj._id if obj else "" + raise Exception(_id, str(fn), args) + +def resize_safe(obj, co, y, x): + __invoke_fn(obj, co.resize, y, x) + +def move_safe(obj, co, y, x): + __invoke_fn(obj, co.move, y, x) + +def addstr_safe(obj, co, y, x, str, *args): + __invoke_fn(obj, co.addstr, y, x, str, *args) + +class _TuiColor: + def __init__(self, v) -> None: + self.__v = v + def __int__(self): + return self.__v + def bright(self): + return self.__v + 8 + +class TuiColor: + black = _TuiColor(curses.COLOR_BLACK) + red = _TuiColor(curses.COLOR_RED) + green = _TuiColor(curses.COLOR_GREEN) + yellow = _TuiColor(curses.COLOR_YELLOW) + blue = _TuiColor(curses.COLOR_BLUE) + magenta = _TuiColor(curses.COLOR_MAGENTA) + cyan = _TuiColor(curses.COLOR_CYAN) + white = _TuiColor(curses.COLOR_WHITE) + +class Alignment: + LEFT = 0b000001 + RIGHT = 0b000010 + CENTER = 0b000100 + TOP = 0b001000 + BOT = 0b010000 + ABS = 0b000000 + REL = 0b100000 + +class ColorScope: + WIN = 1 + PANEL = 2 + TEXT = 3 + TEXT_HI = 4 + SHADOW = 5 + SELECT = 6 + HINT = 7 + BOX = 8 + +class EventType: + E_KEY = 0 + E_REDRAW = 1 + E_QUIT = 2 + E_TASK = 3 + E_CHFOCUS = 4 + E_M_FOCUS = 0b10000000 + + def focused_only(t): + return (t & EventType.E_M_FOCUS) + + def value(t): + return t & ~EventType.E_M_FOCUS + + def key_press(t): + return (t & ~EventType.E_M_FOCUS) == EventType.E_KEY + +class Matchers: + RelSize = re.compile(r"(?P[0-9]+(?:\.[0-9]+)?)?\*(?P[+-][0-9]+)?") + +class BoundExpression: + def __init__(self, expr = None): + self._mult = 0 + self._add = 0 + + if expr: + self.update(expr) + + def set_pair(self, mult, add): + self._mult = mult + self._add = add + + def set(self, expr): + self._mult = expr._mult + self._add = expr._add + + def update(self, expr): + if isinstance(expr, int): + m = None + else: + m = Matchers.RelSize.match(expr) + + if m: + g = m.groupdict() + mult = 1 if not g["mult"] else float(g["mult"]) + add = 0 if not g["add"] else int(g["add"]) + self._mult = mult + self._add = add + else: + self.set_pair(0, int(expr)) + + def calc(self, ref_val): + return int(self._mult * ref_val + self._add) + + def absolute(self): + return self._mult == 0 + + def nullity(self): + return self._mult == 0 and self._add == 0 + + def scale_mult(self, scalar): + self._mult *= scalar + return self + + @staticmethod + def normalise(*exprs): + v = BoundExpression() + for e in exprs: + v += e + return [e.scale_mult(1 / v._mult) for e in exprs] + + def __add__(self, b): + v = BoundExpression() + v.set(self) + v._mult += b._mult + v._add += b._add + return v + + def __sub__(self, b): + v = BoundExpression() + v.set(self) + v._mult -= b._mult + v._add -= b._add + return v + + def __iadd__(self, b): + self._mult += b._mult + self._add += b._add + return self + + def __isub__(self, b): + self._mult -= b._mult + self._add -= b._add + return self + + def __rmul__(self, scalar): + v = BoundExpression() + v.set(self) + v._mult *= scalar + v._add *= scalar + return v + + def __truediv__(self, scalar): + v = BoundExpression() + v.set(self) + v._mult /= float(scalar) + v._add /= scalar + return v + +class DynamicBound: + def __init__(self): + self.__x = BoundExpression() + self.__y = BoundExpression() + + def dyn_x(self): + return self.__x + + def dyn_y(self): + return self.__y + + def resolve(self, ref_w, ref_h): + return (self.__y.calc(ref_h), self.__x.calc(ref_w)) + + def set(self, dyn_bound): + self.__x.set(dyn_bound.dyn_x()) + self.__y.set(dyn_bound.dyn_y()) + +class Bound: + def __init__(self) -> None: + self.__x = 0 + self.__y = 0 + + def shrinkX(self, dx): + self.__x -= dx + def shrinkY(self, dy): + self.__y -= dy + + def growX(self, dx): + self.__x += dx + def growY(self, dy): + self.__y += dy + + def resetX(self, x): + self.__x = x + def resetY(self, y): + self.__y = y + + def update(self, dynsz, ref_bound): + y, x = dynsz.resolve(ref_bound.x(), ref_bound.y()) + self.__x = x + self.__y = y + + def reset(self, x, y): + self.__x, self.__y = x, y + + def x(self): + return self.__x + + def y(self): + return self.__y + + def yx(self, scale = 1): + return int(self.__y * scale), int(self.__x * scale) + +class TuiStackWindow: + def __init__(self, obj) -> None: + self.__obj = obj + self.__win = curses.newwin(0, 0) + self.__pan = cpanel.new_panel(self.__win) + self.__pan.hide() + + def resize(self, h, w): + resize_safe(self.__obj, self.__win, h, w) + + def relocate(self, y, x): + move_safe(self.__obj, self.__pan, y, x) + + def set_geometric(self, h, w, y, x): + resize_safe(self.__obj, self.__win, h, w) + move_safe(self.__obj, self.__pan, y, x) + + def set_background(self, color_scope): + self.__win.bkgd(' ', curses.color_pair(color_scope)) + + def show(self): + self.__pan.show() + + def hide(self): + self.__pan.hide() + + def send_back(self): + self.__pan.bottom() + + def send_front(self): + self.__pan.top() + + def window(self): + return self.__win + +class SpatialObject: + def __init__(self) -> None: + self._local_pos = DynamicBound() + self._pos = Bound() + self._dyn_size = DynamicBound() + self._size = Bound() + self._margin = (0, 0, 0, 0) + self._padding = (0, 0, 0, 0) + self._align = Alignment.TOP | Alignment.LEFT + + def set_local_pos(self, x, y): + self._local_pos.dyn_x().update(x) + self._local_pos.dyn_y().update(y) + + def set_alignment(self, align): + self._align = align + + def set_size(self, w = None, h = None): + if w: + self._dyn_size.dyn_x().update(w) + if h: + self._dyn_size.dyn_y().update(h) + + def set_margin(self, top, right, bottom, left): + self._margin = (top, right, bottom, left) + + def set_padding(self, top, right, bottom, left): + self._padding = (top, right, bottom, left) + + def reset(self): + self._pos.reset(0, 0) + self._size.reset(0, 0) + + def deduce_spatial(self, constrain): + self.reset() + self.__satisfy_bound(constrain) + self.__satisfy_alignment(constrain) + self.__satisfy_margin(constrain) + self.__satisfy_padding(constrain) + + self.__to_corner_pos(constrain) + + def __satisfy_alignment(self, constrain): + local_pos = self._local_pos + cbound = constrain._size + size = self._size + + cy, cx = cbound.yx() + ry, rx = local_pos.resolve(cx, cy) + ay, ax = size.yx(0.5) + + if self._align & Alignment.CENTER: + ax = cx // 2 + ay = cy // 2 + + if self._align & Alignment.BOT: + ay = min(cy - ay, cy - 1) + ry = -ry + elif self._align & Alignment.TOP: + ay = size.y() // 2 + + if self._align & Alignment.RIGHT: + ax = cx - ax + rx = -rx + elif self._align & Alignment.LEFT: + ax = size.x() // 2 + + self._pos.reset(ax + rx, ay + ry) + + def __satisfy_margin(self, constrain): + tm, lm, bm, rm = self._margin + + self._pos.growX(rm - lm) + self._pos.growY(bm - tm) + + def __satisfy_padding(self, constrain): + csize = constrain._size + ch, cw = csize.yx() + h, w = self._size.yx(0.5) + y, x = self._pos.yx() + + tp, lp, bp, rp = self._padding + + if not (tp or lp or bp or rp): + return + + dtp = min(y - h, tp) - tp + dbp = min(ch - (y + h), bp) - bp + + dlp = min(x - w, lp) - lp + drp = min(cw - (x + w), rp) - rp + + self._size.growX(drp + dlp) + self._size.growY(dtp + dbp) + + def __satisfy_bound(self, constrain): + self._size.update(self._dyn_size, constrain._size) + + def __to_corner_pos(self, constrain): + h, w = self._size.yx(0.5) + g_pos = constrain._pos + + self._pos.shrinkX(w) + self._pos.shrinkY(h) + + self._pos.growX(g_pos.x()) + self._pos.growY(g_pos.y()) + + +class TuiObject(SpatialObject): + def __init__(self, context, id): + super().__init__() + self._id = id + self._context = context + self._parent = None + self._visible = True + self._focused = False + + def set_parent(self, parent): + self._parent = parent + + def canvas(self): + if self._parent: + return self._parent.canvas() + return (self, self._context.window()) + + def context(self): + return self._context + + def session(self): + return self._context.session() + + def on_create(self): + pass + + def on_destory(self): + pass + + def on_quit(self): + pass + + def on_layout(self): + if self._parent: + self.deduce_spatial(self._parent) + + def on_draw(self): + pass + + def on_event(self, ev_type, ev_arg): + pass + + def on_focused(self): + self._focused = True + + def on_focus_lost(self): + self._focused = False + + def set_visbility(self, visible): + self._visible = visible + + def do_draw(self): + if self._visible: + self.on_draw() + + def do_layout(self): + self.on_layout() + +class TuiWidget(TuiObject): + def __init__(self, context, id): + super().__init__(context, id) + + def on_layout(self): + super().on_layout() + + co, _ = self.canvas() + + y, x = co._pos.yx() + self._pos.shrinkX(x) + self._pos.shrinkY(y) + +class TuiContainerObject(TuiObject): + def __init__(self, context, id): + super().__init__(context, id) + self._children = [] + + def add(self, child): + child.set_parent(self) + self._children.append(child) + + def children(self): + return self._children + + def on_create(self): + super().on_create() + for child in self._children: + child.on_create() + + def on_destory(self): + super().on_destory() + for child in self._children: + child.on_destory() + + def on_quit(self): + super().on_quit() + for child in self._children: + child.on_quit() + + def on_layout(self): + super().on_layout() + for child in self._children: + child.do_layout() + + def on_draw(self): + super().on_draw() + for child in self._children: + child.do_draw() + + def on_event(self, ev_type, ev_arg): + super().on_event(ev_type, ev_arg) + for child in self._children: + child.on_event(ev_type, ev_arg) + + +class TuiScrollable(TuiObject): + def __init__(self, context, id): + super().__init__(context, id) + self.__spos = Bound() + + self.__pad = curses.newpad(1, 1) + self.__pad.bkgd(' ', curses.color_pair(ColorScope.PANEL)) + self.__pad_panel = cpanel.new_panel(self.__pad) + self.__content = None + + def canvas(self): + return (self, self.__pad) + + def set_content(self, content): + self.__content = content + self.__content.set_parent(self) + + def set_scrollY(self, y): + self.__spos.resetY(y) + + def set_scrollX(self, x): + self.__spos.resetX(x) + + def reached_last(self): + off = self.__spos.y() + self._size.y() + return off >= self.__content._size.y() + + def reached_top(self): + return self.__spos.y() < self._size.y() + + def on_layout(self): + super().on_layout() + + if not self.__content: + return + + self.__content.on_layout() + + h, w = self._size.yx() + ch, cw = self.__content._size.yx() + sh, sw = max(ch, h), max(cw, w) + + self.__spos.resetX(min(self.__spos.x(), max(cw, w) - w)) + self.__spos.resetY(min(self.__spos.y(), max(ch, h) - h)) + + resize_safe(self, self.__pad, sh, sw) + + def on_draw(self): + if not self.__content: + return + + self.__pad.erase() + + self.__pad_panel.top() + self.__content.on_draw() + + wminy, wminx = self._pos.yx() + wmaxy, wmaxx = self._size.yx() + wmaxy, wmaxx = wmaxy + wminy, wmaxx + wminx + self.__pad.touchwin() + self.__pad.refresh(*self.__spos.yx(), + wminy, wminx, wmaxy - 1, wmaxx - 1) + + +class Layout(TuiContainerObject): + class Cell(TuiObject): + def __init__(self, context): + super().__init__(context, "cell") + self.__obj = None + + def set_obj(self, obj): + self.__obj = obj + self.__obj.set_parent(self) + + def on_create(self): + if self.__obj: + self.__obj.on_create() + + def on_destory(self): + if self.__obj: + self.__obj.on_destory() + + def on_quit(self): + if self.__obj: + self.__obj.on_quit() + + def on_layout(self): + super().on_layout() + if self.__obj: + self.__obj.do_layout() + + def on_draw(self): + if self.__obj: + self.__obj.do_draw() + + def on_event(self, ev_type, ev_arg): + if self.__obj: + self.__obj.on_event(ev_type, ev_arg) + + def __init__(self, context, id, ratios): + super().__init__(context, id) + + rs = [BoundExpression(r) for r in ratios.split(',')] + self._rs = BoundExpression.normalise(*rs) + + for _ in range(len(self._rs)): + cell = Layout.Cell(self._context) + super().add(cell) + + self._adjust_to_fit() + + def _adjust_to_fit(self): + pass + + def add(self, child): + raise RuntimeError("invalid operation") + + def set_cell(self, i, obj): + if i > len(self._children): + raise ValueError(f"cell #{i} out of bound") + + self._children[i].set_obj(obj) + + +class FlexLinearLayout(Layout): + LANDSCAPE = 0 + PORTRAIT = 1 + def __init__(self, context, id, ratios): + self.__horizontal = False + + super().__init__(context, id, ratios) + + def orientation(self, orient): + self.__horizontal = orient == FlexLinearLayout.LANDSCAPE + + def on_layout(self): + self.__apply_ratio() + super().on_layout() + + def _adjust_to_fit(self): + sum_abs = BoundExpression() + i = 0 + for r in self._rs: + if r.absolute(): + sum_abs += r + else: + i += 1 + + sum_abs /= i + for i, r in enumerate(self._rs): + if not r.absolute(): + self._rs[i] -= sum_abs + + def __apply_ratio(self): + if self.__horizontal: + self.__adjust_horizontal() + else: + self.__adjust_vertical() + + def __adjust_horizontal(self): + acc = BoundExpression() + for r, cell in zip(self._rs, self.children()): + cell._dyn_size.dyn_y().set_pair(1, 0) + cell._dyn_size.dyn_x().set(r) + + cell.set_alignment(Alignment.LEFT) + cell._local_pos.dyn_y().set_pair(0, 0) + cell._local_pos.dyn_x().set(acc) + + acc += r + + def __adjust_vertical(self): + acc = BoundExpression() + for r, cell in zip(self._rs, self.children()): + cell._dyn_size.dyn_x().set_pair(1, 0) + cell._dyn_size.dyn_y().set(r) + + cell.set_alignment(Alignment.TOP | Alignment.CENTER) + cell._local_pos.dyn_x().set_pair(0, 0) + cell._local_pos.dyn_y().set(acc) + + acc += r + + +class TuiPanel(TuiContainerObject): + def __init__(self, context, id): + super().__init__(context, id) + + self.__use_border = False + self.__use_shadow = False + self.__shadow_param = (0, 0) + + self.__swin = TuiStackWindow(self) + self.__shad = TuiStackWindow(self) + + self.__swin.set_background(ColorScope.PANEL) + self.__shad.set_background(ColorScope.SHADOW) + + def canvas(self): + return (self, self.__swin.window()) + + def drop_shadow(self, off_y, off_x): + self.__shadow_param = (off_y, off_x) + self.__use_shadow = not (off_y == off_x and off_y == 0) + + def border(self, _b): + self.__use_border = _b + + def bkgd_override(self, scope): + self.__swin.set_background(scope) + + def on_layout(self): + super().on_layout() + + self.__swin.hide() + + h, w = self._size.y(), self._size.x() + y, x = self._pos.y(), self._pos.x() + self.__swin.set_geometric(h, w, y, x) + + if self.__use_shadow: + sy, sx = self.__shadow_param + self.__shad.set_geometric(h, w, y + sy, x + sx) + + def on_destory(self): + super().on_destory() + self.__swin.hide() + self.__shad.hide() + + def on_draw(self): + win = self.__swin.window() + win.erase() + + if self.__use_border: + win.border() + + if self.__use_shadow: + self.__shad.show() + else: + self.__shad.hide() + + self.__swin.show() + self.__swin.send_front() + + super().on_draw() + + win.touchwin() + +class TuiLabel(TuiWidget): + def __init__(self, context, id): + super().__init__(context, id) + self._text = "TuiLabel" + self._wrapped = [] + + self.__auto_fit = True + self.__trunc = False + self.__dopad = False + self.__highlight = False + self.__color_scope = -1 + + def __try_fit_text(self, txt): + if self.__auto_fit: + self._dyn_size.dyn_x().set_pair(0, len(txt)) + self._dyn_size.dyn_y().set_pair(0, 1) + + def __pad_text(self): + for i, t in enumerate(self._wrapped): + self._wrapped[i] = str.rjust(t, self._size.x()) + + def set_text(self, text): + self._text = text + self.__try_fit_text(text) + + def override_color(self, color = -1): + self.__color_scope = color + + def auto_fit(self, _b): + self.__auto_fit = _b + + def truncate(self, _b): + self.__trunc = _b + + def hightlight(self, _b): + self.__highlight = _b + + def pad_around(self, _b): + self.__dopad = _b + + def on_layout(self): + txt = self._text + if self.__dopad: + txt = f" {txt} " + self.__try_fit_text(txt) + + super().on_layout() + + if len(txt) <= self._size.x(): + self._wrapped = [txt] + self.__pad_text() + return + + if not self.__trunc: + txt = txt[:self._size.x() - 1] + self._wrapped = [txt] + self.__pad_text() + return + + self._wrapped = textwrap.wrap(txt, self._size.x()) + self.__pad_text() + + def on_draw(self): + _, win = self.canvas() + y, x = self._pos.yx() + + if self.__color_scope != -1: + color = curses.color_pair(self.__color_scope) + elif self.__highlight: + color = curses.color_pair(ColorScope.TEXT_HI) + else: + color = curses.color_pair(ColorScope.TEXT) + + for i, t in enumerate(self._wrapped): + addstr_safe(self, win, y + i, x, t, color) + + +class TuiTextBlock(TuiWidget): + def __init__(self, context, id): + super().__init__(context, id) + self.__lines = [] + self.__wrapped = [] + self.__fit_to_height = False + + def set_text(self, text): + text = textwrap.dedent(text) + self.__lines = text.split('\n') + if self.__fit_to_height: + self._dyn_size.dyn_y().set_pair(0, 0) + + def height_auto_fit(self, yes): + self.__fit_to_height = yes + + def on_layout(self): + super().on_layout() + + self.__wrapped.clear() + for t in self.__lines: + if not t: + self.__wrapped.append(t) + continue + wrap = textwrap.wrap(t, self._size.x()) + self.__wrapped += wrap + + if self._dyn_size.dyn_y().nullity(): + h = len(self.__wrapped) + self._dyn_size.dyn_y().set_pair(0, h) + + # redo layouting + super().on_layout() + + def on_draw(self): + _, win = self.canvas() + y, x = self._pos.yx() + + color = curses.color_pair(ColorScope.TEXT) + for i, t in enumerate(self.__wrapped): + addstr_safe(self, win, y + i, x, t, color) + + +class TuiTextBox(TuiWidget): + def __init__(self, context, id): + super().__init__(context, id) + self.__box = TuiStackWindow(self) + self.__box.set_background(ColorScope.PANEL) + self.__textb = textpad.Textbox(self.__box.window(), True) + self.__textb.stripspaces = True + self.__str = "" + self.__scheduled_edit = False + + self._context.focus_group().register(self, 0) + + def __validate(self, x): + if x == 10 or x == 9: + return 7 + return x + + def on_layout(self): + super().on_layout() + + co, _ = self.canvas() + h, w = self._size.yx() + y, x = self._pos.yx() + cy, cx = co._pos.yx() + y, x = y + cy, x + cx + + self.__box.hide() + self.__box.set_geometric(1, w - 1, y + h // 2, x + 1) + + def on_draw(self): + self.__box.show() + self.__box.send_front() + + _, cwin = self.canvas() + + h, w = self._size.yx() + y, x = self._pos.yx() + textpad.rectangle(cwin, y, x, y + h - 1, x+w) + + win = self.__box.window() + win.touchwin() + + def __edit(self): + self.__str = self.__textb.edit(lambda x: self.__validate(x)) + self.session().schedule(EventType.E_CHFOCUS) + self.__scheduled_edit = False + + def get_text(self): + return self.__str + + def on_focused(self): + self.__box.set_background(ColorScope.BOX) + if not self.__scheduled_edit: + # edit will block, defer to next update cycle + self.session().schedule_task(self.__edit) + self.__scheduled_edit = True + + def on_focus_lost(self): + self.__box.set_background(ColorScope.PANEL) + + +class SimpleList(TuiWidget): + class Item: + def __init__(self) -> None: + pass + def get_text(self): + return "list_item" + def on_selected(self): + pass + def on_key_pressed(self, key): + pass + + def __init__(self, context, id): + super().__init__(context, id) + self.__items = [] + self.__selected = 0 + self.__on_sel_confirm_cb = None + self.__on_sel_change_cb = None + + self._context.focus_group().register(self) + + def set_onselected_cb(self, cb): + self.__on_sel_confirm_cb = cb + + def set_onselection_change_cb(self, cb): + self.__on_sel_change_cb = cb + + def count(self): + return len(self.__items) + + def index(self): + return self.__selected + + def add_item(self, item): + self.__items.append(item) + + def clear(self): + self.__items.clear() + + def on_layout(self): + super().on_layout() + self.__selected = min(self.__selected, len(self.__items)) + self._size.resetY(len(self.__items) + 1) + + def on_draw(self): + _, win = self.canvas() + w = self._size.x() + + for i, item in enumerate(self.__items): + color = curses.color_pair(ColorScope.TEXT) + if i == self.__selected: + if self._focused: + color = curses.color_pair(ColorScope.SELECT) + else: + color = curses.color_pair(ColorScope.BOX) + + txt = str.ljust(item.get_text(), w) + txt = txt[:w] + addstr_safe(self, win, i, 0, txt, color) + + def on_event(self, ev_type, ev_arg): + if not EventType.key_press(ev_type): + return + + if len(self.__items) == 0: + return + + sel = self.__items[self.__selected] + + if ev_arg == 10: + sel.on_selected() + + if self.__on_sel_confirm_cb: + self.__on_sel_confirm_cb(self, self.__selected, sel) + return + + sel.on_key_pressed(ev_arg) + + if (ev_arg != curses.KEY_DOWN and + ev_arg != curses.KEY_UP): + return + + prev = self.__selected + if ev_arg == curses.KEY_DOWN: + self.__selected += 1 + else: + self.__selected -= 1 + + self.__selected = max(self.__selected, 0) + self.__selected = self.__selected % len(self.__items) + + if self.__on_sel_change_cb: + self.__on_sel_change_cb(self, prev, self.__selected) + + +class TuiButton(TuiLabel): + def __init__(self, context, id): + super().__init__(context, id) + self.__onclick = None + + context.focus_group().register(self) + + def set_text(self, text): + return super().set_text(f"<{text}>") + + def set_click_callback(self, cb): + self.__onclick = cb + + def hightlight(self, _b): + raise NotImplemented() + + def on_draw(self): + _, win = self.canvas() + y, x = self._pos.yx() + + if self._focused: + color = curses.color_pair(ColorScope.SELECT) + else: + color = curses.color_pair(ColorScope.TEXT) + + addstr_safe(self, win, y, x, self._wrapped[0], color) + + def on_event(self, ev_type, ev_arg): + if not EventType.focused_only(ev_type): + return + if not EventType.key_press(ev_type): + return + + if ev_arg == ord('\n') and self.__onclick: + self.__onclick(self) + + +class TuiSession: + def __init__(self) -> None: + self.stdsc = curses.initscr() + curses.start_color() + + curses.noecho() + curses.cbreak() + + self.__context_stack = [] + self.__sched_events = [] + + ws = self.window_size() + self.__win = curses.newwin(*ws) + self.__winbg = curses.newwin(*ws) + self.__panbg = cpanel.new_panel(self.__winbg) + + self.__winbg.bkgd(' ', curses.color_pair(ColorScope.WIN)) + + self.__win.timeout(50) + self.__win.keypad(True) + + def window_size(self): + return self.stdsc.getmaxyx() + + def set_color(self, scope, fg, bg): + curses.init_pair(scope, int(fg), int(bg)) + + def schedule_redraw(self): + self.schedule(EventType.E_REDRAW) + + def schedule_task(self, task): + self.schedule(EventType.E_REDRAW) + self.schedule(EventType.E_TASK, task) + + def schedule(self, event, arg = None): + if len(self.__sched_events) > 0: + if self.__sched_events[-1] == event: + return + + self.__sched_events.append((event, arg)) + + def push_context(self, tuictx): + tuictx.prepare() + self.__context_stack.append(tuictx) + self.schedule(EventType.E_REDRAW) + + curses.curs_set(self.active().curser_mode()) + + def pop_context(self): + if len(self.__context_stack) == 1: + return + + ctx = self.__context_stack.pop() + ctx.on_destory() + self.schedule(EventType.E_REDRAW) + + curses.curs_set(self.active().curser_mode()) + + def active(self): + return self.__context_stack[-1] + + def event_loop(self): + if len(self.__context_stack) == 0: + raise RuntimeError("no tui context to display") + + while True: + key = self.__win.getch() + if key != -1: + self.schedule(EventType.E_KEY, key) + + if len(self.__sched_events) == 0: + continue + + evt, arg = self.__sched_events.pop(0) + if evt == EventType.E_REDRAW: + self.__redraw() + elif evt == EventType.E_QUIT: + self.__notify_quit() + break + + self.active().dispatch_event(evt, arg) + + def __notify_quit(self): + while len(self.__context_stack) == 0: + ctx = self.__context_stack.pop() + ctx.dispatch_event(EventType.E_QUIT, None) + + def __redraw(self): + self.stdsc.erase() + self.__win.erase() + + self.active().redraw(self.__win) + + self.__panbg.bottom() + self.__win.touchwin() + self.__winbg.touchwin() + + self.__win.refresh() + self.__winbg.refresh() + + cpanel.update_panels() + curses.doupdate() + +class TuiFocusGroup: + def __init__(self) -> None: + self.__grp = [] + self.__id = 0 + self.__sel = 0 + self.__focused = None + + def register(self, tui_obj, pos=-1): + if pos == -1: + self.__grp.append((self.__id, tui_obj)) + else: + self.__grp.insert(pos, (self.__id, tui_obj)) + self.__id += 1 + return self.__id - 1 + + def navigate_focus(self, dir = 1): + self.__sel = (self.__sel + dir) % len(self.__grp) + f = None if not len(self.__grp) else self.__grp[self.__sel][1] + if f and f != self.__focused: + if self.__focused: + self.__focused.on_focus_lost() + f.on_focused() + self.__focused = f + + def focused(self): + return self.__focused + +class TuiContext(TuiObject): + def __init__(self, session: TuiSession): + super().__init__(self, "context") + self.__root = None + self.__sobj = None + self.__session = session + + self.__win = None + + y, x = self.__session.window_size() + self._size.reset(x, y) + self.set_parent(None) + + self.__focus_group = TuiFocusGroup() + self.__curser_mode = 0 + + def set_curser_mode(self, mode): + self.__curser_mode = mode + + def curser_mode(self): + return self.__curser_mode + + def set_root(self, root): + self.__root = root + self.__root.set_parent(self) + + def set_state(self, obj): + self.__sobj = obj + + def state(self): + return self.__sobj + + def prepare(self): + self.__root.on_create() + + def on_destory(self): + self.__root.on_destory() + + def canvas(self): + return (self, self.__win) + + def session(self): + return self.__session + + def dispatch_event(self, evt, arg): + if evt == EventType.E_REDRAW: + self.__focus_group.navigate_focus(0) + elif evt == EventType.E_CHFOCUS: + self.__focus_group.navigate_focus(1) + self.__session.schedule(EventType.E_REDRAW) + return + elif evt == EventType.E_TASK: + arg() + elif evt == EventType.E_QUIT: + self.__root.on_quit() + elif evt == EventType.E_KEY: + self._handle_key_event(arg) + else: + self.__root.on_event(evt, arg) + + focused = self.__focus_group.focused() + if focused: + focused.on_event(evt | EventType.E_M_FOCUS, arg) + + def redraw(self, win): + self.__win = win + self.on_layout() + self.on_draw() + + def on_layout(self): + self.__root.on_layout() + + def on_draw(self): + self.__root.on_draw() + + def focus_group(self): + return self.__focus_group + + def _handle_key_event(self, key): + if key == ord('\t') or key == curses.KEY_RIGHT: + self.__focus_group.navigate_focus() + elif key == curses.KEY_LEFT: + self.__focus_group.navigate_focus(-1) + else: + self.__root.on_event(EventType.E_KEY, key) + + if self.__focus_group.focused(): + self.__session.schedule(EventType.E_REDRAW) \ No newline at end of file diff --git a/lunaix-os/scripts/build-tools/integration/lunamenu.py b/lunaix-os/scripts/build-tools/integration/lunamenu.py new file mode 100644 index 0000000..c8d0730 --- /dev/null +++ b/lunaix-os/scripts/build-tools/integration/lunamenu.py @@ -0,0 +1,457 @@ +from lcfg.api import RenderContext +from lcfg.types import ( + PrimitiveType, + MultipleChoiceType +) + +import subprocess +import curses +import textwrap +import integration.libtui as tui +import integration.libmenu as menu + +from integration.libtui import ColorScope, TuiColor, Alignment, EventType +from integration.libmenu import Dialogue, ListView, show_dialog + +__git_repo_info = None +__tainted = False + +def mark_tainted(): + global __tainted + __tainted = True + +def unmark_tainted(): + global __tainted + __tainted = False + +def get_git_hash(): + try: + hsh = subprocess.check_output([ + 'git', 'rev-parse', '--short', 'HEAD' + ]).decode('ascii').strip() + branch = subprocess.check_output([ + 'git', 'branch', '--show-current' + ]).decode('ascii').strip() + return f"{branch}@{hsh}" + except: + return None + +def get_git_info(): + global __git_repo_info + return __git_repo_info + +def do_save(session): + show_dialog(session, "Notice", "Configuration saved") + unmark_tainted() + +def do_exit(session): + global __tainted + if not __tainted: + session.schedule(EventType.E_QUIT) + return + + quit = QuitDialogue(session) + quit.show() + +class MainMenuContext(tui.TuiContext): + def __init__(self, session, view_title): + super().__init__(session) + + self.__title = view_title + + self.__prepare_layout() + + def __prepare_layout(self): + + root = tui.TuiPanel(self, "main_panel") + root.set_size("*-10", "*-5") + root.set_alignment(Alignment.CENTER) + root.drop_shadow(1, 2) + root.border(True) + + layout = tui.FlexLinearLayout(self, "layout", "6,*,5") + layout.orientation(tui.FlexLinearLayout.PORTRAIT) + layout.set_size("*", "*") + layout.set_padding(1, 1, 1, 1) + + listv = ListView(self, "list_view") + listv.set_size("70", "*") + listv.set_alignment(Alignment.CENTER) + + hint = tui.TuiTextBlock(self, "hint") + hint.set_size(w="*") + hint.set_local_pos("0.1*", 0) + hint.height_auto_fit(True) + hint.set_text( + "Use // to select from list\n" + "Use // to change focus\n" + ": show help (if applicable), : back previous level" + ) + hint.set_alignment(Alignment.CENTER | Alignment.LEFT) + + suffix = "" + btns_defs = [ + { + "text": "Save", + "onclick": lambda x: do_save(self.session()) + }, + { + "text": "Exit", + "onclick": lambda x: do_exit(self.session()) + } + ] + + repo_info = get_git_info() + + if self.__title: + suffix += f" - {self.__title}" + btns_defs.insert(1, { + "text": "Back", + "onclick": lambda x: self.session().pop_context() + }) + + btns = menu.create_buttons(self, btns_defs, sizes="50,*") + + layout.set_cell(0, hint) + layout.set_cell(1, listv) + layout.set_cell(2, btns) + + t = menu.create_title(self, "Lunaix Kernel Configuration" + suffix) + t2 = menu.create_title(self, repo_info) + t2.set_alignment(Alignment.BOT | Alignment.RIGHT) + + root.add(t) + root.add(t2) + root.add(layout) + + self.set_root(root) + self.__menu_list = listv + + def menu(self): + return self.__menu_list + + def _handle_key_event(self, key): + if key == curses.KEY_BACKSPACE or key == 8: + self.session().pop_context() + elif key == 27: + do_exit(self.session()) + return + + super()._handle_key_event(key) + +class ItemType: + Expandable = 0 + Switch = 1 + Choice = 2 + Other = 3 + def __init__(self, node, expandable) -> None: + self.__node = node + + if expandable: + self.__type = ItemType.Expandable + return + + self.__type = ItemType.Other + self.__primitive = False + type_provider = node.get_type() + + if isinstance(type_provider, PrimitiveType): + self.__primitive = True + + if isinstance(type_provider, MultipleChoiceType): + self.__type = ItemType.Choice + elif type_provider._type == bool: + self.__type = ItemType.Switch + + self.__provider = type_provider + + def get_formatter(self): + if self.__type == ItemType.Expandable: + return "%s ---->" + + v = self.__node.get_value() + + if self.is_switch(): + mark = "*" if v else " " + return f"[{mark}] %s" + + if self.is_choice() or isinstance(v, int): + return f"({v}) %s" + + return "%s" + + def expandable(self): + return self.__type == ItemType.Expandable + + def is_switch(self): + return self.__type == ItemType.Switch + + def is_choice(self): + return self.__type == ItemType.Choice + + def read_only(self): + return not self.expandable() and self.__node.read_only() + + def provider(self): + return self.__provider + +class MultiChoiceItem(tui.SimpleList.Item): + def __init__(self, value, get_val) -> None: + super().__init__() + self.__val = value + self.__getval = get_val + + def get_text(self): + marker = "*" if self.__getval() == self.__val else " " + return f" ({marker}) {self.__val}" + + def value(self): + return self.__val + +class LunaConfigItem(tui.SimpleList.Item): + def __init__(self, session, node, name, expand_cb = None): + super().__init__() + self.__node = node + self.__type = ItemType(node, expand_cb is not None) + self.__name = name + self.__expand_cb = expand_cb + self.__session = session + + def get_text(self): + fmt = self.__type.get_formatter() + if self.__type.read_only(): + fmt += "*" + return f" {fmt%(self.__name)}" + + def on_selected(self): + if self.__type.read_only(): + show_dialog( + self.__session, + f"Read-only: \"{self.__name}\"", + f"Value defined in this field:\n\n'{self.__node.get_value()}'") + return + + if self.__type.expandable(): + view = CollectionView(self.__session, self.__node, self.__name) + view.set_reloader(self.__expand_cb) + view.show() + return + + if self.__type.is_switch(): + v = self.__node.get_value() + self.change_value(not v) + else: + dia = ValueEditDialogue(self.__session, self) + dia.show() + + self.__session.schedule(EventType.E_REDRAW) + + def name(self): + return self.__name + + def node(self): + return self.__node + + def type(self): + return self.__type + + def change_value(self, val): + try: + self.__node.set_value(val) + except: + show_dialog( + self.__session, "Invalid value", + f"Value: '{val}' does not match the type") + return False + + mark_tainted() + CollectionView.reload_active(self.__session) + return True + + def on_key_pressed(self, key): + if (key & ~0b100000) != ord('H'): + return + + h = self.__node.help_prompt() + if not self.__type.expandable(): + h = "\n".join([ + h, "", "--------", + "Supported Values:", + textwrap.indent(str(self.__type.provider()), " ") + ]) + + dia = HelpDialogue(self.__session, f"Help: '{self.__name}'", h) + dia.show() + +class CollectionView(RenderContext): + def __init__(self, session, node, label = None) -> None: + super().__init__() + + ctx = MainMenuContext(session, label) + self.__node = node + self.__tui_ctx = ctx + self.__listv = ctx.menu() + self.__session = session + self.__reloader = lambda x: node.render(x) + + ctx.set_state(self) + + def set_reloader(self, cb): + self.__reloader = cb + + def add_expandable(self, label, node, on_expand_cb): + item = LunaConfigItem(self.__session, node, label, on_expand_cb) + self.__listv.add_item(item) + + def add_field(self, label, node): + item = LunaConfigItem(self.__session, node, label) + self.__listv.add_item(item) + + def show(self): + self.reload() + self.__session.push_context(self.__tui_ctx) + + def reload(self): + self.__listv.clear() + self.__reloader(self) + self.__session.schedule(EventType.E_REDRAW) + + @staticmethod + def reload_active(session): + state = session.active().state() + if isinstance(state, CollectionView): + state.reload() + +class ValueEditDialogue(menu.Dialogue): + def __init__(self, session, item: LunaConfigItem): + name = item.name() + title = f"Edit \"{name}\"" + super().__init__(session, title, None, False, + "Confirm", "Cancle") + + self.__item = item + self.__value = item.node().get_value() + + self.decide_content() + + def __get_val(self): + return self.__value + + def decide_content(self): + if not self.__item.type().is_choice(): + self.set_input_dialogue(True) + return + + listv = ListView(self.context(), "choices") + listv.set_size("0.8*", "*") + listv.set_alignment(Alignment.CENTER) + listv.set_onselected_cb(self.__on_selected) + + for t in self.__item.type().provider()._type: + listv.add_item(MultiChoiceItem(t, self.__get_val)) + + self.set_content(listv) + self.set_size() + + def __on_selected(self, listv, index, item): + self.__value = item.value() + + def _ok_onclick(self): + if self._textbox: + self.__value = self._textbox.get_text() + + if self.__item.change_value(self.__value): + super()._ok_onclick() + +class QuitDialogue(menu.Dialogue): + def __init__(self, session): + super().__init__(session, + "Quit ?", "Unsaved changes, sure to quit?", False, + "Quit Anyway", "No", "Save and Quit") + + def _ok_onclick(self): + self.session().schedule(EventType.E_QUIT) + + def _abort_onclick(self): + unmark_tainted() + self._ok_onclick() + + +class HelpDialogue(menu.Dialogue): + def __init__(self, session, title="", content=""): + super().__init__(session, title, None, no_btn=None) + + self.__content = content + self.__scroll_y = 0 + self.set_local_pos(0, -2) + + def prepare(self): + tb = tui.TuiTextBlock(self._context, "content") + tb.set_size(w="70") + tb.set_text(self.__content) + tb.height_auto_fit(True) + self.__tb = tb + + self.__scroll = tui.TuiScrollable(self._context, "scroll") + self.__scroll.set_size("65", "*") + self.__scroll.set_alignment(Alignment.CENTER) + self.__scroll.set_content(tb) + + self.set_size(w="75") + self.set_content(self.__scroll) + + super().prepare() + + def _handle_key_event(self, key): + if key == curses.KEY_UP: + self.__scroll_y = max(self.__scroll_y - 1, 0) + self.__scroll.set_scrollY(self.__scroll_y) + elif key == curses.KEY_DOWN: + y = self.__tb._size.y() + self.__scroll_y = min(self.__scroll_y + 1, y) + self.__scroll.set_scrollY(self.__scroll_y) + super()._handle_key_event(key) + +class TerminalSizeCheckFailed(Exception): + def __init__(self, *args: object) -> None: + super().__init__(*args) + +def main(_, root_node): + global __git_repo_info + + __git_repo_info = get_git_hash() + + session = tui.TuiSession() + + h, w = session.window_size() + if h < 30 or w < 85: + raise TerminalSizeCheckFailed((90, 40), (w, h)) + + base_background = TuiColor.white.bright() + session.set_color(ColorScope.WIN, + TuiColor.black, TuiColor.blue) + session.set_color(ColorScope.PANEL, + TuiColor.black, base_background) + session.set_color(ColorScope.TEXT, + TuiColor.black, base_background) + session.set_color(ColorScope.TEXT_HI, + TuiColor.magenta, base_background) + session.set_color(ColorScope.SHADOW, + TuiColor.black, TuiColor.black) + session.set_color(ColorScope.SELECT, + TuiColor.white, TuiColor.black.bright()) + session.set_color(ColorScope.HINT, + TuiColor.cyan, base_background) + session.set_color(ColorScope.BOX, + TuiColor.black, TuiColor.white) + + main_view = CollectionView(session, root_node) + main_view.show() + + session.event_loop() + +def menuconfig(root_node): + global __tainted + curses.wrapper(main, root_node) + + return not __tainted \ No newline at end of file diff --git a/lunaix-os/scripts/build-tools/lcfg/types.py b/lunaix-os/scripts/build-tools/lcfg/types.py index 123f6a9..8fc373d 100644 --- a/lunaix-os/scripts/build-tools/lcfg/types.py +++ b/lunaix-os/scripts/build-tools/lcfg/types.py @@ -70,8 +70,8 @@ class MultipleChoiceType(PrimitiveType): return None in self._type def __str__(self) -> str: - accepted = [f" {t}" for t in self._type] + accepted = [f" * {t}" for t in self._type] return "\n".join([ - "choose one: \n", + "choose one:", *accepted ]) diff --git a/lunaix-os/scripts/build-tools/luna_build.py b/lunaix-os/scripts/build-tools/luna_build.py index e3c6908..ce3be82 100755 --- a/lunaix-os/scripts/build-tools/luna_build.py +++ b/lunaix-os/scripts/build-tools/luna_build.py @@ -8,6 +8,7 @@ from integration.config_io import CHeaderConfigProvider from integration.lbuild_bridge import LConfigProvider from integration.render_ishell import InteractiveShell from integration.build_gen import MakefileBuildGen, install_lbuild_functions +from integration.lunamenu import menuconfig, TerminalSizeCheckFailed import lcfg.types as lcfg_type import lcfg.builtins as builtin @@ -37,12 +38,23 @@ def prepare_lconfig_env(out_dir): def do_config(opt, lcfg_env): redo_config = not exists(opt.config_save) or opt.force - if not redo_config: + if not redo_config or opt.quiet: return + + try: + clean_quit = menuconfig(lcfg_env) + except TerminalSizeCheckFailed as e: + least = e.args[0] + current = e.args[1] + print( + f"Your terminal size: {current} is less than minimum requirement of {least}.\n" + "menuconfig will not function properly, switch to prompt based.\n") + + shell = InteractiveShell(lcfg_env) + clean_quit = shell.render_loop() - shell = InteractiveShell(lcfg_env) - if not shell.render_loop(): - print("Configuration aborted.") + if not clean_quit: + print("Configuration aborted. Nothing has been saved.") exit(-1) def do_buildfile_gen(opts, lcfg_env): @@ -71,6 +83,7 @@ def do_buildfile_gen(opts, lcfg_env): def main(): parser = ArgumentParser() parser.add_argument("--config", action="store_true", default=False) + parser.add_argument("--quiet", action="store_true", default=False) parser.add_argument("--lconfig-file", default="LConfig") parser.add_argument("--config-save", default=".config.json") parser.add_argument("--force", action="store_true", default=False) diff --git a/lunaix-os/scripts/qemu.py b/lunaix-os/scripts/qemu.py index f2808e5..f12881a 100755 --- a/lunaix-os/scripts/qemu.py +++ b/lunaix-os/scripts/qemu.py @@ -3,8 +3,10 @@ import subprocess, time, os, re, argparse, json from pathlib import PurePosixPath import logging +import uuid logger = logging.getLogger("auto_qemu") +logging.basicConfig(level=logging.INFO) g_lookup = {} @@ -51,12 +53,89 @@ def get_config(opt, path, default=None, required=False): def join_attrs(attrs): return ",".join(attrs) -def parse_protocol(opt): - protocol = get_config(opt, "protocol", "telnet") - addr = get_config(opt, "addr", ":12345") - logfile = get_config(opt, "logfile") +def get_uniq(): + return uuid.uuid4().hex[:8] - return (f"{protocol}:{addr}", logfile) +def map_bool(b): + return "on" if b else "off" + + + +################################# +# IO Backend Definitions +# + +class IOBackend: + def __init__(self, opt, id_prefix="io") -> None: + self._type = get_config(opt, "type", required=True) + self._logfile = get_config(opt, "logfile") + self._id = f"{id_prefix}.{get_uniq()}" + + def get_options(self): + opts = [] + if self._logfile: + opts.append(f"logfile={self._logfile}") + return opts + + def to_cmdline(self): + return join_attrs([ + self._type, f"id={self._id}", *self.get_options() + ]) + + def name(self): + return self._id + +class FileIOBackend(IOBackend): + def __init__(self, opt) -> None: + super().__init__(opt) + self.__path = get_config(opt, "path", required=True) + + def get_options(self): + opts = [ + f"path={self.__path}" + ] + return opts + super().get_options() + +class SocketIOBackend(IOBackend): + def __init__(self, opt) -> None: + super().__init__(opt) + self.__protocol = self._type + self._type = "socket" + self.__host = get_config(opt, "host", default="localhost") + self.__port = get_config(opt, "port", required=True) + self.__server = bool(get_config(opt, "server", True)) + self.__wait = bool(get_config(opt, "wait", True)) + + def get_options(self): + opts = [ + f"host={self.__host}", + f"port={self.__port}", + f"server={map_bool(self.__server)}", + f"wait={map_bool(self.__wait)}", + ] + if self.__protocol == "telnet": + opts.append("telnet=on") + if self.__protocol == "ws": + opts.append("websocket=on") + return opts + super().get_options() + +def select_backend(opt): + bopt = get_config(opt, "io", required=True) + backend_type = get_config(bopt, "type", required=True) + + if backend_type in ["telnet", "ws", "tcp"]: + return SocketIOBackend(bopt) + + if backend_type in ["file", "pipe", "serial", "parallel"]: + return FileIOBackend(bopt) + + return IOBackend(bopt) + + + +################################# +# QEMU Emulated Device Definitions +# class QEMUPeripherals: def __init__(self, name, opt) -> None: @@ -66,39 +145,71 @@ class QEMUPeripherals: def get_qemu_opts(self) -> list: pass -class BasicSerialDevice(QEMUPeripherals): +class ISASerialDevice(QEMUPeripherals): def __init__(self, opt) -> None: - super().__init__("serial", opt) + super().__init__("isa-serial", opt) def get_qemu_opts(self): - link, logfile = parse_protocol(self._opt) + chardev = select_backend(self._opt) + + cmds = [ + "isa-serial", + f"id=com.{get_uniq()}", + f"chardev={chardev.name()}" + ] - cmds = [ link, "server", "nowait" ] - if logfile: - cmds.append(f"logfile={logfile}") - return [ "-serial", join_attrs(cmds) ] + return [ + "-chardev", chardev.to_cmdline(), + "-device", join_attrs(cmds) + ] class PCISerialDevice(QEMUPeripherals): def __init__(self, opt) -> None: super().__init__("pci-serial", opt) def get_qemu_opts(self): - uniq = hex(self.__hash__())[2:] - name = f"chrdev.{uniq}" - cmds = [ "pci-serial", f"id=uart.{uniq}", f"chardev={name}" ] - chrdev = [ "file", f"id={name}" ] - - logfile = get_config(self._opt, "logfile", required=True) - chrdev.append(f"path={logfile}") + chardev = select_backend(self._opt) + + cmds = [ + "pci-serial", + f"id=uart.{get_uniq()}", + f"chardev={chardev.name()}" + ] return [ - "-chardev", join_attrs(chrdev), + "-chardev", chardev.to_cmdline(), "-device", join_attrs(cmds) ] class AHCIBus(QEMUPeripherals): def __init__(self, opt) -> None: super().__init__("ahci", opt) + + def __create_disklet(self, index, bus, opt): + d_type = get_config(opt, "type", default="ide-hd") + d_img = get_config(opt, "img", required=True) + d_ro = get_config(opt, "ro", default=False) + d_fmt = get_config(opt, "format", default="raw") + d_id = f"disk_{index}" + + if not os.path.exists(d_img): + logger.warning(f"AHCI bus: {d_img} not exists, skipped") + return [] + + return [ + "-drive", join_attrs([ + f"id={d_id}", + f"file={d_img}", + f"readonly={'on' if d_ro else 'off'}", + f"if=none", + f"format={d_fmt}" + ]), + "-device", join_attrs([ + d_type, + f"drive={d_id}", + f"bus={bus}.{index}" + ]) + ] def get_qemu_opts(self): opt = self._opt @@ -106,31 +217,9 @@ class AHCIBus(QEMUPeripherals): name = name.strip().replace(" ", "_") cmds = [ "-device", f"ahci,id={name}" ] - for i, disk in enumerate(get_config(opt, "disks", default=[])): - d_type = get_config(disk, "type", default="ide-hd") - d_img = get_config(disk, "img", required=True) - d_ro = get_config(disk, "ro", default=False) - d_fmt = get_config(disk, "format", default="raw") - d_id = f"disk_{i}" - - if not os.path.exists(d_img): - logger.warning(f"AHCI bus: {d_img} not exists, disk skipped") - continue - - cmds += [ - "-drive", join_attrs([ - f"id={d_id}," - f"file={d_img}", - f"readonly={'on' if d_ro else 'off'}", - f"if=none", - f"format={d_fmt}" - ]), - "-device", join_attrs([ - d_type, - f"drive={d_id}", - f"bus={name}.{i}" - ]) - ] + disklets = get_config(opt, "disks", default=[]) + for i, disk in enumerate(disklets): + cmds += self.__create_disklet(i, name, disk) return cmds @@ -148,36 +237,49 @@ class QEMUMonitor(QEMUPeripherals): super().__init__("monitor", opt) def get_qemu_opts(self): - link, logfile = parse_protocol(self._opt) + + chardev = select_backend(self._opt) return [ - "-monitor", join_attrs([ - link, - "server", - "nowait", - f"logfile={logfile}" + "-chardev", chardev.to_cmdline(), + "-mon", join_attrs([ + chardev.name(), + "mode=readline", ]) ] -class QEMUExec: - devices = { - "basic_serial": BasicSerialDevice, +class QEMUDevices: + __devs = { + "isa-serial": ISASerialDevice, "ahci": AHCIBus, "rtc": RTCDevice, "hmp": QEMUMonitor, "pci-serial": PCISerialDevice } + @staticmethod + def get(name): + if name not in QEMUDevices.__devs: + raise Exception(f"device class: {name} is not defined") + return QEMUDevices.__devs[name] + + + +################################# +# QEMU Machine Definitions +# + +class QEMUExec: + + def __init__(self, options) -> None: self._opt = options self._devices = [] for dev in get_config(options, "devices", default=[]): dev_class = get_config(dev, "class") - if dev_class not in QEMUExec.devices: - raise Exception(f"device class: {dev_class} is not defined") - - self._devices.append(QEMUExec.devices[dev_class](dev)) + device = QEMUDevices.get(dev_class) + self._devices.append(device(dev)) def get_qemu_exec_name(self): pass @@ -217,7 +319,7 @@ class QEMUExec: def add_peripheral(self, peripheral): self._devices.append(peripheral) - def start(self, qemu_dir_override=""): + def start(self, qemu_dir_override="", dryrun=False, extras=[]): qemu_path = self.get_qemu_exec_name() qemu_path = os.path.join(qemu_dir_override, qemu_path) cmds = [ @@ -230,7 +332,12 @@ class QEMUExec: for dev in self._devices: cmds += dev.get_qemu_opts() + cmds += extras print(" ".join(cmds), "\n") + + if dryrun: + logger.info("[DRY RUN] QEMU not invoked") + return handle = subprocess.Popen(cmds) @@ -257,9 +364,10 @@ def main(): arg.add_argument("config_file") arg.add_argument("--qemu-dir", default="") + arg.add_argument("--dry", action='store_true') arg.add_argument("-v", "--values", action='append', default=[]) - arg_opt = arg.parse_args() + arg_opt, extras = arg.parse_known_args() opts = {} with open(arg_opt.config_file, 'r') as f: @@ -277,7 +385,8 @@ def main(): else: raise Exception(f"undefined arch: {arch}") - q.start(arg_opt.qemu_dir) + extras = [ x for x in extras if x != '--'] + q.start(arg_opt.qemu_dir, arg_opt.dry, extras) if __name__ == "__main__": main() \ No newline at end of file diff --git a/lunaix-os/scripts/qemus/qemu_x86_dev.json b/lunaix-os/scripts/qemus/qemu_x86_dev.json index 023dfec..60b292e 100644 --- a/lunaix-os/scripts/qemus/qemu_x86_dev.json +++ b/lunaix-os/scripts/qemus/qemu_x86_dev.json @@ -28,18 +28,26 @@ }, "devices": [ { - "class": "basic_serial", - "protocol": "telnet", - "addr": ":12345", - "logfile": "lunaix_ttyS0.log" + "class": "isa-serial", + "io": { + "type": "telnet", + "port": "12345", + "logfile": "lunaix_ttyS0.log" + } }, { "class": "pci-serial", - "logfile": "ttyPCI0.log" + "io": { + "type": "null", + "logfile": "ttypci1.log" + } }, { "class": "pci-serial", - "logfile": "ttyPCI1.log" + "io": { + "type": "null", + "logfile": "ttypci2.log" + } }, { "class": "rtc", @@ -64,9 +72,11 @@ }, { "class": "hmp", - "protocol": "telnet", - "addr": ":$QMPORT", - "logfile": "qm.log" + "io": { + "type": "telnet", + "port": "$QMPORT", + "logfile": "qm.log" + } } ] } \ No newline at end of file diff --git a/lunaix-os/usr/LConfig b/lunaix-os/usr/LConfig index ea38d57..3f44fe2 100644 --- a/lunaix-os/usr/LConfig +++ b/lunaix-os/usr/LConfig @@ -1,4 +1,4 @@ -@Term +@Term("Architecture") def arch(): """ set the ISA target diff --git a/lunaix-os/usr/makefile b/lunaix-os/usr/makefile index 54f7260..a69500f 100644 --- a/lunaix-os/usr/makefile +++ b/lunaix-os/usr/makefile @@ -1,5 +1,8 @@ include utils.mkinc include toolchain.mkinc + +LCONFIG_FLAGS := --quiet + include lunabuild.mkinc include $(lbuild_mkinc)