# Copyright 2016 The TensorFlow Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Curses-Based Command-Line Interface of TensorFlow Debugger (tfdbg).""" from __future__ import absolute_import from __future__ import division from __future__ import print_function import collections import curses from curses import textpad import os import signal import sys import threading from six.moves import xrange # pylint: disable=redefined-builtin from tensorflow.python.debug.cli import base_ui from tensorflow.python.debug.cli import cli_shared from tensorflow.python.debug.cli import command_parser from tensorflow.python.debug.cli import curses_widgets from tensorflow.python.debug.cli import debugger_cli_common from tensorflow.python.debug.cli import tensor_format _SCROLL_REFRESH = "refresh" _SCROLL_UP = "up" _SCROLL_DOWN = "down" _SCROLL_UP_A_LINE = "up_a_line" _SCROLL_DOWN_A_LINE = "down_a_line" _SCROLL_HOME = "home" _SCROLL_END = "end" _SCROLL_TO_LINE_INDEX = "scroll_to_line_index" _COLOR_READY_COLORTERMS = ["gnome-terminal", "xfce4-terminal"] _COLOR_ENABLED_TERM = "xterm-256color" def _get_command_from_line_attr_segs(mouse_x, attr_segs): """Attempt to extract command from the attribute segments of a line. Args: mouse_x: (int) x coordinate of the mouse event. attr_segs: (list) The list of attribute segments of a line from a RichTextLines object. Returns: (str or None) If a command exists: the command as a str; otherwise, None. """ for seg in attr_segs: if seg[0] <= mouse_x < seg[1]: attributes = seg[2] if isinstance(seg[2], list) else [seg[2]] for attr in attributes: if isinstance(attr, debugger_cli_common.MenuItem): return attr.content class ScrollBar(object): """Vertical ScrollBar for Curses-based CLI. An object of this class has knowledge of the location of the scroll bar in the screen coordinates, the current scrolling position, and the total number of text lines in the screen text. By using this information, it can generate text rendering of the scroll bar, which consists of and UP button on the top and a DOWN button on the bottom, in addition to a scroll block in between, whose exact location is determined by the scrolling position. The object can also calculate the scrolling command (e.g., _SCROLL_UP_A_LINE, _SCROLL_DOWN) from the coordinate of a mouse click event in the screen region it occupies. """ BASE_ATTR = cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE def __init__(self, min_x, min_y, max_x, max_y, scroll_position, output_num_rows): """Constructor of ScrollBar. Args: min_x: (int) left index of the scroll bar on the screen (inclusive). min_y: (int) top index of the scroll bar on the screen (inclusive). max_x: (int) right index of the scroll bar on the screen (inclusive). max_y: (int) bottom index of the scroll bar on the screen (inclusive). scroll_position: (int) 0-based location of the screen output. For example, if the screen output is scrolled to the top, the value of scroll_position should be 0. If it is scrolled to the bottom, the value should be output_num_rows - 1. output_num_rows: (int) Total number of output rows. Raises: ValueError: If the width or height of the scroll bar, as determined by min_x, max_x, min_y and max_y, is too small. """ self._min_x = min_x self._min_y = min_y self._max_x = max_x self._max_y = max_y self._scroll_position = scroll_position self._output_num_rows = output_num_rows self._scroll_bar_height = max_y - min_y + 1 if self._max_x < self._min_x: raise ValueError("Insufficient width for ScrollBar (%d)" % (self._max_x - self._min_x + 1)) if self._max_y < self._min_y + 3: raise ValueError("Insufficient height for ScrollBar (%d)" % (self._max_y - self._min_y + 1)) def _block_y(self, screen_coord_sys=False): """Get the 0-based y coordinate of the scroll block. This y coordinate takes into account the presence of the UP and DN buttons present at the top and bottom of the ScrollBar. For example, at the home location, the return value will be 1; at the bottom location, the return value will be self._scroll_bar_height - 2. Args: screen_coord_sys: (`bool`) whether the return value will be in the screen coordinate system. Returns: (int) 0-based y coordinate of the scroll block, in the ScrollBar coordinate system by default. For example, when scroll position is at the top, this return value will be 1 (not 0, because of the presence of the UP button). When scroll position is at the bottom, this return value will be self._scroll_bar_height - 2 (not self._scroll_bar_height - 1, because of the presence of the DOWN button). """ rel_block_y = int( float(self._scroll_position) / (self._output_num_rows - 1) * (self._scroll_bar_height - 3)) + 1 return rel_block_y + self._min_y if screen_coord_sys else rel_block_y def layout(self): """Get the RichTextLines layout of the scroll bar. Returns: (debugger_cli_common.RichTextLines) The text layout of the scroll bar. """ width = self._max_x - self._min_x + 1 empty_line = " " * width foreground_font_attr_segs = [(0, width, self.BASE_ATTR)] if self._output_num_rows > 1: block_y = self._block_y() if width == 1: up_text = "U" down_text = "D" elif width == 2: up_text = "UP" down_text = "DN" elif width == 3: up_text = "UP " down_text = "DN " else: up_text = " UP " down_text = "DOWN" layout = debugger_cli_common.RichTextLines( [up_text], font_attr_segs={0: [(0, width, self.BASE_ATTR)]}) for i in xrange(1, self._scroll_bar_height - 1): font_attr_segs = foreground_font_attr_segs if i == block_y else None layout.append(empty_line, font_attr_segs=font_attr_segs) layout.append(down_text, font_attr_segs=foreground_font_attr_segs) else: layout = debugger_cli_common.RichTextLines( [empty_line] * self._scroll_bar_height) return layout def get_click_command(self, mouse_y): if self._output_num_rows <= 1: return None elif mouse_y == self._min_y: return _SCROLL_UP_A_LINE elif mouse_y == self._max_y: return _SCROLL_DOWN_A_LINE elif (mouse_y > self._block_y(screen_coord_sys=True) and mouse_y < self._max_y): return _SCROLL_DOWN elif (mouse_y < self._block_y(screen_coord_sys=True) and mouse_y > self._min_y): return _SCROLL_UP else: return None class CursesUI(base_ui.BaseUI): """Curses-based Command-line UI. In this class, the methods with the prefix "_screen_" are the methods that interact with the actual terminal using the curses library. """ CLI_TERMINATOR_KEY = 7 # Terminator key for input text box. CLI_TAB_KEY = ord("\t") BACKSPACE_KEY = ord("\b") REGEX_SEARCH_PREFIX = "/" TENSOR_INDICES_NAVIGATION_PREFIX = "@" _NAVIGATION_FORWARD_COMMAND = "next" _NAVIGATION_BACK_COMMAND = "prev" # Limit screen width to work around the limitation of the curses library that # it may return invalid x coordinates for large values. _SCREEN_WIDTH_LIMIT = 220 # Possible Enter keys. 343 is curses key code for the num-pad Enter key when # num lock is off. CLI_CR_KEYS = [ord("\n"), ord("\r"), 343] _KEY_MAP = { 127: curses.KEY_BACKSPACE, # Backspace curses.KEY_DC: 4, # Delete } _FOREGROUND_COLORS = { cli_shared.COLOR_WHITE: curses.COLOR_WHITE, cli_shared.COLOR_RED: curses.COLOR_RED, cli_shared.COLOR_GREEN: curses.COLOR_GREEN, cli_shared.COLOR_YELLOW: curses.COLOR_YELLOW, cli_shared.COLOR_BLUE: curses.COLOR_BLUE, cli_shared.COLOR_CYAN: curses.COLOR_CYAN, cli_shared.COLOR_MAGENTA: curses.COLOR_MAGENTA, cli_shared.COLOR_BLACK: curses.COLOR_BLACK, } _BACKGROUND_COLORS = { "transparent": -1, cli_shared.COLOR_WHITE: curses.COLOR_WHITE, cli_shared.COLOR_BLACK: curses.COLOR_BLACK, } # Font attribute for search and highlighting. _SEARCH_HIGHLIGHT_FONT_ATTR = ( cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE) _ARRAY_INDICES_COLOR_PAIR = ( cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE) _ERROR_TOAST_COLOR_PAIR = ( cli_shared.COLOR_RED + "_on_" + cli_shared.COLOR_WHITE) _INFO_TOAST_COLOR_PAIR = ( cli_shared.COLOR_BLUE + "_on_" + cli_shared.COLOR_WHITE) _STATUS_BAR_COLOR_PAIR = ( cli_shared.COLOR_BLACK + "_on_" + cli_shared.COLOR_WHITE) _UI_WAIT_COLOR_PAIR = ( cli_shared.COLOR_MAGENTA + "_on_" + cli_shared.COLOR_WHITE) _NAVIGATION_WARNING_COLOR_PAIR = ( cli_shared.COLOR_RED + "_on_" + cli_shared.COLOR_WHITE) _UI_WAIT_MESSAGE = "Processing..." # The delay (in ms) between each update of the scroll bar when the mouse # button is held down on the scroll bar. Controls how fast the screen scrolls. _MOUSE_SCROLL_DELAY_MS = 100 _single_instance_lock = threading.Lock() def __init__(self, on_ui_exit=None, config=None): """Constructor of CursesUI. Args: on_ui_exit: (Callable) Callback invoked when the UI exits. config: An instance of `cli_config.CLIConfig()` carrying user-facing configurations. """ base_ui.BaseUI.__init__(self, on_ui_exit=on_ui_exit, config=config) self._screen_init() self._screen_refresh_size() # TODO(cais): Error out if the size of the screen is too small. # Initialize some UI component size and locations. self._init_layout() self._command_history_store = debugger_cli_common.CommandHistory() # Active list of command history, used in history navigation. # _command_handler_registry holds all the history commands the CLI has # received, up to a size limit. _active_command_history is the history # currently being navigated in, e.g., using the Up/Down keys. The latter # can be different from the former during prefixed or regex-based history # navigation, e.g., when user enter the beginning of a command and hit Up. self._active_command_history = [] # Pointer to the current position in the history sequence. # 0 means it is a new command being keyed in. self._command_pointer = 0 self._command_history_limit = 100 self._pending_command = "" self._nav_history = curses_widgets.CursesNavigationHistory(10) # State related to screen output. self._output_pad = None self._output_pad_row = 0 self._output_array_pointer_indices = None self._curr_unwrapped_output = None self._curr_wrapped_output = None try: # Register signal handler for SIGINT. signal.signal(signal.SIGINT, self._interrupt_handler) except ValueError: # Running in a child thread, can't catch signals. pass self.register_command_handler( "mouse", self._mouse_mode_command_handler, "Get or set the mouse mode of this CLI: (on|off)", prefix_aliases=["m"]) def _init_layout(self): """Initialize the layout of UI components. Initialize the location and size of UI components such as command textbox and output region according to the terminal size. """ # NamedTuple for rectangular locations on screen self.rectangle = collections.namedtuple("rectangle", "top left bottom right") # Height of command text box self._command_textbox_height = 2 self._title_row = 0 # Row index of the Navigation Bar (i.e., the bar that contains forward and # backward buttons and displays the current command line). self._nav_bar_row = 1 # Top row index of the output pad. # A "pad" is a curses object that holds lines of text and not limited to # screen size. It can be rendered on the screen partially with scroll # parameters specified. self._output_top_row = 2 # Number of rows that the output pad has. self._output_num_rows = ( self._max_y - self._output_top_row - self._command_textbox_height - 1) # Row index of scroll information line: Taking into account the zero-based # row indexing and the command textbox area under the scroll information # row. self._output_scroll_row = self._max_y - 1 - self._command_textbox_height # Tab completion bottom row. self._candidates_top_row = self._output_scroll_row - 4 self._candidates_bottom_row = self._output_scroll_row - 1 # Maximum number of lines the candidates display can have. self._candidates_max_lines = int(self._output_num_rows / 2) self.max_output_lines = 10000 # Regex search state. self._curr_search_regex = None self._unwrapped_regex_match_lines = [] # Size of view port on screen, which is always smaller or equal to the # screen size. self._output_pad_screen_height = self._output_num_rows - 1 self._output_pad_screen_width = self._max_x - 2 self._output_pad_screen_location = self.rectangle( top=self._output_top_row, left=0, bottom=self._output_top_row + self._output_num_rows, right=self._output_pad_screen_width) def _screen_init(self): """Screen initialization. Creates curses stdscr and initialize the color pairs for display. """ # If the terminal type is color-ready, enable it. if os.getenv("COLORTERM") in _COLOR_READY_COLORTERMS: os.environ["TERM"] = _COLOR_ENABLED_TERM self._stdscr = curses.initscr() self._command_window = None self._screen_color_init() def _screen_color_init(self): """Initialization of screen colors.""" curses.start_color() curses.use_default_colors() self._color_pairs = {} color_index = 0 # Prepare color pairs. for fg_color in self._FOREGROUND_COLORS: for bg_color in self._BACKGROUND_COLORS: color_index += 1 curses.init_pair(color_index, self._FOREGROUND_COLORS[fg_color], self._BACKGROUND_COLORS[bg_color]) color_name = fg_color if bg_color != "transparent": color_name += "_on_" + bg_color self._color_pairs[color_name] = curses.color_pair(color_index) # Try getting color(s) available only under 256-color support. try: color_index += 1 curses.init_pair(color_index, 245, -1) self._color_pairs[cli_shared.COLOR_GRAY] = curses.color_pair(color_index) except curses.error: # Use fall-back color(s): self._color_pairs[cli_shared.COLOR_GRAY] = ( self._color_pairs[cli_shared.COLOR_GREEN]) # A_BOLD or A_BLINK is not really a "color". But place it here for # convenience. self._color_pairs["bold"] = curses.A_BOLD self._color_pairs["blink"] = curses.A_BLINK self._color_pairs["underline"] = curses.A_UNDERLINE # Default color pair to use when a specified color pair does not exist. self._default_color_pair = self._color_pairs[cli_shared.COLOR_WHITE] def _screen_launch(self, enable_mouse_on_start): """Launch the curses screen.""" curses.noecho() curses.cbreak() self._stdscr.keypad(1) self._mouse_enabled = self.config.get("mouse_mode") self._screen_set_mousemask() self.config.set_callback( "mouse_mode", lambda cfg: self._set_mouse_enabled(cfg.get("mouse_mode"))) self._screen_create_command_window() def _screen_create_command_window(self): """Create command window according to screen size.""" if self._command_window: del self._command_window self._command_window = curses.newwin( self._command_textbox_height, self._max_x - len(self.CLI_PROMPT), self._max_y - self._command_textbox_height, len(self.CLI_PROMPT)) def _screen_refresh(self): self._stdscr.refresh() def _screen_terminate(self): """Terminate the curses screen.""" self._stdscr.keypad(0) curses.nocbreak() curses.echo() curses.endwin() try: # Remove SIGINT handler. signal.signal(signal.SIGINT, signal.SIG_DFL) except ValueError: # Can't catch signals unless you're the main thread. pass def run_ui(self, init_command=None, title=None, title_color=None, enable_mouse_on_start=True): """Run the CLI: See the doc of base_ui.BaseUI.run_ui for more details.""" # Only one instance of the Curses UI can be running at a time, since # otherwise they would try to both read from the same keystrokes, and write # to the same screen. self._single_instance_lock.acquire() self._screen_launch(enable_mouse_on_start=enable_mouse_on_start) # Optional initial command. if init_command is not None: self._dispatch_command(init_command) if title is not None: self._title(title, title_color=title_color) # CLI main loop. exit_token = self._ui_loop() if self._on_ui_exit: self._on_ui_exit() self._screen_terminate() self._single_instance_lock.release() return exit_token def get_help(self): return self._command_handler_registry.get_help() def _addstr(self, *args): try: self._stdscr.addstr(*args) except curses.error: pass def _refresh_pad(self, pad, *args): try: pad.refresh(*args) except curses.error: pass def _screen_create_command_textbox(self, existing_command=None): """Create command textbox on screen. Args: existing_command: (str) A command string to put in the textbox right after its creation. """ # Display the tfdbg prompt. self._addstr(self._max_y - self._command_textbox_height, 0, self.CLI_PROMPT, curses.A_BOLD) self._stdscr.refresh() self._command_window.clear() # Command text box. self._command_textbox = textpad.Textbox( self._command_window, insert_mode=True) # Enter existing command. self._auto_key_in(existing_command) def _ui_loop(self): """Command-line UI loop. Returns: An exit token of arbitrary type. The token can be None. """ while True: # Enter history command if pointer is in history (> 0): if self._command_pointer > 0: existing_command = self._active_command_history[-self._command_pointer] else: existing_command = self._pending_command self._screen_create_command_textbox(existing_command) try: command, terminator, pending_command_changed = self._get_user_command() except debugger_cli_common.CommandLineExit as e: return e.exit_token if not command and terminator != self.CLI_TAB_KEY: continue if terminator in self.CLI_CR_KEYS or terminator == curses.KEY_MOUSE: exit_token = self._dispatch_command(command) if exit_token is not None: return exit_token elif terminator == self.CLI_TAB_KEY: tab_completed = self._tab_complete(command) self._pending_command = tab_completed self._cmd_ptr = 0 elif pending_command_changed: self._pending_command = command return def _get_user_command(self): """Get user command from UI. Returns: command: (str) The user-entered command. terminator: (str) Terminator type for the command. If command is a normal command entered with the Enter key, the value will be the key itself. If this is a tab completion call (using the Tab key), the value will reflect that as well. pending_command_changed: (bool) If the pending command has changed. Used during command history navigation. """ # First, reset textbox state variables. self._textbox_curr_terminator = None self._textbox_pending_command_changed = False command = self._screen_get_user_command() command = self._strip_terminator(command) return (command, self._textbox_curr_terminator, self._textbox_pending_command_changed) def _screen_get_user_command(self): return self._command_textbox.edit(validate=self._on_textbox_keypress) def _strip_terminator(self, command): if not command: return command for v in self.CLI_CR_KEYS: if v < 256: command = command.replace(chr(v), "") return command.strip() def _screen_refresh_size(self): self._max_y, self._max_x = self._stdscr.getmaxyx() if self._max_x > self._SCREEN_WIDTH_LIMIT: self._max_x = self._SCREEN_WIDTH_LIMIT def _navigate_screen_output(self, command): """Navigate in screen output history. Args: command: (`str`) the navigation command, from {self._NAVIGATION_FORWARD_COMMAND, self._NAVIGATION_BACK_COMMAND}. """ if command == self._NAVIGATION_FORWARD_COMMAND: if self._nav_history.can_go_forward(): item = self._nav_history.go_forward() scroll_position = item.scroll_position else: self._toast("At the LATEST in navigation history!", color=self._NAVIGATION_WARNING_COLOR_PAIR) return else: if self._nav_history.can_go_back(): item = self._nav_history.go_back() scroll_position = item.scroll_position else: self._toast("At the OLDEST in navigation history!", color=self._NAVIGATION_WARNING_COLOR_PAIR) return self._display_output(item.screen_output) if scroll_position != 0: self._scroll_output(_SCROLL_TO_LINE_INDEX, line_index=scroll_position) def _dispatch_command(self, command): """Dispatch user command. Args: command: (str) Command to dispatch. Returns: An exit token object. None value means that the UI loop should not exit. A non-None value means the UI loop should exit. """ if self._output_pad: self._toast(self._UI_WAIT_MESSAGE, color=self._UI_WAIT_COLOR_PAIR) if command in self.CLI_EXIT_COMMANDS: # Explicit user command-triggered exit: EXPLICIT_USER_EXIT as the exit # token. return debugger_cli_common.EXPLICIT_USER_EXIT elif (command == self._NAVIGATION_FORWARD_COMMAND or command == self._NAVIGATION_BACK_COMMAND): self._navigate_screen_output(command) return if command: self._command_history_store.add_command(command) if (command.startswith(self.REGEX_SEARCH_PREFIX) and self._curr_unwrapped_output): if len(command) > len(self.REGEX_SEARCH_PREFIX): # Command is like "/regex". Perform regex search. regex = command[len(self.REGEX_SEARCH_PREFIX):] self._curr_search_regex = regex self._display_output(self._curr_unwrapped_output, highlight_regex=regex) elif self._unwrapped_regex_match_lines: # Command is "/". Continue scrolling down matching lines. self._display_output( self._curr_unwrapped_output, is_refresh=True, highlight_regex=self._curr_search_regex) self._command_pointer = 0 self._pending_command = "" return elif command.startswith(self.TENSOR_INDICES_NAVIGATION_PREFIX): indices_str = command[1:].strip() if indices_str: try: indices = command_parser.parse_indices(indices_str) omitted, line_index, _, _ = tensor_format.locate_tensor_element( self._curr_wrapped_output, indices) if not omitted: self._scroll_output( _SCROLL_TO_LINE_INDEX, line_index=line_index) except Exception as e: # pylint: disable=broad-except self._error_toast(str(e)) else: self._error_toast("Empty indices.") return try: prefix, args, output_file_path = self._parse_command(command) except SyntaxError as e: self._error_toast(str(e)) return if not prefix: # Empty command: take no action. Should not exit. return # Take into account scroll bar width. screen_info = {"cols": self._max_x - 2} exit_token = None if self._command_handler_registry.is_registered(prefix): try: screen_output = self._command_handler_registry.dispatch_command( prefix, args, screen_info=screen_info) except debugger_cli_common.CommandLineExit as e: exit_token = e.exit_token else: screen_output = debugger_cli_common.RichTextLines([ self.ERROR_MESSAGE_PREFIX + "Invalid command prefix \"%s\"" % prefix ]) # Clear active command history. Until next up/down history navigation # occurs, it will stay empty. self._active_command_history = [] if exit_token is not None: return exit_token self._nav_history.add_item(command, screen_output, 0) self._display_output(screen_output) if output_file_path: try: screen_output.write_to_file(output_file_path) self._info_toast("Wrote output to %s" % output_file_path) except Exception: # pylint: disable=broad-except self._error_toast("Failed to write output to %s" % output_file_path) self._command_pointer = 0 self._pending_command = "" def _screen_gather_textbox_str(self): """Gather the text string in the command text box. Returns: (str) the current text string in the command textbox, excluding any return keys. """ txt = self._command_textbox.gather() return txt.strip() def _on_textbox_keypress(self, x): """Text box key validator: Callback of key strokes. Handles a user's keypress in the input text box. Translates certain keys to terminator keys for the textbox to allow its edit() method to return. Also handles special key-triggered events such as PgUp/PgDown scrolling of the screen output. Args: x: (int) Key code. Returns: (int) A translated key code. In most cases, this is identical to the input x. However, if x is a Return key, the return value will be CLI_TERMINATOR_KEY, so that the text box's edit() method can return. Raises: TypeError: If the input x is not of type int. debugger_cli_common.CommandLineExit: If a mouse-triggered command returns an exit token when dispatched. """ if not isinstance(x, int): raise TypeError("Key validator expected type int, received type %s" % type(x)) if x in self.CLI_CR_KEYS: # Make Enter key the terminator self._textbox_curr_terminator = x return self.CLI_TERMINATOR_KEY elif x == self.CLI_TAB_KEY: self._textbox_curr_terminator = self.CLI_TAB_KEY return self.CLI_TERMINATOR_KEY elif x == curses.KEY_PPAGE: self._scroll_output(_SCROLL_UP_A_LINE) return x elif x == curses.KEY_NPAGE: self._scroll_output(_SCROLL_DOWN_A_LINE) return x elif x == curses.KEY_HOME: self._scroll_output(_SCROLL_HOME) return x elif x == curses.KEY_END: self._scroll_output(_SCROLL_END) return x elif x in [curses.KEY_UP, curses.KEY_DOWN]: # Command history navigation. if not self._active_command_history: hist_prefix = self._screen_gather_textbox_str() self._active_command_history = ( self._command_history_store.lookup_prefix( hist_prefix, self._command_history_limit)) if self._active_command_history: if x == curses.KEY_UP: if self._command_pointer < len(self._active_command_history): self._command_pointer += 1 elif x == curses.KEY_DOWN: if self._command_pointer > 0: self._command_pointer -= 1 else: self._command_pointer = 0 self._textbox_curr_terminator = x # Force return from the textbox edit(), so that the textbox can be # redrawn with a history command entered. return self.CLI_TERMINATOR_KEY elif x == curses.KEY_RESIZE: # Respond to terminal resize. self._screen_refresh_size() self._init_layout() self._screen_create_command_window() self._redraw_output() # Force return from the textbox edit(), so that the textbox can be # redrawn. return self.CLI_TERMINATOR_KEY elif x == curses.KEY_MOUSE and self._mouse_enabled: try: _, mouse_x, mouse_y, _, mouse_event_type = self._screen_getmouse() except curses.error: mouse_event_type = None if mouse_event_type == curses.BUTTON1_PRESSED: # Logic for held mouse-triggered scrolling. if mouse_x >= self._max_x - 2: # Disable blocking on checking for user input. self._command_window.nodelay(True) # Loop while mouse button is pressed. while mouse_event_type == curses.BUTTON1_PRESSED: # Sleep for a bit. curses.napms(self._MOUSE_SCROLL_DELAY_MS) scroll_command = self._scroll_bar.get_click_command(mouse_y) if scroll_command in (_SCROLL_UP_A_LINE, _SCROLL_DOWN_A_LINE): self._scroll_output(scroll_command) # Check to see if different mouse event is in queue. self._command_window.getch() try: _, _, _, _, mouse_event_type = self._screen_getmouse() except curses.error: pass self._command_window.nodelay(False) return x elif mouse_event_type == curses.BUTTON1_RELEASED: # Logic for mouse-triggered scrolling. if mouse_x >= self._max_x - 2: scroll_command = self._scroll_bar.get_click_command(mouse_y) if scroll_command is not None: self._scroll_output(scroll_command) return x else: command = self._fetch_hyperlink_command(mouse_x, mouse_y) if command: self._screen_create_command_textbox() exit_token = self._dispatch_command(command) if exit_token is not None: raise debugger_cli_common.CommandLineExit(exit_token=exit_token) else: # Mark the pending command as modified. self._textbox_pending_command_changed = True # Invalidate active command history. self._command_pointer = 0 self._active_command_history = [] return self._KEY_MAP.get(x, x) def _screen_getmouse(self): return curses.getmouse() def _redraw_output(self): if self._curr_unwrapped_output is not None: self._display_nav_bar() self._display_main_menu(self._curr_unwrapped_output) self._display_output(self._curr_unwrapped_output, is_refresh=True) def _fetch_hyperlink_command(self, mouse_x, mouse_y): output_top = self._output_top_row if self._main_menu_pad: output_top += 1 if mouse_y == self._nav_bar_row and self._nav_bar: # Click was in the nav bar. return _get_command_from_line_attr_segs(mouse_x, self._nav_bar.font_attr_segs[0]) elif mouse_y == self._output_top_row and self._main_menu_pad: # Click was in the menu bar. return _get_command_from_line_attr_segs(mouse_x, self._main_menu.font_attr_segs[0]) else: absolute_mouse_y = mouse_y + self._output_pad_row - output_top if absolute_mouse_y in self._curr_wrapped_output.font_attr_segs: return _get_command_from_line_attr_segs( mouse_x, self._curr_wrapped_output.font_attr_segs[absolute_mouse_y]) def _title(self, title, title_color=None): """Display title. Args: title: (str) The title to display. title_color: (str) Color of the title, e.g., "yellow". """ # Pad input title str with "-" and space characters to make it pretty. self._title_line = "--- %s " % title if len(self._title_line) < self._max_x: self._title_line += "-" * (self._max_x - len(self._title_line)) self._screen_draw_text_line( self._title_row, self._title_line, color=title_color) def _auto_key_in(self, command, erase_existing=False): """Automatically key in a command to the command Textbox. Args: command: The command, as a string or None. erase_existing: (bool) whether existing text (if any) is to be erased first. """ if erase_existing: self._erase_existing_command() command = command or "" for c in command: self._command_textbox.do_command(ord(c)) def _erase_existing_command(self): """Erase existing text in command textpad.""" existing_len = len(self._command_textbox.gather()) for _ in xrange(existing_len): self._command_textbox.do_command(self.BACKSPACE_KEY) def _screen_draw_text_line(self, row, line, attr=curses.A_NORMAL, color=None): """Render a line of text on the screen. Args: row: (int) Row index. line: (str) The line content. attr: curses font attribute. color: (str) font foreground color name. Raises: TypeError: If row is not of type int. """ if not isinstance(row, int): raise TypeError("Invalid type in row") if len(line) > self._max_x: line = line[:self._max_x] color_pair = (self._default_color_pair if color is None else self._color_pairs[color]) self._addstr(row, 0, line, color_pair | attr) self._screen_refresh() def _screen_new_output_pad(self, rows, cols): """Generate a new pad on the screen. Args: rows: (int) Number of rows the pad will have: not limited to screen size. cols: (int) Number of columns the pad will have: not limited to screen size. Returns: A curses textpad object. """ return curses.newpad(rows, cols) def _screen_display_output(self, output): """Actually render text output on the screen. Wraps the lines according to screen width. Pad lines below according to screen height so that the user can scroll the output to a state where the last non-empty line is on the top of the screen. Then renders the lines on the screen. Args: output: (RichTextLines) text lines to display on the screen. These lines may have widths exceeding the screen width. This method will take care of the wrapping. Returns: (List of int) A list of line indices, in the wrapped output, where there are regex matches. """ # Wrap the output lines according to screen width. self._curr_wrapped_output, wrapped_line_indices = ( debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 2)) # Append lines to curr_wrapped_output so that the user can scroll to a # state where the last text line is on the top of the output area. self._curr_wrapped_output.lines.extend([""] * (self._output_num_rows - 1)) # Limit number of lines displayed to avoid curses overflow problems. if self._curr_wrapped_output.num_lines() > self.max_output_lines: self._curr_wrapped_output = self._curr_wrapped_output.slice( 0, self.max_output_lines) self._curr_wrapped_output.lines.append("Output cut off at %d lines!" % self.max_output_lines) self._curr_wrapped_output.font_attr_segs[self.max_output_lines] = [ (0, len(output.lines[-1]), cli_shared.COLOR_MAGENTA) ] self._display_nav_bar() self._display_main_menu(self._curr_wrapped_output) (self._output_pad, self._output_pad_height, self._output_pad_width) = self._display_lines(self._curr_wrapped_output, self._output_num_rows) # The indices of lines with regex matches (if any) need to be mapped to # indices of wrapped lines. return [ wrapped_line_indices[line] for line in self._unwrapped_regex_match_lines ] def _display_output(self, output, is_refresh=False, highlight_regex=None): """Display text output in a scrollable text pad. This method does some preprocessing on the text lines, render them on the screen and scroll to the appropriate line. These are done according to regex highlighting requests (if any), scroll-to-next-match requests (if any), and screen refresh requests (if any). TODO(cais): Separate these unrelated request to increase clarity and maintainability. Args: output: A RichTextLines object that is the screen output text. is_refresh: (bool) Is this a refreshing display with existing output. highlight_regex: (str) Optional string representing the regex used to search and highlight in the current screen output. """ if not output: return if highlight_regex: try: output = debugger_cli_common.regex_find( output, highlight_regex, font_attr=self._SEARCH_HIGHLIGHT_FONT_ATTR) except ValueError as e: self._error_toast(str(e)) return if not is_refresh: # Perform new regex search on the current output. self._unwrapped_regex_match_lines = output.annotations[ debugger_cli_common.REGEX_MATCH_LINES_KEY] else: # Continue scrolling down. self._output_pad_row += 1 else: self._curr_unwrapped_output = output self._unwrapped_regex_match_lines = [] # Display output on the screen. wrapped_regex_match_lines = self._screen_display_output(output) # Now that the text lines are displayed on the screen scroll to the # appropriate line according to previous scrolling state and regex search # and highlighting state. if highlight_regex: next_match_line = -1 for match_line in wrapped_regex_match_lines: if match_line >= self._output_pad_row: next_match_line = match_line break if next_match_line >= 0: self._scroll_output( _SCROLL_TO_LINE_INDEX, line_index=next_match_line) else: # Regex search found no match >= current line number. Display message # stating as such. self._toast("Pattern not found", color=self._ERROR_TOAST_COLOR_PAIR) elif is_refresh: self._scroll_output(_SCROLL_REFRESH) elif debugger_cli_common.INIT_SCROLL_POS_KEY in output.annotations: line_index = output.annotations[debugger_cli_common.INIT_SCROLL_POS_KEY] self._scroll_output(_SCROLL_TO_LINE_INDEX, line_index=line_index) else: self._output_pad_row = 0 self._scroll_output(_SCROLL_HOME) def _display_lines(self, output, min_num_rows): """Display RichTextLines object on screen. Args: output: A RichTextLines object. min_num_rows: (int) Minimum number of output rows. Returns: 1) The text pad object used to display the main text body. 2) (int) number of rows of the text pad, which may exceed screen size. 3) (int) number of columns of the text pad. Raises: ValueError: If input argument "output" is invalid. """ if not isinstance(output, debugger_cli_common.RichTextLines): raise ValueError( "Output is required to be an instance of RichTextLines, but is not.") self._screen_refresh() # Number of rows the output area will have. rows = max(min_num_rows, len(output.lines)) # Size of the output pad, which may exceed screen size and require # scrolling. cols = self._max_x - 2 # Create new output pad. pad = self._screen_new_output_pad(rows, cols) for i in xrange(len(output.lines)): if i in output.font_attr_segs: self._screen_add_line_to_output_pad( pad, i, output.lines[i], color_segments=output.font_attr_segs[i]) else: self._screen_add_line_to_output_pad(pad, i, output.lines[i]) return pad, rows, cols def _display_nav_bar(self): nav_bar_width = self._max_x - 2 self._nav_bar_pad = self._screen_new_output_pad(1, nav_bar_width) self._nav_bar = self._nav_history.render( nav_bar_width, self._NAVIGATION_BACK_COMMAND, self._NAVIGATION_FORWARD_COMMAND) self._screen_add_line_to_output_pad( self._nav_bar_pad, 0, self._nav_bar.lines[0][:nav_bar_width - 1], color_segments=(self._nav_bar.font_attr_segs[0] if 0 in self._nav_bar.font_attr_segs else None)) def _display_main_menu(self, output): """Display main menu associated with screen output, if the menu exists. Args: output: (debugger_cli_common.RichTextLines) The RichTextLines output from the annotations field of which the menu will be extracted and used (if the menu exists). """ if debugger_cli_common.MAIN_MENU_KEY in output.annotations: self._main_menu = output.annotations[ debugger_cli_common.MAIN_MENU_KEY].format_as_single_line( prefix="| ", divider=" | ", enabled_item_attrs=["underline"]) self._main_menu_pad = self._screen_new_output_pad(1, self._max_x - 2) # The unwrapped menu line may exceed screen width, in which case it needs # to be cut off. wrapped_menu, _ = debugger_cli_common.wrap_rich_text_lines( self._main_menu, self._max_x - 3) self._screen_add_line_to_output_pad( self._main_menu_pad, 0, wrapped_menu.lines[0], color_segments=(wrapped_menu.font_attr_segs[0] if 0 in wrapped_menu.font_attr_segs else None)) else: self._main_menu = None self._main_menu_pad = None def _pad_line_end_with_whitespace(self, pad, row, line_end_x): """Pad the whitespace at the end of a line with the default color pair. Prevents spurious color pairs from appearing at the end of the lines in certain text terminals. Args: pad: The curses pad object to operate on. row: (`int`) row index. line_end_x: (`int`) column index of the end of the line (beginning of the whitespace). """ if line_end_x < self._max_x - 2: pad.addstr(row, line_end_x, " " * (self._max_x - 3 - line_end_x), self._default_color_pair) def _screen_add_line_to_output_pad(self, pad, row, txt, color_segments=None): """Render a line in a text pad. Assumes: segments in color_segments are sorted in ascending order of the beginning index. Note: Gaps between the segments are allowed and will be fixed in with a default color. Args: pad: The text pad to render the line in. row: Row index, as an int. txt: The text to be displayed on the specified row, as a str. color_segments: A list of 3-tuples. Each tuple represents the beginning and the end of a color segment, in the form of a right-open interval: [start, end). The last element of the tuple is a color string, e.g., "red". Raisee: TypeError: If color_segments is not of type list. """ if not color_segments: pad.addstr(row, 0, txt, self._default_color_pair) self._pad_line_end_with_whitespace(pad, row, len(txt)) return if not isinstance(color_segments, list): raise TypeError("Input color_segments needs to be a list, but is not.") all_segments = [] all_color_pairs = [] # Process the beginning. if color_segments[0][0] == 0: pass else: all_segments.append((0, color_segments[0][0])) all_color_pairs.append(self._default_color_pair) for (curr_start, curr_end, curr_attrs), (next_start, _, _) in zip( color_segments, color_segments[1:] + [(len(txt), None, None)]): all_segments.append((curr_start, curr_end)) if not isinstance(curr_attrs, list): curr_attrs = [curr_attrs] curses_attr = curses.A_NORMAL for attr in curr_attrs: if (self._mouse_enabled and isinstance(attr, debugger_cli_common.MenuItem)): curses_attr |= curses.A_UNDERLINE else: curses_attr |= self._color_pairs.get(attr, self._default_color_pair) all_color_pairs.append(curses_attr) if curr_end < next_start: # Fill in the gap with the default color. all_segments.append((curr_end, next_start)) all_color_pairs.append(self._default_color_pair) # Finally, draw all the segments. for segment, color_pair in zip(all_segments, all_color_pairs): if segment[1] < self._max_x: pad.addstr(row, segment[0], txt[segment[0]:segment[1]], color_pair) if all_segments: self._pad_line_end_with_whitespace(pad, row, all_segments[-1][1]) def _screen_scroll_output_pad(self, pad, viewport_top, viewport_left, screen_location_top, screen_location_left, screen_location_bottom, screen_location_right): self._refresh_pad(pad, viewport_top, viewport_left, screen_location_top, screen_location_left, screen_location_bottom, screen_location_right) self._scroll_bar = ScrollBar( self._max_x - 2, 3, self._max_x - 1, self._output_num_rows + 1, self._output_pad_row, self._output_pad_height - self._output_pad_screen_height) (scroll_pad, _, _) = self._display_lines( self._scroll_bar.layout(), self._output_num_rows - 1) self._refresh_pad(scroll_pad, 0, 0, self._output_top_row + 1, self._max_x - 2, self._output_num_rows + 1, self._max_x - 1) def _scroll_output(self, direction, line_index=None): """Scroll the output pad. Args: direction: _SCROLL_REFRESH, _SCROLL_UP, _SCROLL_DOWN, _SCROLL_UP_A_LINE, _SCROLL_DOWN_A_LINE, _SCROLL_HOME, _SCROLL_END, _SCROLL_TO_LINE_INDEX line_index: (int) Specifies the zero-based line index to scroll to. Applicable only if direction is _SCROLL_TO_LINE_INDEX. Raises: ValueError: On invalid scroll direction. TypeError: If line_index is not int and direction is _SCROLL_TO_LINE_INDEX. """ if not self._output_pad: # No output pad is present. Do nothing. return if direction == _SCROLL_REFRESH: pass elif direction == _SCROLL_UP: # Scroll up. self._output_pad_row -= int(self._output_num_rows / 3) if self._output_pad_row < 0: self._output_pad_row = 0 elif direction == _SCROLL_DOWN: # Scroll down. self._output_pad_row += int(self._output_num_rows / 3) if (self._output_pad_row > self._output_pad_height - self._output_pad_screen_height - 1): self._output_pad_row = ( self._output_pad_height - self._output_pad_screen_height - 1) elif direction == _SCROLL_UP_A_LINE: # Scroll up a line if self._output_pad_row - 1 >= 0: self._output_pad_row -= 1 elif direction == _SCROLL_DOWN_A_LINE: # Scroll down a line if self._output_pad_row + 1 < ( self._output_pad_height - self._output_pad_screen_height): self._output_pad_row += 1 elif direction == _SCROLL_HOME: # Scroll to top self._output_pad_row = 0 elif direction == _SCROLL_END: # Scroll to bottom self._output_pad_row = ( self._output_pad_height - self._output_pad_screen_height - 1) elif direction == _SCROLL_TO_LINE_INDEX: if not isinstance(line_index, int): raise TypeError("Invalid line_index type (%s) under mode %s" % (type(line_index), _SCROLL_TO_LINE_INDEX)) self._output_pad_row = line_index else: raise ValueError("Unsupported scroll mode: %s" % direction) self._nav_history.update_scroll_position(self._output_pad_row) # Actually scroll the output pad: refresh with new location. output_pad_top = self._output_pad_screen_location.top if self._main_menu_pad: output_pad_top += 1 self._screen_scroll_output_pad(self._output_pad, self._output_pad_row, 0, output_pad_top, self._output_pad_screen_location.left, self._output_pad_screen_location.bottom, self._output_pad_screen_location.right) self._screen_render_nav_bar() self._screen_render_menu_pad() self._scroll_info = self._compile_ui_status_summary() self._screen_draw_text_line( self._output_scroll_row, self._scroll_info, color=self._STATUS_BAR_COLOR_PAIR) def _screen_render_nav_bar(self): if self._nav_bar_pad: self._refresh_pad(self._nav_bar_pad, 0, 0, self._nav_bar_row, 0, self._output_pad_screen_location.top, self._max_x) def _screen_render_menu_pad(self): if self._main_menu_pad: self._refresh_pad( self._main_menu_pad, 0, 0, self._output_pad_screen_location.top, 0, self._output_pad_screen_location.top, self._max_x) def _compile_ui_status_summary(self): """Compile status summary about this Curses UI instance. The information includes: scroll status and mouse ON/OFF status. Returns: (str) A single text line summarizing the UI status, adapted to the current screen width. """ info = "" if self._output_pad_height > self._output_pad_screen_height + 1: # Display information about the scrolling of tall screen output. scroll_percentage = 100.0 * (min( 1.0, float(self._output_pad_row) / (self._output_pad_height - self._output_pad_screen_height - 1))) if self._output_pad_row == 0: scroll_directions = " (PgDn)" elif self._output_pad_row >= ( self._output_pad_height - self._output_pad_screen_height - 1): scroll_directions = " (PgUp)" else: scroll_directions = " (PgDn/PgUp)" info += "--- Scroll%s: %.2f%% " % (scroll_directions, scroll_percentage) self._output_array_pointer_indices = self._show_array_indices() # Add array indices information to scroll message. if self._output_array_pointer_indices: if self._output_array_pointer_indices[0]: info += self._format_indices(self._output_array_pointer_indices[0]) info += "-" if self._output_array_pointer_indices[-1]: info += self._format_indices(self._output_array_pointer_indices[-1]) info += " " # Add mouse mode information. mouse_mode_str = "Mouse: " mouse_mode_str += "ON" if self._mouse_enabled else "OFF" if len(info) + len(mouse_mode_str) + 5 < self._max_x: info += "-" * (self._max_x - len(info) - len(mouse_mode_str) - 4) info += " " info += mouse_mode_str info += " ---" else: info += "-" * (self._max_x - len(info)) return info def _format_indices(self, indices): # Remove the spaces to make it compact. return repr(indices).replace(" ", "") def _show_array_indices(self): """Show array indices for the lines at the top and bottom of the output. For the top line and bottom line of the output display area, show the element indices of the array being displayed. Returns: If either the top of the bottom row has any matching array indices, a dict from line index (0 being the top of the display area, -1 being the bottom of the display area) to array element indices. For example: {0: [0, 0], -1: [10, 0]} Otherwise, None. """ indices_top = self._show_array_index_at_line(0) output_top = self._output_top_row if self._main_menu_pad: output_top += 1 bottom_line_index = ( self._output_pad_screen_location.bottom - output_top - 1) indices_bottom = self._show_array_index_at_line(bottom_line_index) if indices_top or indices_bottom: return {0: indices_top, -1: indices_bottom} else: return None def _show_array_index_at_line(self, line_index): """Show array indices for the specified line in the display area. Uses the line number to array indices map in the annotations field of the RichTextLines object being displayed. If the displayed RichTextLines object does not contain such a mapping, will do nothing. Args: line_index: (int) 0-based line index from the top of the display area. For example,if line_index == 0, this method will display the array indices for the line currently at the top of the display area. Returns: (list) The array indices at the specified line, if available. None, if not available. """ # Examine whether the index information is available for the specified line # number. pointer = self._output_pad_row + line_index if (pointer in self._curr_wrapped_output.annotations and "i0" in self._curr_wrapped_output.annotations[pointer]): indices = self._curr_wrapped_output.annotations[pointer]["i0"] array_indices_str = self._format_indices(indices) array_indices_info = "@" + array_indices_str # TODO(cais): Determine line_index properly given menu pad status. # Test coverage? output_top = self._output_top_row if self._main_menu_pad: output_top += 1 self._toast( array_indices_info, color=self._ARRAY_INDICES_COLOR_PAIR, line_index=output_top + line_index) return indices else: return None def _tab_complete(self, command_str): """Perform tab completion. Obtains tab completion candidates. If there are no candidates, return command_str and take no other actions. If there are candidates, display the candidates on screen and return command_str + (common prefix of the candidates). Args: command_str: (str) The str in the command input textbox when Tab key is hit. Returns: (str) Completed string. Could be the same as command_str if no completion candidate is available. If candidate(s) are available, return command_str appended by the common prefix of the candidates. """ context, prefix, except_last_word = self._analyze_tab_complete_input( command_str) candidates, common_prefix = self._tab_completion_registry.get_completions( context, prefix) if candidates and len(candidates) > 1: self._display_candidates(candidates) else: # In the case of len(candidates) == 1, the single completion will be # entered to the textbox automatically. So there is no need to show any # candidates. self._display_candidates([]) if common_prefix: # Common prefix is not None and non-empty. The completed string will # incorporate the common prefix. return except_last_word + common_prefix else: return except_last_word + prefix def _display_candidates(self, candidates): """Show candidates (e.g., tab-completion candidates) on multiple lines. Args: candidates: (list of str) candidates. """ if self._curr_unwrapped_output: # Force refresh screen output. self._scroll_output(_SCROLL_REFRESH) if not candidates: return candidates_prefix = "Candidates: " candidates_line = candidates_prefix + " ".join(candidates) candidates_output = debugger_cli_common.RichTextLines( candidates_line, font_attr_segs={ 0: [(len(candidates_prefix), len(candidates_line), "yellow")] }) candidates_output, _ = debugger_cli_common.wrap_rich_text_lines( candidates_output, self._max_x - 3) # Calculate how many lines the candidate text should occupy. Limit it to # a maximum value. candidates_num_rows = min( len(candidates_output.lines), self._candidates_max_lines) self._candidates_top_row = ( self._candidates_bottom_row - candidates_num_rows + 1) # Render the candidate text on screen. pad, _, _ = self._display_lines(candidates_output, 0) self._screen_scroll_output_pad( pad, 0, 0, self._candidates_top_row, 0, self._candidates_top_row + candidates_num_rows - 1, self._max_x - 2) def _toast(self, message, color=None, line_index=None): """Display a one-line message on the screen. By default, the toast is displayed in the line right above the scroll bar. But the line location can be overridden with the line_index arg. Args: message: (str) the message to display. color: (str) optional color attribute for the message. line_index: (int) line index. """ pad, _, _ = self._display_lines( debugger_cli_common.RichTextLines( message, font_attr_segs={ 0: [(0, len(message), color or cli_shared.COLOR_WHITE)]}), 0) right_end = min(len(message), self._max_x - 2) if line_index is None: line_index = self._output_scroll_row - 1 self._screen_scroll_output_pad(pad, 0, 0, line_index, 0, line_index, right_end) def _error_toast(self, message): """Display a one-line error message on screen. Args: message: The error message, without the preceding "ERROR: " substring. """ self._toast( self.ERROR_MESSAGE_PREFIX + message, color=self._ERROR_TOAST_COLOR_PAIR) def _info_toast(self, message): """Display a one-line informational message on screen. Args: message: The informational message. """ self._toast( self.INFO_MESSAGE_PREFIX + message, color=self._INFO_TOAST_COLOR_PAIR) def _interrupt_handler(self, signal_num, frame): del signal_num # Unused. del frame # Unused. if self._on_ui_exit: self._on_ui_exit() self._screen_terminate() print("\ntfdbg: caught SIGINT; calling sys.exit(1).", file=sys.stderr) sys.exit(1) def _mouse_mode_command_handler(self, args, screen_info=None): """Handler for the command prefix 'mouse'. Args: args: (list of str) Arguments to the command prefix 'mouse'. screen_info: (dict) Information about the screen, unused by this handler. Returns: None, as this command handler does not generate any screen outputs other than toasts. """ del screen_info if not args or len(args) == 1: if args: if args[0].lower() == "on": enabled = True elif args[0].lower() == "off": enabled = False else: self._error_toast("Invalid mouse mode: %s" % args[0]) return None self._set_mouse_enabled(enabled) mode_str = "on" if self._mouse_enabled else "off" self._info_toast("Mouse mode: %s" % mode_str) else: self._error_toast("mouse_mode: syntax error") return None def _set_mouse_enabled(self, enabled): if self._mouse_enabled != enabled: self._mouse_enabled = enabled self._screen_set_mousemask() self._redraw_output() def _screen_set_mousemask(self): if self._mouse_enabled: curses.mousemask(curses.BUTTON1_RELEASED | curses.BUTTON1_PRESSED) else: curses.mousemask(0)