7b1f16baf5142ab0127c46cb1d855578425dffa9
[lunaix-os.git] / lunaix-os / scripts / build-tools / integration / lunamenu.py
1 from lcfg.api import RenderContext
2 from lcfg.types import (
3     PrimitiveType,
4     MultipleChoiceType
5 )
6
7 import subprocess
8 import curses
9 import textwrap
10 import integration.libtui as tui
11 import integration.libmenu as menu
12
13 from integration.libtui import ColorScope, TuiColor, Alignment, EventType
14 from integration.libmenu import Dialogue, ListView, show_dialog
15
16 __git_repo_info = None
17 __tainted = False
18
19 def mark_tainted():
20     global __tainted
21     __tainted = True
22
23 def unmark_tainted():
24     global __tainted
25     __tainted = False
26
27 def get_git_hash():
28     try:
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}"
36     except:
37         return None
38     
39 def get_git_info():
40     global __git_repo_info
41     return __git_repo_info
42     
43 def do_save(session):
44     show_dialog(session, "Notice", "Configuration saved")
45     unmark_tainted()
46
47 def do_exit(session):
48     global __tainted
49     if not __tainted:
50         session.schedule(EventType.E_QUIT)
51         return
52     
53     quit = QuitDialogue(session)
54     quit.show()
55
56 class MainMenuContext(tui.TuiContext):
57     def __init__(self, session, view_title):
58         super().__init__(session)
59
60         self.__title = view_title
61         
62         self.__prepare_layout()
63
64     def __prepare_layout(self):
65         
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)
70         root.border(True)
71
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)
76
77         listv = ListView(self, "list_view")
78         listv.set_size("70", "*")
79         listv.set_alignment(Alignment.CENTER)
80
81         hint = tui.TuiTextBlock(self, "hint")
82         hint.set_size(w="*")
83         hint.set_local_pos("0.1*", 0)
84         hint.height_auto_fit(True)
85         hint.set_text(
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"
89         )
90         hint.set_alignment(Alignment.CENTER | Alignment.LEFT)
91
92         suffix = ""
93         btns_defs = [
94             { 
95                 "text": "Save", 
96                 "onclick": lambda x: do_save(self.session())
97             },
98             { 
99                 "text": "Exit", 
100                 "onclick": lambda x: do_exit(self.session())
101             }
102         ]
103
104         repo_info = get_git_info()
105
106         if self.__title:
107             suffix += f" - {self.__title}"
108             btns_defs.insert(1, { 
109                 "text": "Back", 
110                 "onclick": lambda x: self.session().pop_context() 
111             })
112
113         btns = menu.create_buttons(self, btns_defs, sizes="50,*")
114
115         layout.set_cell(0, hint)
116         layout.set_cell(1, listv)
117         layout.set_cell(2, btns)
118
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.LEFT)
122
123         root.add(t)
124         root.add(t2)
125         root.add(layout)
126
127         self.set_root(root)
128         self.__menu_list = listv
129
130     def menu(self):
131         return self.__menu_list
132
133     def _handle_key_event(self, key):
134         if key == curses.KEY_BACKSPACE or key == 8:
135             self.session().pop_context()
136         elif key == 27:
137             do_exit(self.session())
138             return
139         
140         super()._handle_key_event(key)
141
142 class ItemType:
143     Expandable = 0
144     Switch = 1
145     Choice = 2
146     Other = 3
147     def __init__(self, node, expandable) -> None:
148         self.__node = node
149
150         if expandable:
151             self.__type = ItemType.Expandable
152             return
153         
154         self.__type = ItemType.Other
155         self.__primitive = False
156         type_provider = node.get_type()
157
158         if isinstance(type_provider, PrimitiveType):
159             self.__primitive = True
160
161             if isinstance(type_provider, MultipleChoiceType):
162                 self.__type = ItemType.Choice
163             elif type_provider._type == bool:
164                 self.__type = ItemType.Switch
165         
166         self.__provider = type_provider
167
168     def get_formatter(self):
169         if self.__type == ItemType.Expandable:
170             return "%s ---->"
171         
172         v = self.__node.get_value()
173
174         if self.is_switch():
175             mark = "*" if v else " "
176             return f"[{mark}] %s"
177         
178         if self.is_choice() or isinstance(v, int):
179             return f"({v}) %s"
180         
181         return "%s"
182     
183     def expandable(self):
184         return self.__type == ItemType.Expandable
185     
186     def is_switch(self):
187         return self.__type == ItemType.Switch
188     
189     def is_choice(self):
190         return self.__type == ItemType.Choice
191     
192     def read_only(self):
193         return not self.expandable() and self.__node.read_only()
194     
195     def provider(self):
196         return self.__provider
197
198 class MultiChoiceItem(tui.SimpleList.Item):
199     def __init__(self, value, get_val) -> None:
200         super().__init__()
201         self.__val = value
202         self.__getval = get_val
203
204     def get_text(self):
205         marker = "*" if self.__getval() == self.__val else " "
206         return f"  ({marker}) {self.__val}"
207     
208     def value(self):
209         return self.__val
210     
211 class LunaConfigItem(tui.SimpleList.Item):
212     def __init__(self, session, node, name, expand_cb = None):
213         super().__init__()
214         self.__node = node
215         self.__type = ItemType(node, expand_cb is not None)
216         self.__name = name
217         self.__expand_cb = expand_cb
218         self.__session = session
219     
220     def get_text(self):
221         fmt = self.__type.get_formatter()
222         if self.__type.read_only():
223             fmt += "*"
224         return f"   {fmt%(self.__name)}"
225     
226     def on_selected(self):
227         if self.__type.read_only():
228             show_dialog(
229                 self.__session, 
230                 f"Read-only: \"{self.__name}\"", 
231                 f"Value defined in this field:\n\n'{self.__node.get_value()}'")
232             return
233         
234         if self.__type.expandable():
235             view = CollectionView(self.__session, self.__node, self.__name)
236             view.set_reloader(self.__expand_cb)
237             view.show()
238             return
239         
240         if self.__type.is_switch():
241             v = self.__node.get_value()
242             self.change_value(not v)
243         else:
244             dia = ValueEditDialogue(self.__session, self)
245             dia.show()
246
247         self.__session.schedule(EventType.E_REDRAW)
248
249     def name(self):
250         return self.__name
251     
252     def node(self):
253         return self.__node
254     
255     def type(self):
256         return self.__type
257     
258     def change_value(self, val):
259         try:
260             self.__node.set_value(val)
261         except:
262             show_dialog(
263                 self.__session, "Invalid value",
264                 f"Value: '{val}' does not match the type")
265             return False
266         
267         mark_tainted()
268         CollectionView.reload_active(self.__session)
269         return True
270     
271     def on_key_pressed(self, key):
272         if (key & ~0b100000) != ord('H'):
273             return
274         
275         h = self.__node.help_prompt()
276         if not self.__type.expandable():
277             h = "\n".join([
278                 h, "", "--------",
279                 "Supported Values:",
280                 textwrap.indent(str(self.__type.provider()), "  ")
281             ])
282
283         dia = HelpDialogue(self.__session, f"Help: '{self.__name}'", h)
284         dia.show()
285     
286 class CollectionView(RenderContext):
287     def __init__(self, session, node, label = None) -> None:
288         super().__init__()
289         
290         ctx = MainMenuContext(session, label)
291         self.__node = node
292         self.__tui_ctx = ctx
293         self.__listv = ctx.menu()
294         self.__session = session
295         self.__reloader = lambda x: node.render(x)
296
297         ctx.set_state(self)
298
299     def set_reloader(self, cb):
300         self.__reloader = cb
301
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)
305
306     def add_field(self, label, node):
307         item = LunaConfigItem(self.__session, node, label)
308         self.__listv.add_item(item)
309
310     def show(self):
311         self.reload()
312         self.__session.push_context(self.__tui_ctx)
313
314     def reload(self):
315         self.__listv.clear()
316         self.__reloader(self)
317         self.__session.schedule(EventType.E_REDRAW)
318
319     @staticmethod
320     def reload_active(session):
321         state = session.active().state()
322         if isinstance(state, CollectionView):
323             state.reload()
324
325 class ValueEditDialogue(menu.Dialogue):
326     def __init__(self, session, item: LunaConfigItem):
327         name = item.name()
328         title = f"Edit \"{name}\""
329         super().__init__(session, title, None, False, 
330                          "Confirm", "Cancle")
331         
332         self.__item = item
333         self.__value = item.node().get_value()
334
335         self.decide_content()
336
337     def __get_val(self):
338         return self.__value
339
340     def decide_content(self):
341         if not self.__item.type().is_choice():
342             self.set_input_dialogue(True)
343             return
344         
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)
349
350         for t in self.__item.type().provider()._type:
351             listv.add_item(MultiChoiceItem(t, self.__get_val))
352         
353         self.set_content(listv)
354         self.set_size()
355     
356     def __on_selected(self, listv, index, item):
357         self.__value = item.value()
358     
359     def _ok_onclick(self):
360         if self._textbox:
361             self.__value = self._textbox.get_text()
362
363         if self.__item.change_value(self.__value):
364             super()._ok_onclick()
365
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")
371         
372     def _ok_onclick(self):
373         self.session().schedule(EventType.E_QUIT)
374
375     def _abort_onclick(self):
376         unmark_tainted()
377         self._ok_onclick()
378
379
380 class HelpDialogue(menu.Dialogue):
381     def __init__(self, session, title="", content=""):
382         super().__init__(session, title, None, no_btn=None)
383
384         self.__content = content
385         self.__scroll_y = 0
386         self.set_local_pos(0, -2)
387
388     def prepare(self):
389         tb = tui.TuiTextBlock(self._context, "content")
390         tb.set_size(w="70")
391         tb.set_text(self.__content)
392         tb.height_auto_fit(True)
393         self.__tb = tb
394         
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)
399
400         self.set_size(w="75")
401         self.set_content(self.__scroll)
402         
403         super().prepare()
404
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)
414
415 class TerminalSizeCheckFailed(Exception):
416     def __init__(self, *args: object) -> None:
417         super().__init__(*args)
418
419 def main(_, root_node):
420     global __git_repo_info
421
422     __git_repo_info = get_git_hash()
423
424     session = tui.TuiSession()
425
426     h, w = session.window_size()
427     if h < 30 or w < 85:
428         raise TerminalSizeCheckFailed((90, 40), (w, h))
429
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)
447
448     main_view = CollectionView(session, root_node)
449     main_view.show()
450
451     session.event_loop()
452
453 def menuconfig(root_node):
454     global __tainted
455     curses.wrapper(main, root_node)
456
457     return not __tainted