2 # libtui - TUI framework using ncurses
5 # I sware, this is the last time I ever touch
6 # any sort of the GUI messes.
11 import curses.panel as cpanel
12 import curses.textpad as textpad
15 def __invoke_fn(obj, fn, *args):
19 _id = obj._id if obj else "<root>"
20 raise Exception(_id, str(fn), args)
22 def resize_safe(obj, co, y, x):
23 __invoke_fn(obj, co.resize, y, x)
25 def move_safe(obj, co, y, x):
26 __invoke_fn(obj, co.move, y, x)
28 def addstr_safe(obj, co, y, x, str, *args):
29 __invoke_fn(obj, co.addstr, y, x, str, *args)
32 def __init__(self, v) -> None:
40 black = _TuiColor(curses.COLOR_BLACK)
41 red = _TuiColor(curses.COLOR_RED)
42 green = _TuiColor(curses.COLOR_GREEN)
43 yellow = _TuiColor(curses.COLOR_YELLOW)
44 blue = _TuiColor(curses.COLOR_BLUE)
45 magenta = _TuiColor(curses.COLOR_MAGENTA)
46 cyan = _TuiColor(curses.COLOR_CYAN)
47 white = _TuiColor(curses.COLOR_WHITE)
74 E_M_FOCUS = 0b10000000
77 return (t & EventType.E_M_FOCUS)
80 return t & ~EventType.E_M_FOCUS
83 return (t & ~EventType.E_M_FOCUS) == EventType.E_KEY
86 RelSize = re.compile(r"(?P<mult>[0-9]+(?:\.[0-9]+)?)?\*(?P<add>[+-][0-9]+)?")
88 class BoundExpression:
89 def __init__(self, expr = None):
96 def set_pair(self, mult, add):
101 self._mult = expr._mult
102 self._add = expr._add
104 def update(self, expr):
105 if isinstance(expr, int):
108 m = Matchers.RelSize.match(expr)
112 mult = 1 if not g["mult"] else float(g["mult"])
113 add = 0 if not g["add"] else int(g["add"])
117 self.set_pair(0, int(expr))
119 def calc(self, ref_val):
120 return int(self._mult * ref_val + self._add)
123 return self._mult == 0
126 return self._mult == 0 and self._add == 0
128 def scale_mult(self, scalar):
133 def normalise(*exprs):
134 v = BoundExpression()
137 return [e.scale_mult(1 / v._mult) for e in exprs]
139 def __add__(self, b):
140 v = BoundExpression()
146 def __sub__(self, b):
147 v = BoundExpression()
153 def __iadd__(self, b):
154 self._mult += b._mult
158 def __isub__(self, b):
159 self._mult -= b._mult
163 def __rmul__(self, scalar):
164 v = BoundExpression()
170 def __truediv__(self, scalar):
171 v = BoundExpression()
173 v._mult /= float(scalar)
179 self.__x = BoundExpression()
180 self.__y = BoundExpression()
188 def resolve(self, ref_w, ref_h):
189 return (self.__y.calc(ref_h), self.__x.calc(ref_w))
191 def set(self, dyn_bound):
192 self.__x.set(dyn_bound.dyn_x())
193 self.__y.set(dyn_bound.dyn_y())
196 def __init__(self) -> None:
200 def shrinkX(self, dx):
202 def shrinkY(self, dy):
215 def update(self, dynsz, ref_bound):
216 y, x = dynsz.resolve(ref_bound.x(), ref_bound.y())
220 def reset(self, x, y):
221 self.__x, self.__y = x, y
229 def yx(self, scale = 1):
230 return int(self.__y * scale), int(self.__x * scale)
232 class TuiStackWindow:
233 def __init__(self, obj) -> None:
235 self.__win = curses.newwin(0, 0)
236 self.__pan = cpanel.new_panel(self.__win)
239 def resize(self, h, w):
240 resize_safe(self.__obj, self.__win, h, w)
242 def relocate(self, y, x):
243 move_safe(self.__obj, self.__pan, y, x)
245 def set_geometric(self, h, w, y, x):
246 resize_safe(self.__obj, self.__win, h, w)
247 move_safe(self.__obj, self.__pan, y, x)
249 def set_background(self, color_scope):
250 self.__win.bkgd(' ', curses.color_pair(color_scope))
261 def send_front(self):
268 def __init__(self) -> None:
269 self._local_pos = DynamicBound()
271 self._dyn_size = DynamicBound()
273 self._margin = (0, 0, 0, 0)
274 self._padding = (0, 0, 0, 0)
275 self._align = Alignment.TOP | Alignment.LEFT
277 def set_local_pos(self, x, y):
278 self._local_pos.dyn_x().update(x)
279 self._local_pos.dyn_y().update(y)
281 def set_alignment(self, align):
284 def set_size(self, w = None, h = None):
286 self._dyn_size.dyn_x().update(w)
288 self._dyn_size.dyn_y().update(h)
290 def set_margin(self, top, right, bottom, left):
291 self._margin = (top, right, bottom, left)
293 def set_padding(self, top, right, bottom, left):
294 self._padding = (top, right, bottom, left)
297 self._pos.reset(0, 0)
298 self._size.reset(0, 0)
300 def deduce_spatial(self, constrain):
302 self.__satisfy_bound(constrain)
303 self.__satisfy_alignment(constrain)
304 self.__satisfy_margin(constrain)
305 self.__satisfy_padding(constrain)
307 self.__to_corner_pos(constrain)
309 def __satisfy_alignment(self, constrain):
310 local_pos = self._local_pos
311 cbound = constrain._size
315 ry, rx = local_pos.resolve(cx, cy)
316 ay, ax = size.yx(0.5)
318 if self._align & Alignment.CENTER:
322 if self._align & Alignment.BOT:
323 ay = min(cy - ay, cy - 1)
325 elif self._align & Alignment.TOP:
328 if self._align & Alignment.RIGHT:
331 elif self._align & Alignment.LEFT:
334 self._pos.reset(ax + rx, ay + ry)
336 def __satisfy_margin(self, constrain):
337 tm, lm, bm, rm = self._margin
339 self._pos.growX(rm - lm)
340 self._pos.growY(bm - tm)
342 def __satisfy_padding(self, constrain):
343 csize = constrain._size
345 h, w = self._size.yx(0.5)
346 y, x = self._pos.yx()
348 tp, lp, bp, rp = self._padding
350 if not (tp or lp or bp or rp):
353 dtp = min(y - h, tp) - tp
354 dbp = min(ch - (y + h), bp) - bp
356 dlp = min(x - w, lp) - lp
357 drp = min(cw - (x + w), rp) - rp
359 self._size.growX(drp + dlp)
360 self._size.growY(dtp + dbp)
362 def __satisfy_bound(self, constrain):
363 self._size.update(self._dyn_size, constrain._size)
365 def __to_corner_pos(self, constrain):
366 h, w = self._size.yx(0.5)
367 g_pos = constrain._pos
372 self._pos.growX(g_pos.x())
373 self._pos.growY(g_pos.y())
376 class TuiObject(SpatialObject):
377 def __init__(self, context, id):
380 self._context = context
383 self._focused = False
385 def set_parent(self, parent):
386 self._parent = parent
390 return self._parent.canvas()
391 return (self, self._context.window())
397 return self._context.session()
402 def on_destory(self):
410 self.deduce_spatial(self._parent)
415 def on_event(self, ev_type, ev_arg):
418 def on_focused(self):
421 def on_focus_lost(self):
422 self._focused = False
424 def set_visbility(self, visible):
425 self._visible = visible
434 class TuiWidget(TuiObject):
435 def __init__(self, context, id):
436 super().__init__(context, id)
441 co, _ = self.canvas()
447 class TuiContainerObject(TuiObject):
448 def __init__(self, context, id):
449 super().__init__(context, id)
452 def add(self, child):
453 child.set_parent(self)
454 self._children.append(child)
457 return self._children
461 for child in self._children:
464 def on_destory(self):
466 for child in self._children:
471 for child in self._children:
476 for child in self._children:
481 for child in self._children:
484 def on_event(self, ev_type, ev_arg):
485 super().on_event(ev_type, ev_arg)
486 for child in self._children:
487 child.on_event(ev_type, ev_arg)
490 class TuiScrollable(TuiObject):
491 def __init__(self, context, id):
492 super().__init__(context, id)
493 self.__spos = Bound()
495 self.__pad = curses.newpad(1, 1)
496 self.__pad.bkgd(' ', curses.color_pair(ColorScope.PANEL))
497 self.__pad_panel = cpanel.new_panel(self.__pad)
498 self.__content = None
501 return (self, self.__pad)
503 def set_content(self, content):
504 self.__content = content
505 self.__content.set_parent(self)
507 def set_scrollY(self, y):
508 self.__spos.resetY(y)
510 def set_scrollX(self, x):
511 self.__spos.resetX(x)
513 def reached_last(self):
514 off = self.__spos.y() + self._size.y()
515 return off >= self.__content._size.y()
517 def reached_top(self):
518 return self.__spos.y() < self._size.y()
523 if not self.__content:
526 self.__content.on_layout()
528 h, w = self._size.yx()
529 ch, cw = self.__content._size.yx()
530 sh, sw = max(ch, h), max(cw, w)
532 self.__spos.resetX(min(self.__spos.x(), max(cw, w) - w))
533 self.__spos.resetY(min(self.__spos.y(), max(ch, h) - h))
535 resize_safe(self, self.__pad, sh, sw)
538 if not self.__content:
541 self.__pad_panel.top()
542 self.__content.on_draw()
544 wminy, wminx = self._pos.yx()
545 wmaxy, wmaxx = self._size.yx()
546 wmaxy, wmaxx = wmaxy + wminy, wmaxx + wminx
548 self.__pad.refresh(*self.__spos.yx(),
549 wminy, wminx, wmaxy - 1, wmaxx - 1)
552 class Layout(TuiContainerObject):
553 class Cell(TuiObject):
554 def __init__(self, context):
555 super().__init__(context, "cell")
558 def set_obj(self, obj):
560 self.__obj.set_parent(self)
564 self.__obj.on_create()
566 def on_destory(self):
568 self.__obj.on_destory()
577 self.__obj.do_layout()
583 def on_event(self, ev_type, ev_arg):
585 self.__obj.on_event(ev_type, ev_arg)
587 def __init__(self, context, id, ratios):
588 super().__init__(context, id)
590 rs = [BoundExpression(r) for r in ratios.split(',')]
591 self._rs = BoundExpression.normalise(*rs)
593 for _ in range(len(self._rs)):
594 cell = Layout.Cell(self._context)
597 self._adjust_to_fit()
599 def _adjust_to_fit(self):
602 def add(self, child):
603 raise RuntimeError("invalid operation")
605 def set_cell(self, i, obj):
606 if i > len(self._children):
607 raise ValueError(f"cell #{i} out of bound")
609 self._children[i].set_obj(obj)
612 class FlexLinearLayout(Layout):
615 def __init__(self, context, id, ratios):
616 self.__horizontal = False
618 super().__init__(context, id, ratios)
620 def orientation(self, orient):
621 self.__horizontal = orient == FlexLinearLayout.LANDSCAPE
627 def _adjust_to_fit(self):
628 sum_abs = BoundExpression()
637 for i, r in enumerate(self._rs):
639 self._rs[i] -= sum_abs
641 def __apply_ratio(self):
642 if self.__horizontal:
643 self.__adjust_horizontal()
645 self.__adjust_vertical()
647 def __adjust_horizontal(self):
648 acc = BoundExpression()
649 for r, cell in zip(self._rs, self.children()):
650 cell._dyn_size.dyn_y().set_pair(1, 0)
651 cell._dyn_size.dyn_x().set(r)
653 cell.set_alignment(Alignment.LEFT)
654 cell._local_pos.dyn_y().set_pair(0, 0)
655 cell._local_pos.dyn_x().set(acc)
659 def __adjust_vertical(self):
660 acc = BoundExpression()
661 for r, cell in zip(self._rs, self.children()):
662 cell._dyn_size.dyn_x().set_pair(1, 0)
663 cell._dyn_size.dyn_y().set(r)
665 cell.set_alignment(Alignment.TOP | Alignment.CENTER)
666 cell._local_pos.dyn_x().set_pair(0, 0)
667 cell._local_pos.dyn_y().set(acc)
672 class TuiPanel(TuiContainerObject):
673 def __init__(self, context, id):
674 super().__init__(context, id)
676 self.__use_border = False
677 self.__use_shadow = False
678 self.__shadow_param = (0, 0)
680 self.__swin = TuiStackWindow(self)
681 self.__shad = TuiStackWindow(self)
683 self.__swin.set_background(ColorScope.PANEL)
684 self.__shad.set_background(ColorScope.SHADOW)
687 return (self, self.__swin.window())
689 def drop_shadow(self, off_y, off_x):
690 self.__shadow_param = (off_y, off_x)
691 self.__use_shadow = not (off_y == off_x and off_y == 0)
693 def border(self, _b):
694 self.__use_border = _b
696 def bkgd_override(self, scope):
697 self.__swin.set_background(scope)
704 h, w = self._size.y(), self._size.x()
705 y, x = self._pos.y(), self._pos.x()
706 self.__swin.set_geometric(h, w, y, x)
708 if self.__use_shadow:
709 sy, sx = self.__shadow_param
710 self.__shad.set_geometric(h, w, y + sy, x + sx)
712 def on_destory(self):
718 win = self.__swin.window()
721 if self.__use_border:
724 if self.__use_shadow:
730 self.__swin.send_front()
734 class TuiLabel(TuiWidget):
735 def __init__(self, context, id):
736 super().__init__(context, id)
737 self._text = "TuiLabel"
740 self.__auto_fit = True
743 self.__highlight = False
744 self.__color_scope = -1
746 def __try_fit_text(self, txt):
748 self._dyn_size.dyn_x().set_pair(0, len(txt))
749 self._dyn_size.dyn_y().set_pair(0, 1)
751 def __pad_text(self):
752 for i, t in enumerate(self._wrapped):
753 self._wrapped[i] = str.rjust(t, self._size.x())
755 def set_text(self, text):
757 self.__try_fit_text(text)
759 def override_color(self, color = -1):
760 self.__color_scope = color
762 def auto_fit(self, _b):
765 def truncate(self, _b):
768 def hightlight(self, _b):
769 self.__highlight = _b
771 def pad_around(self, _b):
778 self.__try_fit_text(txt)
782 if len(txt) <= self._size.x():
783 self._wrapped = [txt]
788 txt = txt[:self._size.x() - 1]
789 self._wrapped = [txt]
793 self._wrapped = textwrap.wrap(txt, self._size.x())
797 _, win = self.canvas()
798 y, x = self._pos.yx()
800 if self.__color_scope != -1:
801 color = curses.color_pair(self.__color_scope)
802 elif self.__highlight:
803 color = curses.color_pair(ColorScope.TEXT_HI)
805 color = curses.color_pair(ColorScope.TEXT)
807 for i, t in enumerate(self._wrapped):
808 addstr_safe(self, win, y + i, x, t, color)
811 class TuiTextBlock(TuiWidget):
812 def __init__(self, context, id):
813 super().__init__(context, id)
816 self.__fit_to_height = False
818 def set_text(self, text):
819 text = textwrap.dedent(text)
820 self.__lines = text.split('\n')
821 if self.__fit_to_height:
822 self._dyn_size.dyn_y().set_pair(0, 0)
824 def height_auto_fit(self, yes):
825 self.__fit_to_height = yes
830 self.__wrapped.clear()
831 for t in self.__lines:
833 self.__wrapped.append(t)
835 wrap = textwrap.wrap(t, self._size.x())
836 self.__wrapped += wrap
838 if self._dyn_size.dyn_y().nullity():
839 h = len(self.__wrapped)
840 self._dyn_size.dyn_y().set_pair(0, h)
846 _, win = self.canvas()
847 y, x = self._pos.yx()
849 color = curses.color_pair(ColorScope.TEXT)
850 for i, t in enumerate(self.__wrapped):
851 addstr_safe(self, win, y + i, x, t, color)
854 class TuiTextBox(TuiWidget):
855 def __init__(self, context, id):
856 super().__init__(context, id)
857 self.__box = TuiStackWindow(self)
858 self.__box.set_background(ColorScope.PANEL)
859 self.__textb = textpad.Textbox(self.__box.window(), True)
860 self.__textb.stripspaces = True
862 self.__scheduled_edit = False
864 self._context.focus_group().register(self, 0)
866 def __validate(self, x):
867 if x == 10 or x == 9:
874 co, _ = self.canvas()
875 h, w = self._size.yx()
876 y, x = self._pos.yx()
877 cy, cx = co._pos.yx()
878 y, x = y + cy, x + cx
881 self.__box.set_geometric(1, w - 1, y + h // 2, x + 1)
885 self.__box.send_front()
887 _, cwin = self.canvas()
889 h, w = self._size.yx()
890 y, x = self._pos.yx()
891 textpad.rectangle(cwin, y, x, y + h - 1, x+w)
893 win = self.__box.window()
897 self.__str = self.__textb.edit(lambda x: self.__validate(x))
898 self.session().schedule(EventType.E_CHFOCUS)
899 self.__scheduled_edit = False
904 def on_focused(self):
905 self.__box.set_background(ColorScope.BOX)
906 if not self.__scheduled_edit:
907 # edit will block, defer to next update cycle
908 self.session().schedule_task(self.__edit)
909 self.__scheduled_edit = True
911 def on_focus_lost(self):
912 self.__box.set_background(ColorScope.PANEL)
915 class SimpleList(TuiWidget):
917 def __init__(self) -> None:
921 def on_selected(self):
923 def on_key_pressed(self, key):
926 def __init__(self, context, id):
927 super().__init__(context, id)
930 self.__on_sel_confirm_cb = None
931 self.__on_sel_change_cb = None
933 self._context.focus_group().register(self)
935 def set_onselected_cb(self, cb):
936 self.__on_sel_confirm_cb = cb
938 def set_onselection_change_cb(self, cb):
939 self.__on_sel_change_cb = cb
942 return len(self.__items)
945 return self.__selected
947 def add_item(self, item):
948 self.__items.append(item)
955 self.__selected = min(self.__selected, len(self.__items))
956 self._size.resetY(len(self.__items) + 1)
959 _, win = self.canvas()
962 for i, item in enumerate(self.__items):
963 color = curses.color_pair(ColorScope.TEXT)
964 if i == self.__selected:
966 color = curses.color_pair(ColorScope.SELECT)
968 color = curses.color_pair(ColorScope.BOX)
970 txt = str.ljust(item.get_text(), w)
972 addstr_safe(self, win, i, 0, txt, color)
974 def on_event(self, ev_type, ev_arg):
975 if not EventType.key_press(ev_type):
978 if len(self.__items) == 0:
981 sel = self.__items[self.__selected]
986 if self.__on_sel_confirm_cb:
987 self.__on_sel_confirm_cb(self, self.__selected, sel)
990 sel.on_key_pressed(ev_arg)
992 if (ev_arg != curses.KEY_DOWN and
993 ev_arg != curses.KEY_UP):
996 prev = self.__selected
997 if ev_arg == curses.KEY_DOWN:
1000 self.__selected -= 1
1002 self.__selected = max(self.__selected, 0)
1003 self.__selected = self.__selected % len(self.__items)
1005 if self.__on_sel_change_cb:
1006 self.__on_sel_change_cb(self, prev, self.__selected)
1009 class TuiButton(TuiLabel):
1010 def __init__(self, context, id):
1011 super().__init__(context, id)
1012 self.__onclick = None
1014 context.focus_group().register(self)
1016 def set_text(self, text):
1017 return super().set_text(f"<{text}>")
1019 def set_click_callback(self, cb):
1022 def hightlight(self, _b):
1023 raise NotImplemented()
1026 _, win = self.canvas()
1027 y, x = self._pos.yx()
1030 color = curses.color_pair(ColorScope.SELECT)
1032 color = curses.color_pair(ColorScope.TEXT)
1034 addstr_safe(self, win, y, x, self._wrapped[0], color)
1036 def on_event(self, ev_type, ev_arg):
1037 if not EventType.focused_only(ev_type):
1039 if not EventType.key_press(ev_type):
1042 if ev_arg == ord('\n') and self.__onclick:
1043 self.__onclick(self)
1047 def __init__(self) -> None:
1048 self.stdsc = curses.initscr()
1049 curses.start_color()
1054 self.__context_stack = []
1055 self.__sched_events = []
1057 ws = self.window_size()
1058 self.__win = curses.newwin(*ws)
1059 self.__winbg = curses.newwin(*ws)
1060 self.__panbg = cpanel.new_panel(self.__winbg)
1062 self.__winbg.bkgd(' ', curses.color_pair(ColorScope.WIN))
1064 self.__win.timeout(50)
1065 self.__win.keypad(True)
1067 def window_size(self):
1068 return self.stdsc.getmaxyx()
1070 def set_color(self, scope, fg, bg):
1071 curses.init_pair(scope, int(fg), int(bg))
1073 def schedule_redraw(self):
1074 self.schedule(EventType.E_REDRAW)
1076 def schedule_task(self, task):
1077 self.schedule(EventType.E_REDRAW)
1078 self.schedule(EventType.E_TASK, task)
1080 def schedule(self, event, arg = None):
1081 if len(self.__sched_events) > 0:
1082 if self.__sched_events[-1] == event:
1085 self.__sched_events.append((event, arg))
1087 def push_context(self, tuictx):
1089 self.__context_stack.append(tuictx)
1090 self.schedule(EventType.E_REDRAW)
1092 curses.curs_set(self.active().curser_mode())
1094 def pop_context(self):
1095 if len(self.__context_stack) == 1:
1098 ctx = self.__context_stack.pop()
1100 self.schedule(EventType.E_REDRAW)
1102 curses.curs_set(self.active().curser_mode())
1105 return self.__context_stack[-1]
1107 def event_loop(self):
1108 if len(self.__context_stack) == 0:
1109 raise RuntimeError("no tui context to display")
1112 key = self.__win.getch()
1114 self.schedule(EventType.E_KEY, key)
1116 if len(self.__sched_events) == 0:
1119 evt, arg = self.__sched_events.pop(0)
1120 if evt == EventType.E_REDRAW:
1122 elif evt == EventType.E_QUIT:
1123 self.__notify_quit()
1126 self.active().dispatch_event(evt, arg)
1128 def __notify_quit(self):
1129 while len(self.__context_stack) == 0:
1130 ctx = self.__context_stack.pop()
1131 ctx.dispatch_event(EventType.E_QUIT, None)
1134 self.__win.noutrefresh()
1136 self.active().redraw(self.__win)
1138 self.__panbg.bottom()
1140 cpanel.update_panels()
1143 class TuiFocusGroup:
1144 def __init__(self) -> None:
1148 self.__focused = None
1150 def register(self, tui_obj, pos=-1):
1152 self.__grp.append((self.__id, tui_obj))
1154 self.__grp.insert(pos, (self.__id, tui_obj))
1156 return self.__id - 1
1158 def navigate_focus(self, dir = 1):
1159 self.__sel = (self.__sel + dir) % len(self.__grp)
1160 f = None if not len(self.__grp) else self.__grp[self.__sel][1]
1161 if f and f != self.__focused:
1163 self.__focused.on_focus_lost()
1168 return self.__focused
1170 class TuiContext(TuiObject):
1171 def __init__(self, session: TuiSession):
1172 super().__init__(self, "context")
1175 self.__session = session
1179 y, x = self.__session.window_size()
1180 self._size.reset(x, y)
1181 self.set_parent(None)
1183 self.__focus_group = TuiFocusGroup()
1184 self.__curser_mode = 0
1186 def set_curser_mode(self, mode):
1187 self.__curser_mode = mode
1189 def curser_mode(self):
1190 return self.__curser_mode
1192 def set_root(self, root):
1194 self.__root.set_parent(self)
1196 def set_state(self, obj):
1203 self.__root.on_create()
1205 def on_destory(self):
1206 self.__root.on_destory()
1209 return (self, self.__win)
1212 return self.__session
1214 def dispatch_event(self, evt, arg):
1215 if evt == EventType.E_REDRAW:
1216 self.__focus_group.navigate_focus(0)
1217 elif evt == EventType.E_CHFOCUS:
1218 self.__focus_group.navigate_focus(1)
1219 self.__session.schedule(EventType.E_REDRAW)
1221 elif evt == EventType.E_TASK:
1223 elif evt == EventType.E_QUIT:
1224 self.__root.on_quit()
1225 elif evt == EventType.E_KEY:
1226 self._handle_key_event(arg)
1228 self.__root.on_event(evt, arg)
1230 focused = self.__focus_group.focused()
1232 focused.on_event(evt | EventType.E_M_FOCUS, arg)
1234 def redraw(self, win):
1239 def on_layout(self):
1240 self.__root.on_layout()
1243 self.__root.on_draw()
1245 def focus_group(self):
1246 return self.__focus_group
1248 def _handle_key_event(self, key):
1249 if key == ord('\t') or key == curses.KEY_RIGHT:
1250 self.__focus_group.navigate_focus()
1251 elif key == curses.KEY_LEFT:
1252 self.__focus_group.navigate_focus(-1)
1254 self.__root.on_event(EventType.E_KEY, key)
1256 if self.__focus_group.focused():
1257 self.__session.schedule(EventType.E_REDRAW)