# -*- coding: UTF-8 -*- # This file is part of the jetson_stats package (https://github.com/rbonghi/jetson_stats or http://rnext.it). # Copyright (c) 2019-2023 Raffaello Bonghi. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import re import curses # Logging import logging # local modules from ..core.common import get_var from .lib.colors import NColors # Create logger logger = logging.getLogger(__name__) # Gui refresh rate COPYRIGHT_RE = re.compile(r""".*__copyright__ = ["'](.*?)['"]""", re.S) GUI_REFRESH = 1000 // 20 JTOP_MENU_STATUS = 0 JTOP_MENU_EXEC = 1 JTOP_MENU_DESCRIPTION = 2 class JTOPCONFIG: def __init__(self, stdscr, main_page): curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # Initialize variables self._current_menu = main_page self._previous_menu = main_page self._counter_option = 0 # Initialize keyboard status self.key = -1 self.old_key = -1 # Initialize mouse self.mouse = () # Set curses reference, refresh and jetson controller self.stdscr = stdscr # In this program, we don't want keystrokes echoed to the console, # so we run this to disable that curses.noecho() # Additionally, we want to make it so that the user does not have to press # enter to send keys to our program, so here is how we get keys instantly curses.cbreak() # Try to hide the cursor if hasattr(curses, 'curs_set'): try: curses.curs_set(0) except Exception: pass # Lastly, keys such as the arrow keys are sent as funny escape sequences to # our program. We can make curses give us nicer values (such as curses.KEY_LEFT) # so it is easier on us. self.stdscr.keypad(True) # Enable mouse mask _, _ = curses.mousemask(curses.BUTTON1_CLICKED) # Refreshing page curses loop # https://stackoverflow.com/questions/54409978/python-curses-refreshing-text-with-a-loop self.stdscr.nodelay(1) # Run loop self.loop() def draw_menu_page(self, page): height, width = self.stdscr.getmaxyx() # Draw menu menu = [] if 'menu' in page: menu = page['menu'] # Find center on y axis center_y = (height - len(menu)) // 2 # Find center on X axis max_description = max([len(x) for _, _, x in menu]) if len(menu) else 0 center_x = (width - max_description) // 2 # Draw menu for idx, (status, _, description) in enumerate(menu): # Reverse line if counter is the same color = curses.A_REVERSE if self._counter_option == idx else curses.A_NORMAL # Draw command if not None if isinstance(status, str): self.stdscr.addstr(center_y + idx, center_x - 9, status, color) elif callable(status): # Run function and get output if 'run_before' in page: cmd_before_out = page['run_before']() status = status(cmd_before_out) else: status = status() # Print output if isinstance(status, bool): message = " OK " if status else "FAIL" color_status = NColors.green() if status else NColors.red() self.stdscr.addstr(center_y + idx, center_x - 6, message, color_status | curses.A_BOLD) elif isinstance(status, str): self.stdscr.addstr(center_y + idx, center_x - 6, status) # Write description self.stdscr.addstr(center_y + idx, center_x, description, color) # Draw description description_y = 0 if 'description' in page: description = page['description'].split('\n') # Find center description max_line = max([len(x) for x in description]) description_center = (max_description - max_line) // 2 # Draw all lines for idx, line in enumerate(description): self.stdscr.addstr(center_y - len(description) + idx - 1, center_x + description_center, line) description_y = len(description) + 1 # Draw title title = page['title'] title_center = (max_description - len(title)) // 2 self.stdscr.addstr(center_y - 2 - description_y, center_x + title_center, title, curses.A_BOLD) # Draw buttons center_x = (width - 45) // 2 self.stdscr.addstr(center_y + len(menu) + 1, center_x + 5, "Commands:", curses.A_BOLD) # Size 9 if 'menu' in page: # Up key self.stdscr.addch(center_y + len(menu) + 1, center_x + 30, curses.ACS_UARROW) self.stdscr.addstr(center_y + len(menu) + 1, center_x + 32, "Up", curses.A_BOLD) # Down key self.stdscr.addch(center_y + len(menu) + 2, center_x + 30, curses.ACS_DARROW) self.stdscr.addstr(center_y + len(menu) + 2, center_x + 32, "Down", curses.A_BOLD) # Enter self.stdscr.addstr(center_y + len(menu) + 3, center_x + 30, "ENTER", curses.A_BOLD) # Esc button message_button = "ESC = Back" if self._current_menu != self._previous_menu else "ESC = exit" self.stdscr.addstr(center_y + len(menu) + 1, center_x + 16, "{message}".format(message=message_button), curses.A_BOLD) # Quit self.stdscr.addstr(center_y + len(menu) + 2, center_x + 16, "q/Q Quit", curses.A_BOLD) # Draw copyrights self.stdscr.addstr(height - 2, width - 31, "Software parts of jetson-stats", curses.A_NORMAL) self.stdscr.addstr(height - 1, width - 28, get_var(COPYRIGHT_RE), curses.A_NORMAL) def _print_message(self, title, message): return { 'title': title, 'description': message } def loop(self): # Here is the loop of our program, we keep clearing and redrawing in this loop while not self.events(): # First, clear the screen self.stdscr.erase() # Draw menu page self.draw_menu_page(self._current_menu) # Draw the screen self.stdscr.refresh() # Set a timeout and read keystroke self.stdscr.timeout(GUI_REFRESH) def events(self): event = self.stdscr.getch() # Run keyboard check status_keyboard = self.keyboard(event) # Clear event mouse self.mouse = () # Check event mouse if event == curses.KEY_MOUSE: try: _, mx, my, _, _ = curses.getmouse() # Run event menu controller self.mouse = (mx, my) except curses.error: pass return status_keyboard def keyboard(self, event): self.key = event if self.old_key != self.key: # keyboard check list if self.key == ord('q') or self.key == ord('Q'): # keyboard check quit button return True elif self.key == curses.KEY_UP: if self._counter_option > 0: self._counter_option -= 1 elif self.key == curses.KEY_DOWN: if self._counter_option + 1 < len(self._current_menu['menu']): self._counter_option += 1 elif self.key in {curses.KEY_ENTER, 10, 13}: cmd = self._current_menu['menu'][self._counter_option][JTOP_MENU_EXEC] if isinstance(cmd, dict): self._counter_option = 0 self._previous_menu = self._current_menu self._current_menu = cmd elif callable(cmd): # Execute command output = cmd() if output: title = self._current_menu['menu'][self._counter_option][JTOP_MENU_DESCRIPTION] data = self._print_message(title, output) self._counter_option = 0 self._previous_menu = self._current_menu self._current_menu = data elif self.ESC_BUTTON(self.key): if self._current_menu != self._previous_menu: self._current_menu = self._previous_menu self._counter_option = 0 else: return True # Store old value key self.old_key = self.key return False def ESC_BUTTON(self, key): """ Check there is another character prevent combination ALT + https://stackoverflow.com/questions/5977395/ncurses-and-esc-alt-keys """ if key == 27: n = self.stdscr.getch() if n == -1: return True return False # EOF