1 from lcfg.api import RenderContext
2 from lcfg.types import (
10 import integration.libtui as tui
11 import integration.libmenu as menu
13 from integration.libtui import ColorScope, TuiColor, Alignment, EventType
14 from integration.libmenu import Dialogue, ListView, show_dialog
16 __git_repo_info = None
29 hsh = subprocess.check_output([
30 'git', 'rev-parse', '--short', 'HEAD'
31 ]).decode('ascii').strip()
32 branch = subprocess.check_output([
33 'git', 'branch', '--show-current'
34 ]).decode('ascii').strip()
35 return f"{branch}@{hsh}"
40 global __git_repo_info
41 return __git_repo_info
44 show_dialog(session, "Notice", "Configuration saved")
50 session.schedule(EventType.E_QUIT)
53 quit = QuitDialogue(session)
56 class MainMenuContext(tui.TuiContext):
57 def __init__(self, session, view_title):
58 super().__init__(session)
60 self.__title = view_title
62 self.__prepare_layout()
64 def __prepare_layout(self):
66 root = tui.TuiPanel(self, "main_panel")
67 root.set_size("*-10", "*-5")
68 root.set_alignment(Alignment.CENTER)
69 root.drop_shadow(1, 2)
72 layout = tui.FlexLinearLayout(self, "layout", "6,*,5")
73 layout.orientation(tui.FlexLinearLayout.PORTRAIT)
74 layout.set_size("*", "*")
75 layout.set_padding(1, 1, 1, 1)
77 listv = ListView(self, "list_view")
78 listv.set_size("70", "*")
79 listv.set_alignment(Alignment.CENTER)
81 hint = tui.TuiTextBlock(self, "hint")
83 hint.set_local_pos("0.1*", 0)
84 hint.height_auto_fit(True)
86 "Use <UP>/<DOWN>/<ENTER> to select from list\n"
87 "Use <TAB>/<RIGHT>/<LEFT> to change focus\n"
88 "<H>: show help (if applicable), <BACKSPACE>: back previous level"
90 hint.set_alignment(Alignment.CENTER | Alignment.LEFT)
96 "onclick": lambda x: do_save(self.session())
100 "onclick": lambda x: do_exit(self.session())
104 repo_info = get_git_info()
107 suffix += f" - {self.__title}"
108 btns_defs.insert(1, {
110 "onclick": lambda x: self.session().pop_context()
113 btns = menu.create_buttons(self, btns_defs, sizes="50,*")
115 layout.set_cell(0, hint)
116 layout.set_cell(1, listv)
117 layout.set_cell(2, btns)
119 t = menu.create_title(self, "Lunaix Kernel Configuration" + suffix)
120 t2 = menu.create_title(self, repo_info)
121 t2.set_alignment(Alignment.BOT | Alignment.RIGHT)
128 self.__menu_list = listv
131 return self.__menu_list
133 def _handle_key_event(self, key):
134 if key == curses.KEY_BACKSPACE or key == 8:
135 self.session().pop_context()
137 do_exit(self.session())
140 super()._handle_key_event(key)
147 def __init__(self, node, expandable) -> None:
151 self.__type = ItemType.Expandable
154 self.__type = ItemType.Other
155 self.__primitive = False
156 type_provider = node.get_type()
158 if isinstance(type_provider, PrimitiveType):
159 self.__primitive = True
161 if isinstance(type_provider, MultipleChoiceType):
162 self.__type = ItemType.Choice
163 elif type_provider._type == bool:
164 self.__type = ItemType.Switch
166 self.__provider = type_provider
168 def get_formatter(self):
169 if self.__type == ItemType.Expandable:
172 v = self.__node.get_value()
175 mark = "*" if v else " "
176 return f"[{mark}] %s"
178 if self.is_choice() or isinstance(v, int):
183 def expandable(self):
184 return self.__type == ItemType.Expandable
187 return self.__type == ItemType.Switch
190 return self.__type == ItemType.Choice
193 return not self.expandable() and self.__node.read_only()
196 return self.__provider
198 class MultiChoiceItem(tui.SimpleList.Item):
199 def __init__(self, value, get_val) -> None:
202 self.__getval = get_val
205 marker = "*" if self.__getval() == self.__val else " "
206 return f" ({marker}) {self.__val}"
211 class LunaConfigItem(tui.SimpleList.Item):
212 def __init__(self, session, node, name, expand_cb = None):
215 self.__type = ItemType(node, expand_cb is not None)
217 self.__expand_cb = expand_cb
218 self.__session = session
221 fmt = self.__type.get_formatter()
222 if self.__type.read_only():
224 return f" {fmt%(self.__name)}"
226 def on_selected(self):
227 if self.__type.read_only():
230 f"Read-only: \"{self.__name}\"",
231 f"Value defined in this field:\n\n'{self.__node.get_value()}'")
234 if self.__type.expandable():
235 view = CollectionView(self.__session, self.__node, self.__name)
236 view.set_reloader(self.__expand_cb)
240 if self.__type.is_switch():
241 v = self.__node.get_value()
242 self.change_value(not v)
244 dia = ValueEditDialogue(self.__session, self)
247 self.__session.schedule(EventType.E_REDRAW)
258 def change_value(self, val):
260 self.__node.set_value(val)
263 self.__session, "Invalid value",
264 f"Value: '{val}' does not match the type")
268 CollectionView.reload_active(self.__session)
271 def on_key_pressed(self, key):
272 if (key & ~0b100000) != ord('H'):
275 h = self.__node.help_prompt()
276 if not self.__type.expandable():
280 textwrap.indent(str(self.__type.provider()), " ")
283 dia = HelpDialogue(self.__session, f"Help: '{self.__name}'", h)
286 class CollectionView(RenderContext):
287 def __init__(self, session, node, label = None) -> None:
290 ctx = MainMenuContext(session, label)
293 self.__listv = ctx.menu()
294 self.__session = session
295 self.__reloader = lambda x: node.render(x)
299 def set_reloader(self, cb):
302 def add_expandable(self, label, node, on_expand_cb):
303 item = LunaConfigItem(self.__session, node, label, on_expand_cb)
304 self.__listv.add_item(item)
306 def add_field(self, label, node):
307 item = LunaConfigItem(self.__session, node, label)
308 self.__listv.add_item(item)
312 self.__session.push_context(self.__tui_ctx)
316 self.__reloader(self)
317 self.__session.schedule(EventType.E_REDRAW)
320 def reload_active(session):
321 state = session.active().state()
322 if isinstance(state, CollectionView):
325 class ValueEditDialogue(menu.Dialogue):
326 def __init__(self, session, item: LunaConfigItem):
328 title = f"Edit \"{name}\""
329 super().__init__(session, title, None, False,
333 self.__value = item.node().get_value()
335 self.decide_content()
340 def decide_content(self):
341 if not self.__item.type().is_choice():
342 self.set_input_dialogue(True)
345 listv = ListView(self.context(), "choices")
346 listv.set_size("0.8*", "*")
347 listv.set_alignment(Alignment.CENTER)
348 listv.set_onselected_cb(self.__on_selected)
350 for t in self.__item.type().provider()._type:
351 listv.add_item(MultiChoiceItem(t, self.__get_val))
353 self.set_content(listv)
356 def __on_selected(self, listv, index, item):
357 self.__value = item.value()
359 def _ok_onclick(self):
361 self.__value = self._textbox.get_text()
363 if self.__item.change_value(self.__value):
364 super()._ok_onclick()
366 class QuitDialogue(menu.Dialogue):
367 def __init__(self, session):
368 super().__init__(session,
369 "Quit ?", "Unsaved changes, sure to quit?", False,
370 "Quit Anyway", "No", "Save and Quit")
372 def _ok_onclick(self):
373 self.session().schedule(EventType.E_QUIT)
375 def _abort_onclick(self):
380 class HelpDialogue(menu.Dialogue):
381 def __init__(self, session, title="", content=""):
382 super().__init__(session, title, None, no_btn=None)
384 self.__content = content
386 self.set_local_pos(0, -2)
389 tb = tui.TuiTextBlock(self._context, "content")
391 tb.set_text(self.__content)
392 tb.height_auto_fit(True)
395 self.__scroll = tui.TuiScrollable(self._context, "scroll")
396 self.__scroll.set_size("65", "*")
397 self.__scroll.set_alignment(Alignment.CENTER)
398 self.__scroll.set_content(tb)
400 self.set_size(w="75")
401 self.set_content(self.__scroll)
405 def _handle_key_event(self, key):
406 if key == curses.KEY_UP:
407 self.__scroll_y = max(self.__scroll_y - 1, 0)
408 self.__scroll.set_scrollY(self.__scroll_y)
409 elif key == curses.KEY_DOWN:
410 y = self.__tb._size.y()
411 self.__scroll_y = min(self.__scroll_y + 1, y)
412 self.__scroll.set_scrollY(self.__scroll_y)
413 super()._handle_key_event(key)
415 class TerminalSizeCheckFailed(Exception):
416 def __init__(self, *args: object) -> None:
417 super().__init__(*args)
419 def main(_, root_node):
420 global __git_repo_info
422 __git_repo_info = get_git_hash()
424 session = tui.TuiSession()
426 h, w = session.window_size()
428 raise TerminalSizeCheckFailed((90, 40), (w, h))
430 base_background = TuiColor.white.bright()
431 session.set_color(ColorScope.WIN,
432 TuiColor.black, TuiColor.blue)
433 session.set_color(ColorScope.PANEL,
434 TuiColor.black, base_background)
435 session.set_color(ColorScope.TEXT,
436 TuiColor.black, base_background)
437 session.set_color(ColorScope.TEXT_HI,
438 TuiColor.magenta, base_background)
439 session.set_color(ColorScope.SHADOW,
440 TuiColor.black, TuiColor.black)
441 session.set_color(ColorScope.SELECT,
442 TuiColor.white, TuiColor.black.bright())
443 session.set_color(ColorScope.HINT,
444 TuiColor.cyan, base_background)
445 session.set_color(ColorScope.BOX,
446 TuiColor.black, TuiColor.white)
448 main_view = CollectionView(session, root_node)
453 def menuconfig(root_node):
455 curses.wrapper(main, root_node)