import os; import re import colorama def render(pos: int, min: int, max: int, render_distance: int, elements: list): start = pos - render_distance; if (start < min): start = min; stop = pos + render_distance + 1; if (stop > max): stop = max; i = 0; y = pos - start; if (y < min): y = pos; if (stop - start != render_distance * 2 + 1): if (pos - start != render_distance): stop = stop + render_distance - start - pos # elif (stop + render_distance > max): # start = start - (stop + render_distance - max) y = pos - start; if (y < min): y = pos; return (start, stop, y); def preview_formater(title: str, body: str, size_x: int, size_y: int): out: str = ""; out += f"┌── {title} " + "─" * (size_x - 6 - printable_len(title)) + "┐\n" lines = body.split("\n"); for line in lines[:size_y - 2]: out += f"│ {line[: size_x - 3]}" + " " * (size_x - 3 - printable_len(line)) + "│\n" out += "└" + "─" * (size_x - 2) + "┘" return (out); strip_ANSI_pat = re.compile(r""" \x1b # literal ESC \[ # literal [ [;\d]* # zero or more digits or semicolons [A-Za-z] # a letter """, re.VERBOSE).sub def printable_len(string: str): mid = strip_ANSI_pat("", string) out = 0; for char in mid: if (char.isprintable()): out += 1; return (out); class Menu(): def __init__(self, options: list, title: str = None, cursor: str = colorama.Back.WHITE + colorama.Fore.BLACK, cursor_pos: int = 0, preview_body_function = None, preview_title_function = None, preview_args: list = None, preview_ratio: float = 0.5, circular: bool = True, skip_empty_option: bool = False, quit_button: bool = False): self.options = options; self.title = title; self.cursor = cursor; self.cursor_pos = cursor_pos; self.cursor_pos_x = 0; self.preview_body_function = preview_body_function; self.preview_title_function = preview_title_function; if (preview_args == None): self.preview_args = options; else: self.preview_args = preview_args; self.circular = circular; self.skip_empty_option = skip_empty_option; self.size = len(options) self.preview_ratio = preview_ratio; self._nb_printed_lines = 0; self.cursor_size = printable_len(cursor) def _up(self): if (self.circular == False): if (self.cursor_pos != 0): self.cursor_pos = self.cursor_pos - 1 else: self.cursor_pos = (self.cursor_pos - 1) % self.size; if (self.skip_empty_option == True): if (printable_len(self.options[self.cursor_pos]) == 0): self._up() self.cursor_pos_x = 0; def _down(self): if (self.circular == False): if (self.cursor_pos != self.size - 1): self.cursor_pos = self.cursor_pos + 1 else: self.cursor_pos = (self.cursor_pos + 1) % self.size; if (self.skip_empty_option == True): if (printable_len(self.options[self.cursor_pos]) == 0): self._down() self.cursor_pos_x = 0; def _left(self): if (self.cursor_pos_x > 0): self.cursor_pos_x = self.cursor_pos_x - 1; def _right(self): self.cursor_pos_x = self.cursor_pos_x + 1; def show(self): while (True): self._display_menu(); key = self._get_input() if (key == "up"): self._up() elif (key == "down"): self._down() if (key == "left"): self._left() elif (key == "right"): self._right() elif (key == "enter"): self._clear(); return (self.cursor_pos) def _display_menu(self): menu: str = ""; self._clear(); size = os.get_terminal_size(); size_x = size.columns; size_y = size.lines; preview_size = int(self.preview_ratio * size_y) content_size = size_y - preview_size - 2 preview = self._preview(size_x, preview_size) if (self.title != None): menu += self.title[:size_x] + "\n"; min_y, max_y, display_pos = render(self.cursor_pos, 0, self.size, content_size // 2, self.options) for i, option in enumerate(self.options[min_y:max_y]): element = option if (i == display_pos): min_x, max_x, display_pos_x = render(self.cursor_pos_x, 0, size_x, size_x // 2, option) line = self.cursor + element[self.cursor_pos_x:size_x - self.cursor_size + self.cursor_pos_x] else: line = " " * self.cursor_size + element[:size_x - self.cursor_size] line = line + colorama.Style.RESET_ALL + "\n" menu = menu + line; menu = menu[:-1] self._display_screen(menu + "\n" + preview) def _preview(self, size_x: int, size_y: int): if (len(self.preview_args) < self.cursor_pos): return ("") if (self.preview_body_function == None): return (""); if (self.preview_title_function == None): title = "preview" else: title = self.preview_title_function(self.preview_args[self.cursor_pos]) body = self.preview_body_function(self.preview_args[self.cursor_pos]) if (body == None or title == None): return ("") return (preview_formater(title, body, size_x, size_y)); def _clear(self): UP = "\x1B[3A" CLR = "\x1B[0K" for _ in range(self._nb_printed_lines): print(CLR) print(UP) def _display_screen(self, screen: str): print(screen) self._nb_printed_lines = screen.count("\n") + 1 def _get_input(self) -> str: flag_have_getch = False flag_have_msvcrt = False try : import getch flag_have_getch = True first_char = getch.getch() if first_char == '\x1b': #arrow keys a=getch.getch() b=getch.getch() return {'[A': 'up', '[B': 'down', '[C': 'right', '[D': 'left' }[a+b] if ord(first_char) == 10: return 'enter' if ord(first_char) == 32: return 'space' else: return first_char #normal keys like abcd 1234 except : pass try: import msvcrt flag_have_msvcrt = True key = msvcrt.getch() # get keypress if key == b'\x1b': # Esc key to exit return 'esc' elif key == b'\r': # Enter key to select return 'enter' elif key == b'\x48': # Up or Down arrow return 'up' elif key == b'\x50': # Up or Down arrow return 'down' else: return key.decode('utf-8') except: pass if flag_have_getch == False and flag_have_msvcrt == False: print('\nErr:\tcan\'t get input \nFix:\tpip install getch') exit() first_char = getch.getch() if first_char == '\x1b': #arrow keys a=getch.getch() b=getch.getch() return {'[A': 'up', '[B': 'down', '[C': 'right', '[D': 'left' }[a+b] if ord(first_char) == 10: return 'enter' if ord(first_char) == 32: return 'space'